From 030a42291893e27c032d2f96a4aa43a7ba9493a7 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 20 Sep 2025 12:30:52 -0400 Subject: [PATCH 001/247] sdk: rm unused param --- sdk/src/driftClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 01dfa60030..2033723b86 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -9241,7 +9241,6 @@ export class DriftClient { public async updateUserGovTokenInsuranceStake( authority: PublicKey, txParams?: TxParams, - env: DriftEnv = 'mainnet-beta' ): Promise { const ix = await this.getUpdateUserGovTokenInsuranceStakeIx(authority); const tx = await this.buildTransaction(ix, txParams); From fb9a8761a7bea005056f9d3e59257edc23038475 Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Mon, 22 Sep 2025 07:54:41 +0800 Subject: [PATCH 002/247] fix comments (#1844) --- sdk/src/swift/swiftOrderSubscriber.ts | 55 ++++++++++++++++++++------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/sdk/src/swift/swiftOrderSubscriber.ts b/sdk/src/swift/swiftOrderSubscriber.ts index bab3032795..c0555b8111 100644 --- a/sdk/src/swift/swiftOrderSubscriber.ts +++ b/sdk/src/swift/swiftOrderSubscriber.ts @@ -41,6 +41,27 @@ export type SwiftOrderSubscriberConfig = { keypair: Keypair; }; +/** + * Swift order message received from WebSocket + */ +export interface SwiftOrderMessage { + /** Hex string of the order message */ + order_message: string; + /** Base58 string of taker authority */ + taker_authority: string; + /** Base58 string of signing authority */ + signing_authority: string; + /** Base64 string containing the order signature */ + order_signature: string; + /** Swift order UUID */ + uuid: string; + /** Whether the order auction params are likely to be sanitized on submission to program */ + will_sanitize?: boolean; + /** Base64 string of a prerequisite deposit tx. The swift order_message should be bundled + * after the deposit when present */ + depositTx?: string; +} + export class SwiftOrderSubscriber { private heartbeatTimeout: ReturnType | null = null; private readonly heartbeatIntervalMs = 60000; @@ -48,7 +69,7 @@ export class SwiftOrderSubscriber { private driftClient: DriftClient; public userAccountGetter?: AccountGetter; // In practice, this for now is just an OrderSubscriber or a UserMap public onOrder: ( - orderMessageRaw: any, + orderMessageRaw: SwiftOrderMessage, signedMessage: | SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, @@ -120,13 +141,14 @@ export class SwiftOrderSubscriber { async subscribe( onOrder: ( - orderMessageRaw: any, + orderMessageRaw: SwiftOrderMessage, signedMessage: | SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, isDelegateSigner?: boolean ) => Promise, - acceptSanitized = false + acceptSanitized = false, + acceptDepositTrade = false ): Promise { this.onOrder = onOrder; @@ -150,13 +172,20 @@ export class SwiftOrderSubscriber { } if (message['order']) { - const order = message['order']; + const order = message['order'] as SwiftOrderMessage; // ignore likely sanitized orders by default - if (order['will_sanitize'] === true && !acceptSanitized) { + if (order.will_sanitize === true && !acceptSanitized) { return; } + // order has a prerequisite deposit tx attached + if (message['deposit']) { + order.depositTx = message['deposit']; + if (!acceptDepositTrade) { + return; + } + } const signedMsgOrderParamsBuf = Buffer.from( - order['order_message'], + order.order_message, 'hex' ); const isDelegateSigner = signedMsgOrderParamsBuf @@ -224,7 +253,7 @@ export class SwiftOrderSubscriber { } async getPlaceAndMakeSignedMsgOrderIxs( - orderMessageRaw: any, + orderMessageRaw: SwiftOrderMessage, signedMsgOrderParamsMessage: | SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, @@ -235,7 +264,7 @@ export class SwiftOrderSubscriber { } const signedMsgOrderParamsBuf = Buffer.from( - orderMessageRaw['order_message'], + orderMessageRaw.order_message, 'hex' ); @@ -256,10 +285,8 @@ export class SwiftOrderSubscriber { isDelegateSigner ); - const takerAuthority = new PublicKey(orderMessageRaw['taker_authority']); - const signingAuthority = new PublicKey( - orderMessageRaw['signing_authority'] - ); + const takerAuthority = new PublicKey(orderMessageRaw.taker_authority); + const signingAuthority = new PublicKey(orderMessageRaw.signing_authority); const takerUserPubkey = isDelegateSigner ? (signedMessage as SignedMsgOrderParamsDelegateMessage).takerPubkey : await getUserAccountPublicKey( @@ -273,9 +300,9 @@ export class SwiftOrderSubscriber { const ixs = await this.driftClient.getPlaceAndMakeSignedMsgPerpOrderIxs( { orderParams: signedMsgOrderParamsBuf, - signature: Buffer.from(orderMessageRaw['order_signature'], 'base64'), + signature: Buffer.from(orderMessageRaw.order_signature, 'base64'), }, - decodeUTF8(orderMessageRaw['uuid']), + decodeUTF8(orderMessageRaw.uuid), { taker: takerUserPubkey, takerUserAccount, From 2cdb0376422f8b1a9b3088a95c024d5021ebac08 Mon Sep 17 00:00:00 2001 From: Jack Waller Date: Mon, 22 Sep 2025 15:57:02 +1000 Subject: [PATCH 003/247] chore: update laser 0.1.8 --- sdk/package.json | 2 +- .../accounts/laserProgramAccountSubscriber.ts | 18 +++-- sdk/src/isomorphic/grpc.node.ts | 15 ++++ sdk/yarn.lock | 80 +++++++++---------- 4 files changed, 66 insertions(+), 49 deletions(-) diff --git a/sdk/package.json b/sdk/package.json index 9c6d00acca..acad577968 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -55,7 +55,7 @@ "@triton-one/yellowstone-grpc": "1.3.0", "anchor-bankrun": "0.3.0", "gill": "^0.10.2", - "helius-laserstream": "^0.1.7", + "helius-laserstream": "0.1.8", "nanoid": "3.3.4", "node-cache": "5.1.2", "rpc-websockets": "7.5.1", diff --git a/sdk/src/accounts/laserProgramAccountSubscriber.ts b/sdk/src/accounts/laserProgramAccountSubscriber.ts index babfb1704f..e10239a2cc 100644 --- a/sdk/src/accounts/laserProgramAccountSubscriber.ts +++ b/sdk/src/accounts/laserProgramAccountSubscriber.ts @@ -5,13 +5,13 @@ import * as Buffer from 'buffer'; import { WebSocketProgramAccountSubscriber } from './webSocketProgramAccountSubscriber'; import { - CommitmentLevel as LaserCommitmentLevel, - subscribe as LaserSubscribe, + LaserCommitmentLevel, + LaserSubscribe, LaserstreamConfig, - SubscribeRequest, - SubscribeUpdate, + LaserSubscribeRequest, + LaserSubscribeUpdate, CompressionAlgorithms, -} from 'helius-laserstream'; +} from '../isomorphic/grpc.node'; import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; type LaserCommitment = @@ -24,7 +24,7 @@ export class LaserstreamProgramAccountSubscriber< | { id: string; cancel: () => void; - write?: (req: SubscribeRequest) => Promise; + write?: (req: LaserSubscribeRequest) => Promise; } | undefined; @@ -66,6 +66,8 @@ export class LaserstreamProgramAccountSubscriber< }, resubOpts?: ResubOpts ): Promise> { + console.log('using laser stream client'); + const laserConfig: LaserstreamConfig = { apiKey: grpcConfigs.token, endpoint: grpcConfigs.endpoint, @@ -112,7 +114,7 @@ export class LaserstreamProgramAccountSubscriber< }; }); - const request: SubscribeRequest = { + const request: LaserSubscribeRequest = { slots: {}, accounts: { drift: { @@ -134,7 +136,7 @@ export class LaserstreamProgramAccountSubscriber< const stream = await LaserSubscribe( this.laserConfig, request, - async (update: SubscribeUpdate) => { + async (update: LaserSubscribeUpdate) => { if (update.account) { const slot = Number(update.account.slot); const acc = update.account.account; diff --git a/sdk/src/isomorphic/grpc.node.ts b/sdk/src/isomorphic/grpc.node.ts index 4d58f734d1..8652858ef0 100644 --- a/sdk/src/isomorphic/grpc.node.ts +++ b/sdk/src/isomorphic/grpc.node.ts @@ -6,6 +6,15 @@ import type { } from '@triton-one/yellowstone-grpc'; import { ClientDuplexStream, ChannelOptions } from '@grpc/grpc-js'; +import { + CommitmentLevel as LaserCommitmentLevel, + subscribe as LaserSubscribe, + LaserstreamConfig, + SubscribeRequest as LaserSubscribeRequest, + SubscribeUpdate as LaserSubscribeUpdate, + CompressionAlgorithms, +} from 'helius-laserstream'; + export { ClientDuplexStream, ChannelOptions, @@ -13,6 +22,12 @@ export { SubscribeUpdate, CommitmentLevel, Client, + LaserSubscribe, + LaserCommitmentLevel, + LaserstreamConfig, + LaserSubscribeRequest, + LaserSubscribeUpdate, + CompressionAlgorithms, }; // Export a function to create a new Client instance diff --git a/sdk/yarn.lock b/sdk/yarn.lock index fdcc859a96..a18bd777e8 100644 --- a/sdk/yarn.lock +++ b/sdk/yarn.lock @@ -2794,50 +2794,50 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -helius-laserstream-darwin-arm64@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/helius-laserstream-darwin-arm64/-/helius-laserstream-darwin-arm64-0.1.7.tgz#c84df402bdb8a2159bcfc2711bf2b64fe09edd24" - integrity sha512-oMkt6qr7EQLfgiOCVO/9lTQLi8futBVqhUSRvsCmYcAqmaFNsvmf+/rRVq/o56+iq0PseqV6DcRF+5s88tYEIg== - -helius-laserstream-darwin-x64@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/helius-laserstream-darwin-x64/-/helius-laserstream-darwin-x64-0.1.7.tgz#fe15b0513a8d1f55075f27b1a5836bf73ec470ff" - integrity sha512-88utpg/ZMtsnF9RX268D50fl6B3kQOFS0nCzoenS1CwMvMprTqHSEDqYFgBX8O7t52gox5aw8+x3XqhHX0fIMQ== - -helius-laserstream-linux-arm64-gnu@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/helius-laserstream-linux-arm64-gnu/-/helius-laserstream-linux-arm64-gnu-0.1.7.tgz#cc7a636bc12961d314a4c46008d63f4fb6fe6b03" - integrity sha512-4YQuISaa3OWOBQCUnslT+HguuGRMO1KRQeWSjtuSHDYn7oO/KXePCtEo9vqnQx7HtQFDnS1/kuFExIa2L4Sp2w== - -helius-laserstream-linux-arm64-musl@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/helius-laserstream-linux-arm64-musl/-/helius-laserstream-linux-arm64-musl-0.1.7.tgz#96dcf87b855698e9f28214d96031d0934324db45" - integrity sha512-VyN/5nzqUtBN88PLDIMVJmFDgImMCL1sjBWKf49ppAWKN3LVvYVYT8tGMi4nzb5vj7ObduIi1ZZ+cGWNM6H2kA== - -helius-laserstream-linux-x64-gnu@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/helius-laserstream-linux-x64-gnu/-/helius-laserstream-linux-x64-gnu-0.1.7.tgz#0bcaed9697b975033e86a74ab4d734ee3d4d3e1c" - integrity sha512-0//p5wlITWbWKBaW2CIYfS3/9fBJCvMn8fBvBKot28psIWSQ6Uc5u/IqS2ls438NvTiEvBp6pgScWoYHXKU+VQ== - -helius-laserstream-linux-x64-musl@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/helius-laserstream-linux-x64-musl/-/helius-laserstream-linux-x64-musl-0.1.7.tgz#6c38ca7f97bd1ff46947794fd907ecd8492a0249" - integrity sha512-4VFxsKE+X3Jj/DBrdKQCUs/6ljuYadiPrF5QBMIyaflzZka/hOOvd2bDAo8Bi9/qGGCCaJG6F3U3OFKGKSiE9w== - -helius-laserstream@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/helius-laserstream/-/helius-laserstream-0.1.7.tgz#67f9d570f56ba9bb801d210ebff5e9cf1cc97faa" - integrity sha512-xsCbc8dApJpLb6OShOCeJ5/6pQMdGk6sQojEgihTNGZaGhaAzwYzJcxL5q4uszE3qG/viJO67Mi/MxArDb+QaQ== +helius-laserstream-darwin-arm64@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-darwin-arm64/-/helius-laserstream-darwin-arm64-0.1.8.tgz#d78ad15e6cd16dc9379a9a365f9fcb3f958e6c01" + integrity sha512-p/K2Mi3wZnMxEYSLCvu858VyMvtJFonhdF8cLaMcszFv04WWdsK+hINNZpVRfakypvDfDPbMudEhL1Q9USD5+w== + +helius-laserstream-darwin-x64@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-darwin-x64/-/helius-laserstream-darwin-x64-0.1.8.tgz#e57bc8f03135fd3b5c01a5aebd7b87c42129da50" + integrity sha512-Hd5irFyfOqQZLdoj5a+OV7vML2YfySSBuKlOwtisMHkUuIXZ4NpAexslDmK7iP5VWRI+lOv9X/tA7BhxW7RGSQ== + +helius-laserstream-linux-arm64-gnu@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-arm64-gnu/-/helius-laserstream-linux-arm64-gnu-0.1.8.tgz#1b3c8440804d143f650166842620fc334f9c319b" + integrity sha512-PlPm1dvOvTGBL1nuzK98Xe40BJq1JWNREXlBHKDVA/B+KCGQnIMJ1s6e1MevSvFE7SOix5i1BxhYIxGioK2GMg== + +helius-laserstream-linux-arm64-musl@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-arm64-musl/-/helius-laserstream-linux-arm64-musl-0.1.8.tgz#28e0645bebc3253d2a136cf0bd13f8cb5256f47b" + integrity sha512-LFadfMRuTd1zo6RZqLTgHQapo3gJYioS7wFMWFoBOFulG0BpAqHEDNobkxx0002QArw+zX29MQ/5OaOCf8kKTA== + +helius-laserstream-linux-x64-gnu@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-x64-gnu/-/helius-laserstream-linux-x64-gnu-0.1.8.tgz#e59990ca0bcdc27e46f71a8fc2c18fddbe6f07e3" + integrity sha512-IZWK/OQIe0647QqfYikLb1DFK+nYtXLJiMcpj24qnNVWBOtMXmPc1hL6ebazdEiaKt7fxNd5IiM1RqeaqZAZMw== + +helius-laserstream-linux-x64-musl@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-x64-musl/-/helius-laserstream-linux-x64-musl-0.1.8.tgz#42aa0919ef266c40f50ac74d6f9d871d4e2e7c9c" + integrity sha512-riTS6VgxDae1fHOJ2XC/o/v1OZRbEv/3rcoa3NlAOnooDKp5HDgD0zJTcImjQHpYWwGaejx1oX/Ht53lxNoijw== + +helius-laserstream@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream/-/helius-laserstream-0.1.8.tgz#6ee5e0bc9fe2560c03a0d2c9079b9f875c3e6bb7" + integrity sha512-jXQkwQRWiowbVPGQrGacOkI5shKPhrEixCu93OpoMtL5fs9mnhtD7XKMPi8CX0W8gsqsJjwR4NlaR+EflyANbQ== dependencies: "@types/protobufjs" "^6.0.0" protobufjs "^7.5.3" optionalDependencies: - helius-laserstream-darwin-arm64 "0.1.7" - helius-laserstream-darwin-x64 "0.1.7" - helius-laserstream-linux-arm64-gnu "0.1.7" - helius-laserstream-linux-arm64-musl "0.1.7" - helius-laserstream-linux-x64-gnu "0.1.7" - helius-laserstream-linux-x64-musl "0.1.7" + helius-laserstream-darwin-arm64 "0.1.8" + helius-laserstream-darwin-x64 "0.1.8" + helius-laserstream-linux-arm64-gnu "0.1.8" + helius-laserstream-linux-arm64-musl "0.1.8" + helius-laserstream-linux-x64-gnu "0.1.8" + helius-laserstream-linux-x64-musl "0.1.8" humanize-ms@^1.2.1: version "1.2.1" From 8b681355df9a6b1ef9d8d9598efa26760e1a3396 Mon Sep 17 00:00:00 2001 From: Jack Waller Date: Mon, 22 Sep 2025 15:59:46 +1000 Subject: [PATCH 004/247] chore: remove logging --- sdk/src/accounts/laserProgramAccountSubscriber.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/src/accounts/laserProgramAccountSubscriber.ts b/sdk/src/accounts/laserProgramAccountSubscriber.ts index e10239a2cc..914cf1f34a 100644 --- a/sdk/src/accounts/laserProgramAccountSubscriber.ts +++ b/sdk/src/accounts/laserProgramAccountSubscriber.ts @@ -66,8 +66,6 @@ export class LaserstreamProgramAccountSubscriber< }, resubOpts?: ResubOpts ): Promise> { - console.log('using laser stream client'); - const laserConfig: LaserstreamConfig = { apiKey: grpcConfigs.token, endpoint: grpcConfigs.endpoint, From d4288faa8484b9f4d2c80910251663152fa892da Mon Sep 17 00:00:00 2001 From: lil perp Date: Mon, 22 Sep 2025 10:02:09 -0400 Subject: [PATCH 005/247] program: tweak ResizeSignedMsgUserOrders (#1898) --- programs/drift/src/instructions/user.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 2b4a90e19f..8bfdd10066 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -4008,6 +4008,9 @@ pub struct ResizeSignedMsgUserOrders<'info> { pub signed_msg_user_orders: Box>, /// CHECK: authority pub authority: AccountInfo<'info>, + #[account( + has_one = authority + )] pub user: AccountLoader<'info, User>, #[account(mut)] pub payer: Signer<'info>, From 61dd43fc771f24c5ada1ca24ee2fc7858138813f Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 22 Sep 2025 07:28:00 -0700 Subject: [PATCH 006/247] fix linter and cargo test --- package.json | 2 +- programs/drift/src/state/perp_market.rs | 2 +- sdk/src/driftClient.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index de9fdd10f5..854fea3020 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "prettify:fix": "prettier --write './sdk/src/**/*.ts' './tests/**.ts' './cli/**.ts'", "lint": "eslint . --ext ts --quiet --format unix", "lint:fix": "eslint . --ext ts --fix", - "update-idl": "cp target/idl/drift.json sdk/src/idl/drift.json" + "update-idl": "anchor build -- --features anchor-test && cp target/idl/drift.json sdk/src/idl/drift.json" }, "engines": { "node": ">=12" diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index fb977be6dd..6136d05ba3 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -18,7 +18,7 @@ use crate::math::constants::{ LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, MARGIN_PRECISION, MARGIN_PRECISION_U128, MAX_LIQUIDATION_MULTIPLIER, PEG_PRECISION, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, - SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, + PRICE_PRECISION_I64, SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, }; use crate::math::margin::{ calculate_size_discount_asset_weight, calculate_size_premium_liability_weight, diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 2033723b86..4d39ae6d0f 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -9240,7 +9240,7 @@ export class DriftClient { public async updateUserGovTokenInsuranceStake( authority: PublicKey, - txParams?: TxParams, + txParams?: TxParams ): Promise { const ix = await this.getUpdateUserGovTokenInsuranceStakeIx(authority); const tx = await this.buildTransaction(ix, txParams); From 732e6ec70420e67a4cd9653a0226523e20026533 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 22 Sep 2025 07:35:53 -0700 Subject: [PATCH 007/247] fix cargo build errors --- programs/drift/src/state/perp_market.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 6136d05ba3..815fa83fc2 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -10,7 +10,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::amm; use crate::math::casting::Cast; #[cfg(test)] -use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT}; +use crate::math::constants::{ + AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64, +}; use crate::math::constants::{ AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, @@ -18,7 +20,7 @@ use crate::math::constants::{ LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, MARGIN_PRECISION, MARGIN_PRECISION_U128, MAX_LIQUIDATION_MULTIPLIER, PEG_PRECISION, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, - PRICE_PRECISION_I64, SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, + SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, }; use crate::math::margin::{ calculate_size_discount_asset_weight, calculate_size_premium_liability_weight, From 14c484323cabff1d5d1488fed3717d5977810b24 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 22 Sep 2025 07:36:26 -0700 Subject: [PATCH 008/247] v2.138.0 --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 7 ++----- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0dbcfc239..aca1ca772c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +### Fixes + +### Breaking + +## [2.138.0] - 2025-09-22 + +### Features + - program: support scaled ui extension ([#1894](https://github.com/drift-labs/protocol-v2/pull/1894)) +- Revert "Crispeaney/revert swift max margin ratio ([#1877](https://github.com/drift-labs/protocol-v2/pull/1877)) ### Fixes diff --git a/Cargo.lock b/Cargo.lock index 58aef0bce6..9ca9b1301f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.137.0" +version = "2.138.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 945e055dee..39d0571545 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.137.0" +version = "2.138.0" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index 9f2067b649..c5f4056243 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.138.0-beta.9", + "version": "2.138.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 3fab52decf..6c25dcaf1c 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.137.0", + "version": "2.138.0", "name": "drift", "instructions": [ { @@ -16218,8 +16218,5 @@ "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" } - ], - "metadata": { - "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" - } + ] } \ No newline at end of file From e91800ebc24dc098c3ae1a3b406bf3c0c0030b26 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:45:00 +0000 Subject: [PATCH 009/247] sdk: release v2.139.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index b0c8a5df9a..2f0921947c 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.138.0-beta.9 \ No newline at end of file +2.139.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c5f4056243..3f18133b90 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.138.0", + "version": "2.139.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 6cce7e5207d9cbf3db39dbd419f4697e33828b5f Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:10:36 -0400 Subject: [PATCH 010/247] program: init-delegated-if-stake (#1859) * program: init-delegated-if-stake * add sdk * CHANGELOG --------- Co-authored-by: Chris Heaney --- CHANGELOG.md | 2 + programs/drift/src/instructions/admin.rs | 69 ++++++++++++++++++++++-- programs/drift/src/lib.rs | 7 ++- sdk/src/adminClient.ts | 51 ++++++++++++++++++ 4 files changed, 123 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aca1ca772c..8ba5081dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: add delegate stake if ([#1859](https://github.com/drift-labs/protocol-v2/pull/1859)) + ### Fixes ### Breaking diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index e552576f63..9f29229fe4 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -18,11 +18,12 @@ use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::{ AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO, DEFAULT_LIQUIDATION_MARGIN_BUFFER_RATIO, - FEE_POOL_TO_REVENUE_POOL_THRESHOLD, IF_FACTOR_PRECISION, INSURANCE_A_MAX, INSURANCE_B_MAX, - INSURANCE_C_MAX, INSURANCE_SPECULATIVE_MAX, LIQUIDATION_FEE_PRECISION, - MAX_CONCENTRATION_COEFFICIENT, MAX_SQRT_K, MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, - PERCENTAGE_PRECISION_I64, QUOTE_SPOT_MARKET_INDEX, SPOT_CUMULATIVE_INTEREST_PRECISION, - SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, TWENTY_FOUR_HOUR, + FEE_POOL_TO_REVENUE_POOL_THRESHOLD, GOV_SPOT_MARKET_INDEX, IF_FACTOR_PRECISION, + INSURANCE_A_MAX, INSURANCE_B_MAX, INSURANCE_C_MAX, INSURANCE_SPECULATIVE_MAX, + LIQUIDATION_FEE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, MAX_SQRT_K, + MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, + QUOTE_SPOT_MARKET_INDEX, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_IMF_PRECISION, + SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, TWENTY_FOUR_HOUR, }; use crate::math::cp_curve::get_update_k_result; use crate::math::helpers::get_proportion_u128; @@ -45,6 +46,7 @@ use crate::state::fulfillment_params::serum::SerumContext; use crate::state::fulfillment_params::serum::SerumV3FulfillmentConfig; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use crate::state::if_rebalance_config::{IfRebalanceConfig, IfRebalanceConfigParams}; +use crate::state::insurance_fund_stake::InsuranceFundStake; use crate::state::insurance_fund_stake::ProtocolIfSharesTransferConfig; use crate::state::oracle::get_sb_on_demand_price; use crate::state::oracle::{ @@ -4915,6 +4917,39 @@ pub fn handle_update_feature_bit_flags_median_trigger_price( Ok(()) } +pub fn handle_update_delegate_user_gov_token_insurance_stake( + ctx: Context, +) -> Result<()> { + let insurance_fund_stake = &mut load_mut!(ctx.accounts.insurance_fund_stake)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; + + validate!( + insurance_fund_stake.market_index == GOV_SPOT_MARKET_INDEX, + ErrorCode::IncorrectSpotMarketAccountPassed, + "insurance_fund_stake is not for governance market index = {}", + GOV_SPOT_MARKET_INDEX + )?; + + if insurance_fund_stake.market_index == GOV_SPOT_MARKET_INDEX + && spot_market.market_index == GOV_SPOT_MARKET_INDEX + { + let clock = Clock::get()?; + let now = clock.unix_timestamp; + + crate::controller::insurance::update_user_stats_if_stake_amount( + 0, + ctx.accounts.insurance_fund_vault.amount, + insurance_fund_stake, + user_stats, + spot_market, + now, + )?; + } + + Ok(()) +} + #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] @@ -5759,3 +5794,27 @@ pub struct UpdateIfRebalanceConfig<'info> { )] pub state: Box>, } + +#[derive(Accounts)] +pub struct UpdateDelegateUserGovTokenInsuranceStake<'info> { + #[account( + mut, + seeds = [b"spot_market", 15_u16.to_le_bytes().as_ref()], + bump + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + pub insurance_fund_stake: AccountLoader<'info, InsuranceFundStake>, + #[account(mut)] + pub user_stats: AccountLoader<'info, UserStats>, + pub admin: Signer<'info>, + #[account( + mut, + seeds = [b"insurance_fund_vault".as_ref(), 15_u16.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_vault: Box>, + #[account( + has_one = admin + )] + pub state: Box>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 940129cfd1..517ddb2c86 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -725,6 +725,7 @@ pub mod drift { handle_update_spot_market_expiry(ctx, expiry_ts) } + // IF stakers pub fn update_user_quote_asset_insurance_stake( ctx: Context, ) -> Result<()> { @@ -737,7 +738,11 @@ pub mod drift { handle_update_user_gov_token_insurance_stake(ctx) } - // IF stakers + pub fn update_delegate_user_gov_token_insurance_stake( + ctx: Context, + ) -> Result<()> { + handle_update_delegate_user_gov_token_insurance_stake(ctx) + } pub fn initialize_insurance_fund_stake( ctx: Context, diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 352e051bf2..030677e254 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -39,6 +39,7 @@ import { getFuelOverflowAccountPublicKey, getTokenProgramForSpotMarket, getIfRebalanceConfigPublicKey, + getInsuranceFundStakeAccountPublicKey, } from './addresses/pda'; import { squareRootBN } from './math/utils'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; @@ -50,6 +51,7 @@ import { ONE, BASE_PRECISION, PRICE_PRECISION, + GOV_SPOT_MARKET_INDEX, } from './constants/numericConstants'; import { calculateTargetPriceTrade } from './math/trade'; import { calculateAmmReservesAfterSwap, getSwapDirection } from './math/amm'; @@ -4649,4 +4651,53 @@ export class AdminClient extends DriftClient { } ); } + + public async updateDelegateUserGovTokenInsuranceStake( + authority: PublicKey, + delegate: PublicKey + ): Promise { + const updateDelegateUserGovTokenInsuranceStakeIx = + await this.getUpdateDelegateUserGovTokenInsuranceStakeIx( + authority, + delegate + ); + + const tx = await this.buildTransaction( + updateDelegateUserGovTokenInsuranceStakeIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateDelegateUserGovTokenInsuranceStakeIx( + authority: PublicKey, + delegate: PublicKey + ): Promise { + const marketIndex = GOV_SPOT_MARKET_INDEX; + const spotMarket = this.getSpotMarketAccount(marketIndex); + const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( + this.program.programId, + delegate, + marketIndex + ); + const userStatsPublicKey = getUserStatsAccountPublicKey( + this.program.programId, + authority + ); + + const ix = + this.program.instruction.getUpdateDelegateUserGovTokenInsuranceStakeIx({ + accounts: { + state: await this.getStatePublicKey(), + spotMarket: spotMarket.pubkey, + insuranceFundStake: ifStakeAccountPublicKey, + userStats: userStatsPublicKey, + signer: this.wallet.publicKey, + insuranceFundVault: spotMarket.insuranceFund.vault, + }, + }); + + return ix; + } } From 88b10c5e4011e43e5d5ab78b00dd4ef69c2367e7 Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:14:00 -0400 Subject: [PATCH 011/247] program: auction-order-params-on-slow-fast-twap-divergence (#1882) * program: auction-order-params-on-slow-fast-twap-divergence * change tests * rm dlog * CHANGELOG * cargo fmt -- --------- Co-authored-by: Chris Heaney --- CHANGELOG.md | 1 + programs/drift/src/state/order_params.rs | 68 ++++++++++-------- .../drift/src/state/order_params/tests.rs | 69 ++++++++++++------- 3 files changed, 82 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba5081dab..f66acb9f71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: auction order params account for twap divergence ([#1882](https://github.com/drift-labs/protocol-v2/pull/1882)) - program: add delegate stake if ([#1859](https://github.com/drift-labs/protocol-v2/pull/1859)) ### Fixes diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 69e1c6710d..3b3431a38c 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -702,13 +702,6 @@ impl OrderParams { } .cast::()?; - let baseline_start_price_offset_slow = mark_twap_slow.safe_sub( - perp_market - .amm - .historical_oracle_data - .last_oracle_price_twap, - )?; - let baseline_start_price_offset_fast = perp_market .amm .last_mark_price_twap_5min @@ -720,27 +713,42 @@ impl OrderParams { .last_oracle_price_twap_5min, )?; - let frac_of_long_spread_in_price: i64 = perp_market - .amm - .long_spread - .cast::()? - .safe_mul(mark_twap_slow)? - .safe_div(PRICE_PRECISION_I64 * 10)?; + let baseline_start_price_offset_slow = mark_twap_slow.safe_sub( + perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap, + )?; - let frac_of_short_spread_in_price: i64 = perp_market - .amm - .short_spread - .cast::()? - .safe_mul(mark_twap_slow)? - .safe_div(PRICE_PRECISION_I64 * 10)?; - - let baseline_start_price_offset = match direction { - PositionDirection::Long => baseline_start_price_offset_slow - .safe_add(frac_of_long_spread_in_price)? - .min(baseline_start_price_offset_fast.safe_sub(frac_of_short_spread_in_price)?), - PositionDirection::Short => baseline_start_price_offset_slow - .safe_sub(frac_of_short_spread_in_price)? - .max(baseline_start_price_offset_fast.safe_add(frac_of_long_spread_in_price)?), + let baseline_start_price_offset = if baseline_start_price_offset_slow + .abs_diff(baseline_start_price_offset_fast) + <= perp_market.amm.last_mark_price_twap_5min / 200 + { + let frac_of_long_spread_in_price: i64 = perp_market + .amm + .long_spread + .cast::()? + .safe_mul(mark_twap_slow)? + .safe_div(PRICE_PRECISION_I64 * 10)?; + + let frac_of_short_spread_in_price: i64 = perp_market + .amm + .short_spread + .cast::()? + .safe_mul(mark_twap_slow)? + .safe_div(PRICE_PRECISION_I64 * 10)?; + + match direction { + PositionDirection::Long => baseline_start_price_offset_slow + .safe_add(frac_of_long_spread_in_price)? + .min(baseline_start_price_offset_fast.safe_sub(frac_of_short_spread_in_price)?), + PositionDirection::Short => baseline_start_price_offset_slow + .safe_sub(frac_of_short_spread_in_price)? + .max(baseline_start_price_offset_fast.safe_add(frac_of_long_spread_in_price)?), + } + } else { + // more than 50bps different of fast/slow twap, use fast only + baseline_start_price_offset_fast }; Ok(baseline_start_price_offset) @@ -891,15 +899,15 @@ fn get_auction_duration( ) -> DriftResult { let percent_diff = price_diff.safe_mul(PERCENTAGE_PRECISION_U64)?.div(price); - let slots_per_bp = if contract_tier.is_as_safe_as_contract(&ContractTier::B) { + let slots_per_pct = if contract_tier.is_as_safe_as_contract(&ContractTier::B) { 100 } else { 60 }; Ok(percent_diff - .safe_mul(slots_per_bp)? - .safe_div_ceil(PERCENTAGE_PRECISION_U64 / 100)? // 1% = 60 slots + .safe_mul(slots_per_pct)? + .safe_div_ceil(PERCENTAGE_PRECISION_U64 / 100)? // 1% = 40 slots .clamp(1, 180) as u8) // 180 slots max } diff --git a/programs/drift/src/state/order_params/tests.rs b/programs/drift/src/state/order_params/tests.rs index 5f59a6c3f3..d2d54af789 100644 --- a/programs/drift/src/state/order_params/tests.rs +++ b/programs/drift/src/state/order_params/tests.rs @@ -409,10 +409,11 @@ mod update_perp_auction_params { ..AMM::default() }; amm.last_bid_price_twap = (oracle_price * 15 / 10 - 192988) as u64; - amm.last_mark_price_twap_5min = (oracle_price * 16 / 10) as u64; + amm.last_mark_price_twap_5min = (oracle_price * 155 / 100) as u64; amm.last_ask_price_twap = (oracle_price * 16 / 10 + 192988) as u64; amm.historical_oracle_data.last_oracle_price_twap = oracle_price; amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; amm.historical_oracle_data.last_oracle_price = oracle_price; let perp_market = PerpMarket { @@ -436,7 +437,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); - assert_eq!(order_params_after.auction_start_price, Some(72_524_319)); + assert_eq!(order_params_after.auction_start_price, Some(79_750_000)); assert_eq!(order_params_after.auction_end_price, Some(90_092_988)); assert_eq!(order_params_after.auction_duration, Some(180)); } @@ -456,13 +457,13 @@ mod update_perp_auction_params { ..AMM::default() }; amm.last_bid_price_twap = (oracle_price * 99 / 100) as u64; - amm.last_mark_price_twap_5min = oracle_price as u64; amm.last_ask_price_twap = (oracle_price * 101 / 100) as u64; amm.historical_oracle_data.last_oracle_price_twap = oracle_price; amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; amm.historical_oracle_data.last_oracle_price = oracle_price; - let perp_market = PerpMarket { + let mut perp_market = PerpMarket { amm, ..PerpMarket::default() }; @@ -563,10 +564,10 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_duration, Some(175)); + assert_eq!(order_params_after.auction_duration, Some(120)); assert_eq!( order_params_after.auction_start_price, - Some(100 * PRICE_PRECISION_I64 - 901000) + Some(100 * PRICE_PRECISION_I64) ); assert_eq!( order_params_after.auction_end_price, @@ -599,20 +600,24 @@ mod update_perp_auction_params { direction: PositionDirection::Short, ..OrderParams::default() }; + + // tighten bid/ask to mark twap 5min to activate buffer + perp_market.amm.last_bid_price_twap = amm.last_mark_price_twap_5min - 100000; + perp_market.amm.last_ask_price_twap = amm.last_mark_price_twap_5min + 100000; let mut order_params_after = order_params_before; order_params_after .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_duration, Some(174)); assert_eq!( order_params_after.auction_start_price, - Some(100 * PRICE_PRECISION_I64 + 899000) // %1 / 10 = 10 bps aggression + Some(100 * PRICE_PRECISION_I64 + 100100) // a bit more passive than mid ); assert_eq!( order_params_after.auction_end_price, Some(98 * PRICE_PRECISION_I64) ); + assert_eq!(order_params_after.auction_duration, Some(127)); } #[test] @@ -786,10 +791,12 @@ mod update_perp_auction_params { }; amm.historical_oracle_data.last_oracle_price = oracle_price; amm.historical_oracle_data.last_oracle_price_twap = oracle_price - 97238; + amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price - 97238; amm.last_ask_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 217999; amm.last_bid_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 17238; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; let mut perp_market = PerpMarket { amm, @@ -812,7 +819,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), 98901080); + assert_eq!(order_params_after.auction_start_price.unwrap(), 99018698); let amm_bid_price = amm.bid_price(amm.reserve_price().unwrap()).unwrap(); assert_eq!(amm_bid_price, 98010000); assert!(order_params_after.auction_start_price.unwrap() as u64 > amm_bid_price); @@ -832,7 +839,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), 99118879); + assert_eq!(order_params_after.auction_start_price.unwrap(), 99216738); // skip for prelaunch oracle perp_market.amm.oracle_source = OracleSource::Prelaunch; @@ -893,12 +900,13 @@ mod update_perp_auction_params { ..AMM::default() }; amm.historical_oracle_data.last_oracle_price = oracle_price; + amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price - 97238; amm.historical_oracle_data.last_oracle_price_twap = oracle_price - 97238; amm.last_ask_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 217999; amm.last_bid_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 17238; - + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; let perp_market = PerpMarket { amm, contract_tier: ContractTier::B, @@ -920,7 +928,8 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), -98920); + assert_eq!(order_params_after.auction_start_price.unwrap(), 18698); + assert_eq!(order_params_after.auction_end_price.unwrap(), 2196053); let order_params_before = OrderParams { order_type: OrderType::Oracle, @@ -985,10 +994,12 @@ mod update_perp_auction_params { }; amm.historical_oracle_data.last_oracle_price = oracle_price; amm.historical_oracle_data.last_oracle_price_twap = oracle_price - 97238; + amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price - 97238; amm.last_ask_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 217999; amm.last_bid_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 17238; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; let perp_market = PerpMarket { amm, @@ -1011,7 +1022,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), 98653580); + assert_eq!(order_params_after.auction_start_price.unwrap(), 98771198); let order_params_before = OrderParams { order_type: OrderType::Market, @@ -1030,7 +1041,7 @@ mod update_perp_auction_params { assert_ne!(order_params_before, order_params_after); assert_eq!( order_params_after.auction_start_price.unwrap(), - (99 * PRICE_PRECISION_I64 - oracle_price / 400) - 98920 // approx equal with some noise + (99 * PRICE_PRECISION_I64 - oracle_price / 400 + 18698) // approx equal with some noise ); let order_params_before = OrderParams { @@ -1049,7 +1060,7 @@ mod update_perp_auction_params { .unwrap(); assert_eq!( order_params_after.auction_start_price.unwrap(), - 99118879 + oracle_price / 400 + 99118879 + oracle_price / 400 + 97859 ); let order_params_before = OrderParams { @@ -1069,7 +1080,7 @@ mod update_perp_auction_params { assert_ne!(order_params_before, order_params_after); assert_eq!( order_params_after.auction_start_price.unwrap(), - (99 * PRICE_PRECISION_U64 + 100000) as i64 + oracle_price / 400 + 18879 // use limit price and oracle buffer with some noise + (99 * PRICE_PRECISION_U64 + 100000) as i64 + oracle_price / 400 + 116738 // use limit price and oracle buffer with some noise ); let order_params_before = OrderParams { @@ -1088,11 +1099,11 @@ mod update_perp_auction_params { .unwrap(); assert_eq!( order_params_after.auction_start_price.unwrap(), - 99118879 + oracle_price / 400 + 99118879 + oracle_price / 400 + 97859 ); assert_eq!(order_params_after.auction_end_price.unwrap(), 98028211); - assert_eq!(order_params_after.auction_duration, Some(82)); + assert_eq!(order_params_after.auction_duration, Some(88)); let order_params_before = OrderParams { order_type: OrderType::Market, @@ -1110,11 +1121,11 @@ mod update_perp_auction_params { .unwrap(); assert_eq!( order_params_after.auction_start_price.unwrap(), - 98901080 - oracle_price / 400 + 98901080 - oracle_price / 400 + 117618 ); assert_eq!(order_params_after.auction_end_price.unwrap(), 100207026); - assert_eq!(order_params_after.auction_duration, Some(95)); + assert_eq!(order_params_after.auction_duration, Some(88)); } #[test] @@ -1325,8 +1336,11 @@ mod get_close_perp_params { let amm = AMM { last_ask_price_twap: 101 * PRICE_PRECISION_U64, last_bid_price_twap: 99 * PRICE_PRECISION_U64, + last_mark_price_twap_5min: 99 * PRICE_PRECISION_U64, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + last_oracle_price_twap_5min: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() }, mark_std: PRICE_PRECISION_U64, @@ -1385,7 +1399,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, PRICE_PRECISION_I64); + assert_eq!(auction_start_price, 2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, 4 * PRICE_PRECISION_I64); assert_eq!(oracle_price_offset, 4 * PRICE_PRECISION_I64 as i32); @@ -1420,7 +1434,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, -3 * PRICE_PRECISION_I64); + assert_eq!(auction_start_price, -2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, 0); assert_eq!(oracle_price_offset, 0); @@ -1436,8 +1450,11 @@ mod get_close_perp_params { let amm = AMM { last_ask_price_twap: 101 * PRICE_PRECISION_U64, last_bid_price_twap: 99 * PRICE_PRECISION_U64, + last_mark_price_twap_5min: 100 * PRICE_PRECISION_U64, + historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + last_oracle_price_twap_5min: 100 * PRICE_PRECISION_I64, ..HistoricalOracleData::default() }, mark_std: PRICE_PRECISION_U64, @@ -1462,7 +1479,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, 1000000); + assert_eq!(auction_start_price, 0); assert_eq!(auction_end_price, -2 * PRICE_PRECISION_I64); assert_eq!(oracle_price_offset, -2 * PRICE_PRECISION_I64 as i32); @@ -1498,7 +1515,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, 3 * PRICE_PRECISION_I64); + assert_eq!(auction_start_price, 2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, 0); assert_eq!(oracle_price_offset, 0); @@ -1536,7 +1553,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, -PRICE_PRECISION_I64); + assert_eq!(auction_start_price, -2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, -4 * PRICE_PRECISION_I64); assert_eq!(oracle_price_offset, -4 * PRICE_PRECISION_I64 as i32); @@ -1613,7 +1630,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, 641); + assert_eq!(auction_start_price, 284); assert_eq!(auction_end_price, -1021); assert_eq!(oracle_price_offset, -1021); From 82377466ce39286b9eb6cfadb0efed853f305abd Mon Sep 17 00:00:00 2001 From: lil perp Date: Mon, 22 Sep 2025 17:15:11 -0400 Subject: [PATCH 012/247] program: add invariant for max in amount for if swap (#1825) --- programs/drift/src/controller/insurance.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 91ad374ed5..04f98f7e89 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -1098,6 +1098,14 @@ pub fn handle_if_end_swap( if_rebalance_config.epoch_max_in_amount )?; + validate!( + if_rebalance_config.current_in_amount <= if_rebalance_config.total_in_amount, + ErrorCode::InvalidIfRebalanceSwap, + "current_in_amount={} > total_in_amount={}", + if_rebalance_config.current_in_amount, + if_rebalance_config.total_in_amount + )?; + let oracle_twap = out_spot_market .historical_oracle_data .last_oracle_price_twap; From 107f0a873d01bdf43811da3023a4434f49adb5c1 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 21:15:31 +0000 Subject: [PATCH 013/247] sdk: release v2.139.0-beta.1 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 2f0921947c..fd03ad91a0 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.0 \ No newline at end of file +2.139.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 3f18133b90..f26e7faa52 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.0", + "version": "2.139.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 0656f34328b74e1e4613ce38083e58abdc666e90 Mon Sep 17 00:00:00 2001 From: Jack Waller Date: Tue, 23 Sep 2025 09:49:32 +1000 Subject: [PATCH 014/247] chore: add grpc client to order subscriber --- sdk/src/accounts/types.ts | 1 + sdk/src/orderSubscriber/grpcSubscription.ts | 42 +++++++++++++++------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/sdk/src/accounts/types.ts b/sdk/src/accounts/types.ts index 98ab7133fb..c572ae2798 100644 --- a/sdk/src/accounts/types.ts +++ b/sdk/src/accounts/types.ts @@ -234,6 +234,7 @@ export type GrpcConfigs = { * Defaults to false, will throw on connection loss. */ enableReconnect?: boolean; + client?: 'yellowstone' | 'laser'; }; export interface HighLeverageModeConfigAccountSubscriber { diff --git a/sdk/src/orderSubscriber/grpcSubscription.ts b/sdk/src/orderSubscriber/grpcSubscription.ts index 41101435ab..04160c0bff 100644 --- a/sdk/src/orderSubscriber/grpcSubscription.ts +++ b/sdk/src/orderSubscriber/grpcSubscription.ts @@ -5,6 +5,7 @@ import { OrderSubscriber } from './OrderSubscriber'; import { GrpcConfigs, ResubOpts } from '../accounts/types'; import { UserAccount } from '../types'; import { getUserFilter, getNonIdleUserFilter } from '../memcmp'; +import { LaserstreamProgramAccountSubscriber } from '../accounts/laserProgramAccountSubscriber'; export class grpcSubscription { private orderSubscriber: OrderSubscriber; @@ -12,7 +13,9 @@ export class grpcSubscription { private resubOpts?: ResubOpts; private resyncIntervalMs?: number; - private subscriber?: grpcProgramAccountSubscriber; + private subscriber?: + | grpcProgramAccountSubscriber + | LaserstreamProgramAccountSubscriber; private resyncTimeoutId?: ReturnType; private decoded?: boolean; @@ -47,17 +50,32 @@ export class grpcSubscription { return; } - this.subscriber = await grpcProgramAccountSubscriber.create( - this.grpcConfigs, - 'OrderSubscriber', - 'User', - this.orderSubscriber.driftClient.program, - this.orderSubscriber.decodeFn, - { - filters: [getUserFilter(), getNonIdleUserFilter()], - }, - this.resubOpts - ); + if (this.grpcConfigs.client === 'laser') { + this.subscriber = + await LaserstreamProgramAccountSubscriber.create( + this.grpcConfigs, + 'OrderSubscriber', + 'User', + this.orderSubscriber.driftClient.program, + this.orderSubscriber.decodeFn, + { + filters: [getUserFilter(), getNonIdleUserFilter()], + }, + this.resubOpts + ); + } else { + this.subscriber = await grpcProgramAccountSubscriber.create( + this.grpcConfigs, + 'OrderSubscriber', + 'User', + this.orderSubscriber.driftClient.program, + this.orderSubscriber.decodeFn, + { + filters: [getUserFilter(), getNonIdleUserFilter()], + }, + this.resubOpts + ); + } await this.subscriber.subscribe( ( From 6afe376f8ce276a434e4be1184345497a5bec695 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 00:14:10 +0000 Subject: [PATCH 015/247] sdk: release v2.139.0-beta.2 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- sdk/yarn.lock | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index fd03ad91a0..efa9a6c58f 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.1 \ No newline at end of file +2.139.0-beta.2 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c5b9c00e2a..8b4c57fe98 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.1", + "version": "2.139.0-beta.2", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", diff --git a/sdk/yarn.lock b/sdk/yarn.lock index 3e80163db6..c3130ae226 100644 --- a/sdk/yarn.lock +++ b/sdk/yarn.lock @@ -1281,6 +1281,13 @@ dependencies: undici-types "~5.26.4" +"@types/protobufjs@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/protobufjs/-/protobufjs-6.0.0.tgz#aeabb43f9507bb19c8adfb479584c151082353e4" + integrity sha512-A27RDExpAf3rdDjIrHKiJK6x8kqqJ4CmoChwtipfhVAn1p7+wviQFFP7dppn8FslSbHtQeVPvi8wNKkDjSYjHw== + dependencies: + protobufjs "*" + "@types/semver@^7.5.0": version "7.7.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e" @@ -3718,6 +3725,24 @@ pretty-ms@^7.0.1: dependencies: parse-ms "^2.1.0" +protobufjs@*, protobufjs@^7.5.3: + version "7.5.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protobufjs@^7.2.5, protobufjs@^7.4.0: version "7.5.3" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.3.tgz#13f95a9e3c84669995ec3652db2ac2fb00b89363" From 763b978c046a099ba2c96b6513a0e9b7838b6b69 Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:10:07 -0400 Subject: [PATCH 016/247] sdk: add market index 76 to constant (#1901) --- sdk/src/constants/perpMarkets.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 03643e7d06..7a28100885 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1312,6 +1312,18 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ oracleSource: OracleSource.PYTH_LAZER, pythLazerId: 1578, }, + { + fullName: 'ASTER', + category: ['DEX'], + symbol: 'ASTER-PERP', + baseAssetSymbol: 'ASTER', + marketIndex: 76, + oracle: new PublicKey('T1VdaNiTLJ9gMvopnybxtbqdEAAeDQBet4JmvQ4GCP9'), + launchTs: 1758632629000, + oracleSource: OracleSource.PYTH_PULL, + pythFeedId: + '0xa903b5a82cb572397e3d47595d2889cf80513f5b4cf7a36b513ae10cc8b1e338', + }, ]; export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = { From d443dda40419d7a54e6845d8c6b46d14c0275e5c Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:15:48 +0000 Subject: [PATCH 017/247] sdk: release v2.139.0-beta.3 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index efa9a6c58f..627cb8ed50 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.2 \ No newline at end of file +2.139.0-beta.3 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 8b4c57fe98..f683e3a5b3 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.2", + "version": "2.139.0-beta.3", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From ad516ec120593ed522042a29db4f6ff1d43998b2 Mon Sep 17 00:00:00 2001 From: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:16:23 -0400 Subject: [PATCH 018/247] fix ui build (#1902) --- sdk/bun.lock | 21 +++++++++++++++++++ .../accounts/laserProgramAccountSubscriber.ts | 4 ++-- sdk/src/isomorphic/grpc.node.ts | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/sdk/bun.lock b/sdk/bun.lock index b97bf95c4b..c3292b4520 100644 --- a/sdk/bun.lock +++ b/sdk/bun.lock @@ -20,6 +20,7 @@ "@triton-one/yellowstone-grpc": "1.3.0", "anchor-bankrun": "0.3.0", "gill": "^0.10.2", + "helius-laserstream": "0.1.8", "nanoid": "3.3.4", "node-cache": "5.1.2", "rpc-websockets": "7.5.1", @@ -335,6 +336,8 @@ "@types/node": ["@types/node@22.13.8", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ=="], + "@types/protobufjs": ["@types/protobufjs@6.0.0", "", { "dependencies": { "protobufjs": "*" } }, "sha512-A27RDExpAf3rdDjIrHKiJK6x8kqqJ4CmoChwtipfhVAn1p7+wviQFFP7dppn8FslSbHtQeVPvi8wNKkDjSYjHw=="], + "@types/semver": ["@types/semver@7.7.0", "", {}, "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA=="], "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], @@ -711,6 +714,20 @@ "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "helius-laserstream": ["helius-laserstream@0.1.8", "", { "dependencies": { "@types/protobufjs": "^6.0.0", "protobufjs": "^7.5.3" }, "optionalDependencies": { "helius-laserstream-darwin-arm64": "0.1.8", "helius-laserstream-darwin-x64": "0.1.8", "helius-laserstream-linux-arm64-gnu": "0.1.8", "helius-laserstream-linux-arm64-musl": "0.1.8", "helius-laserstream-linux-x64-gnu": "0.1.8", "helius-laserstream-linux-x64-musl": "0.1.8" }, "os": [ "linux", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-jXQkwQRWiowbVPGQrGacOkI5shKPhrEixCu93OpoMtL5fs9mnhtD7XKMPi8CX0W8gsqsJjwR4NlaR+EflyANbQ=="], + + "helius-laserstream-darwin-arm64": ["helius-laserstream-darwin-arm64@0.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p/K2Mi3wZnMxEYSLCvu858VyMvtJFonhdF8cLaMcszFv04WWdsK+hINNZpVRfakypvDfDPbMudEhL1Q9USD5+w=="], + + "helius-laserstream-darwin-x64": ["helius-laserstream-darwin-x64@0.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Hd5irFyfOqQZLdoj5a+OV7vML2YfySSBuKlOwtisMHkUuIXZ4NpAexslDmK7iP5VWRI+lOv9X/tA7BhxW7RGSQ=="], + + "helius-laserstream-linux-arm64-gnu": ["helius-laserstream-linux-arm64-gnu@0.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PlPm1dvOvTGBL1nuzK98Xe40BJq1JWNREXlBHKDVA/B+KCGQnIMJ1s6e1MevSvFE7SOix5i1BxhYIxGioK2GMg=="], + + "helius-laserstream-linux-arm64-musl": ["helius-laserstream-linux-arm64-musl@0.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-LFadfMRuTd1zo6RZqLTgHQapo3gJYioS7wFMWFoBOFulG0BpAqHEDNobkxx0002QArw+zX29MQ/5OaOCf8kKTA=="], + + "helius-laserstream-linux-x64-gnu": ["helius-laserstream-linux-x64-gnu@0.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-IZWK/OQIe0647QqfYikLb1DFK+nYtXLJiMcpj24qnNVWBOtMXmPc1hL6ebazdEiaKt7fxNd5IiM1RqeaqZAZMw=="], + + "helius-laserstream-linux-x64-musl": ["helius-laserstream-linux-x64-musl@0.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-riTS6VgxDae1fHOJ2XC/o/v1OZRbEv/3rcoa3NlAOnooDKp5HDgD0zJTcImjQHpYWwGaejx1oX/Ht53lxNoijw=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -1195,6 +1212,8 @@ "@switchboard-xyz/on-demand/bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], + "@types/protobufjs/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], @@ -1221,6 +1240,8 @@ "glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "helius-laserstream/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "jayson/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], diff --git a/sdk/src/accounts/laserProgramAccountSubscriber.ts b/sdk/src/accounts/laserProgramAccountSubscriber.ts index 914cf1f34a..a2315ad92f 100644 --- a/sdk/src/accounts/laserProgramAccountSubscriber.ts +++ b/sdk/src/accounts/laserProgramAccountSubscriber.ts @@ -11,8 +11,8 @@ import { LaserSubscribeRequest, LaserSubscribeUpdate, CompressionAlgorithms, -} from '../isomorphic/grpc.node'; -import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; + CommitmentLevel, +} from '../isomorphic/grpc'; type LaserCommitment = (typeof LaserCommitmentLevel)[keyof typeof LaserCommitmentLevel]; diff --git a/sdk/src/isomorphic/grpc.node.ts b/sdk/src/isomorphic/grpc.node.ts index 8652858ef0..907bdcc432 100644 --- a/sdk/src/isomorphic/grpc.node.ts +++ b/sdk/src/isomorphic/grpc.node.ts @@ -2,8 +2,8 @@ import type Client from '@triton-one/yellowstone-grpc'; import type { SubscribeRequest, SubscribeUpdate, - CommitmentLevel, } from '@triton-one/yellowstone-grpc'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; import { ClientDuplexStream, ChannelOptions } from '@grpc/grpc-js'; import { From c926d98463379169e2dba6c9b1971a0750a160cd Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:21:51 +0000 Subject: [PATCH 019/247] sdk: release v2.139.0-beta.4 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 627cb8ed50..cdfcf5688f 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.3 \ No newline at end of file +2.139.0-beta.4 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index f683e3a5b3..6642d69747 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.3", + "version": "2.139.0-beta.4", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From bb520314d2efb612b8e57ffdeb89f8e6319a8b39 Mon Sep 17 00:00:00 2001 From: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:35:21 -0400 Subject: [PATCH 020/247] sdk: update aster config (#1903) * update aster config * add pythLazerId --- sdk/src/constants/perpMarkets.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 7a28100885..2e0f8aac86 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1318,11 +1318,12 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ symbol: 'ASTER-PERP', baseAssetSymbol: 'ASTER', marketIndex: 76, - oracle: new PublicKey('T1VdaNiTLJ9gMvopnybxtbqdEAAeDQBet4JmvQ4GCP9'), + oracle: new PublicKey('E4tyjB3os4jVczLVQ258uxLdcwjuqmhcsPquVWgrpah4'), launchTs: 1758632629000, - oracleSource: OracleSource.PYTH_PULL, + oracleSource: OracleSource.PYTH_LAZER, pythFeedId: '0xa903b5a82cb572397e3d47595d2889cf80513f5b4cf7a36b513ae10cc8b1e338', + pythLazerId: 2310, }, ]; From 31c69e0718dd583f65b8dd1af3db54c0b1975cee Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:40:36 +0000 Subject: [PATCH 021/247] sdk: release v2.139.0-beta.5 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index cdfcf5688f..d009d382d1 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.4 \ No newline at end of file +2.139.0-beta.5 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 6642d69747..bc9fa4e4d0 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.4", + "version": "2.139.0-beta.5", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From e592e676089613f1290560adcecb65b05a66cf62 Mon Sep 17 00:00:00 2001 From: wphan Date: Tue, 23 Sep 2025 22:37:41 -0700 Subject: [PATCH 022/247] Revert "Revert "Crispeaney/revert swift max margin ratio" (#1877)" (#1907) This reverts commit 0a8e15349f45e135df3eb2341f163d70ef09fe64. --- CHANGELOG.md | 1 - programs/drift/src/instructions/keeper.rs | 4 - programs/drift/src/state/order_params.rs | 2 - .../drift/src/validation/sig_verification.rs | 108 +++---- .../src/validation/sig_verification/tests.rs | 293 ------------------ sdk/src/driftClient.ts | 14 +- sdk/src/idl/drift.json | 19 +- sdk/src/types.ts | 2 - tests/placeAndMakeSignedMsgBankrun.ts | 101 +----- 9 files changed, 47 insertions(+), 497 deletions(-) delete mode 100644 programs/drift/src/validation/sig_verification/tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f66acb9f71..214ac37868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - program: post only respects reduce only ([#1878](https://github.com/drift-labs/protocol-v2/pull/1878)) - program: add sequence id to exchange/mm oracle ([#1834](https://github.com/drift-labs/protocol-v2/pull/1834)) - program: perp position max margin ratio ([#1847](https://github.com/drift-labs/protocol-v2/pull/1847)) -- program: add padding to swift messages ([#1845](https://github.com/drift-labs/protocol-v2/pull/1845)) - program: rm lp ([#1755](https://github.com/drift-labs/protocol-v2/pull/1755)) ### Fixes diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 4e66b0fcee..d5b0523d3e 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -760,10 +760,6 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( return Ok(()); } - if let Some(max_margin_ratio) = verified_message_and_signature.max_margin_ratio { - taker.update_perp_position_max_margin_ratio(market_index, max_margin_ratio)?; - } - // Dont place order if signed msg order already exists let mut taker_order_id_to_use = taker.next_order_id; let mut signed_msg_order_id = diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 3b3431a38c..9c74fa1406 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -872,7 +872,6 @@ pub struct SignedMsgOrderParamsMessage { pub uuid: [u8; 8], pub take_profit_order_params: Option, pub stop_loss_order_params: Option, - pub max_margin_ratio: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] @@ -883,7 +882,6 @@ pub struct SignedMsgOrderParamsDelegateMessage { pub uuid: [u8; 8], pub take_profit_order_params: Option, pub stop_loss_order_params: Option, - pub max_margin_ratio: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index 66ba1cab98..3349c7d5d7 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -14,9 +14,6 @@ use solana_program::program_memory::sol_memcmp; use solana_program::sysvar; use std::convert::TryInto; -#[cfg(test)] -mod tests; - const ED25519_PROGRAM_INPUT_HEADER_LEN: usize = 2; const SIGNATURE_LEN: u16 = 64; @@ -48,7 +45,6 @@ pub struct Ed25519SignatureOffsets { pub message_instruction_index: u16, } -#[derive(Debug)] pub struct VerifiedMessage { pub signed_msg_order_params: OrderParams, pub sub_account_id: Option, @@ -57,7 +53,6 @@ pub struct VerifiedMessage { pub uuid: [u8; 8], pub take_profit_order_params: Option, pub stop_loss_order_params: Option, - pub max_margin_ratio: Option, pub signature: [u8; 64], } @@ -65,69 +60,6 @@ fn slice_eq(a: &[u8], b: &[u8]) -> bool { a.len() == b.len() && sol_memcmp(a, b, a.len()) == 0 } -pub fn deserialize_into_verified_message( - payload: Vec, - signature: &[u8; 64], - is_delegate_signer: bool, -) -> Result { - if is_delegate_signer { - if payload.len() < 8 { - return Err(SignatureVerificationError::InvalidMessageDataSize.into()); - } - let min_len: usize = std::mem::size_of::(); - let mut owned = payload; - if owned.len() < min_len { - owned.resize(min_len, 0); - } - let deserialized = SignedMsgOrderParamsDelegateMessage::deserialize( - &mut &owned[8..], // 8 byte manual discriminator - ) - .map_err(|_| { - msg!("Invalid message encoding for is_delegate_signer = true"); - SignatureVerificationError::InvalidMessageDataSize - })?; - - return Ok(VerifiedMessage { - signed_msg_order_params: deserialized.signed_msg_order_params, - sub_account_id: None, - delegate_signed_taker_pubkey: Some(deserialized.taker_pubkey), - slot: deserialized.slot, - uuid: deserialized.uuid, - take_profit_order_params: deserialized.take_profit_order_params, - stop_loss_order_params: deserialized.stop_loss_order_params, - max_margin_ratio: deserialized.max_margin_ratio, - signature: *signature, - }); - } else { - if payload.len() < 8 { - return Err(SignatureVerificationError::InvalidMessageDataSize.into()); - } - let min_len: usize = std::mem::size_of::(); - let mut owned = payload; - if owned.len() < min_len { - owned.resize(min_len, 0); - } - let deserialized = SignedMsgOrderParamsMessage::deserialize( - &mut &owned[8..], // 8 byte manual discriminator - ) - .map_err(|_| { - msg!("Invalid delegate message encoding for with is_delegate_signer = false"); - SignatureVerificationError::InvalidMessageDataSize - })?; - return Ok(VerifiedMessage { - signed_msg_order_params: deserialized.signed_msg_order_params, - sub_account_id: Some(deserialized.sub_account_id), - delegate_signed_taker_pubkey: None, - slot: deserialized.slot, - uuid: deserialized.uuid, - take_profit_order_params: deserialized.take_profit_order_params, - stop_loss_order_params: deserialized.stop_loss_order_params, - max_margin_ratio: deserialized.max_margin_ratio, - signature: *signature, - }); - } -} - /// Check Ed25519Program instruction data verifies the given msg /// /// `ix` an Ed25519Program instruction [see](https://github.com/solana-labs/solana/blob/master/sdk/src/ed25519_instruction.rs)) @@ -300,7 +232,45 @@ pub fn verify_and_decode_ed25519_msg( let payload = hex::decode(payload).map_err(|_| SignatureVerificationError::InvalidMessageHex)?; - deserialize_into_verified_message(payload, signature, is_delegate_signer) + if is_delegate_signer { + let deserialized = SignedMsgOrderParamsDelegateMessage::deserialize( + &mut &payload[8..], // 8 byte manual discriminator + ) + .map_err(|_| { + msg!("Invalid message encoding for is_delegate_signer = true"); + SignatureVerificationError::InvalidMessageDataSize + })?; + + return Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: None, + delegate_signed_taker_pubkey: Some(deserialized.taker_pubkey), + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + signature: *signature, + }); + } else { + let deserialized = SignedMsgOrderParamsMessage::deserialize( + &mut &payload[8..], // 8 byte manual discriminator + ) + .map_err(|_| { + msg!("Invalid delegate message encoding for with is_delegate_signer = false"); + SignatureVerificationError::InvalidMessageDataSize + })?; + + return Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: Some(deserialized.sub_account_id), + delegate_signed_taker_pubkey: None, + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + signature: *signature, + }); + } } #[error_code] diff --git a/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs deleted file mode 100644 index fae2456b43..0000000000 --- a/programs/drift/src/validation/sig_verification/tests.rs +++ /dev/null @@ -1,293 +0,0 @@ -mod sig_verification { - use std::str::FromStr; - - use anchor_lang::prelude::Pubkey; - - use crate::controller::position::PositionDirection; - use crate::validation::sig_verification::deserialize_into_verified_message; - - #[test] - fn test_deserialize_into_verified_message_non_delegate() { - let signature = [1u8; 64]; - let payload = vec![ - 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 1, 0, 202, 154, 59, 0, 0, 0, 0, 0, 248, - 89, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 192, 181, 74, 13, 0, 0, 0, 0, - 1, 0, 248, 89, 13, 0, 0, 0, 0, 0, 0, 232, 3, 0, 0, 0, 0, 0, 0, 72, 112, 54, 84, 106, - 83, 48, 107, 0, 0, - ]; - - // Test deserialization with non-delegate signer - let result = deserialize_into_verified_message(payload, &signature, false); - assert!(result.is_ok()); - - let verified_message = result.unwrap(); - - // Verify the deserialized message has expected structure - assert_eq!(verified_message.signature, signature); - assert_eq!(verified_message.sub_account_id, Some(0)); - assert_eq!(verified_message.delegate_signed_taker_pubkey, None); - assert_eq!(verified_message.slot, 1000); - assert_eq!(verified_message.uuid, [72, 112, 54, 84, 106, 83, 48, 107]); - assert!(verified_message.take_profit_order_params.is_none()); - assert!(verified_message.stop_loss_order_params.is_none()); - assert!(verified_message.max_margin_ratio.is_none()); - // Verify order params - let order_params = &verified_message.signed_msg_order_params; - assert_eq!(order_params.user_order_id, 1); - assert_eq!(order_params.direction, PositionDirection::Long); - assert_eq!(order_params.base_asset_amount, 1000000000u64); - assert_eq!(order_params.price, 224000000u64); - assert_eq!(order_params.market_index, 0); - assert_eq!(order_params.reduce_only, false); - assert_eq!(order_params.auction_duration, Some(10)); - assert_eq!(order_params.auction_start_price, Some(223000000i64)); - assert_eq!(order_params.auction_end_price, Some(224000000i64)); - } - - #[test] - fn test_deserialize_into_verified_message_non_delegate_with_tpsl() { - let signature = [1u8; 64]; - let payload = vec![ - 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, - 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, - 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, - 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, - 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, - ]; - - // Test deserialization with delegate signer - let result = deserialize_into_verified_message(payload, &signature, false); - assert!(result.is_ok()); - - let verified_message = result.unwrap(); - - // Verify the deserialized message has expected structure - assert_eq!(verified_message.signature, signature); - assert_eq!(verified_message.sub_account_id, Some(2)); - assert_eq!(verified_message.delegate_signed_taker_pubkey, None); - assert_eq!(verified_message.slot, 2345); - assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); - assert!(verified_message.max_margin_ratio.is_none()); - - assert!(verified_message.take_profit_order_params.is_some()); - let tp = verified_message.take_profit_order_params.unwrap(); - assert_eq!(tp.base_asset_amount, 3456000000u64); - assert_eq!(tp.trigger_price, 240000000u64); - - assert!(verified_message.stop_loss_order_params.is_some()); - let sl = verified_message.stop_loss_order_params.unwrap(); - assert_eq!(sl.base_asset_amount, 3456000000u64); - assert_eq!(sl.trigger_price, 225000000u64); - - // Verify order params - let order_params = &verified_message.signed_msg_order_params; - assert_eq!(order_params.user_order_id, 3); - assert_eq!(order_params.direction, PositionDirection::Long); - assert_eq!(order_params.base_asset_amount, 3456000000u64); - assert_eq!(order_params.price, 237000000u64); - assert_eq!(order_params.market_index, 0); - assert_eq!(order_params.reduce_only, false); - assert_eq!(order_params.auction_duration, Some(10)); - assert_eq!(order_params.auction_start_price, Some(230000000i64)); - assert_eq!(order_params.auction_end_price, Some(237000000i64)); - } - - #[test] - fn test_deserialize_into_verified_message_non_delegate_with_max_margin_ratio() { - let signature = [1u8; 64]; - let payload = vec![ - 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, - 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, - 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, - 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, - 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 1, - ]; - - // Test deserialization with delegate signer - let result = deserialize_into_verified_message(payload, &signature, false); - assert!(result.is_ok()); - - let verified_message = result.unwrap(); - - // Verify the deserialized message has expected structure - assert_eq!(verified_message.signature, signature); - assert_eq!(verified_message.sub_account_id, Some(2)); - assert_eq!(verified_message.delegate_signed_taker_pubkey, None); - assert_eq!(verified_message.slot, 2345); - assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); - assert!(verified_message.max_margin_ratio.is_some()); - assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); - - assert!(verified_message.take_profit_order_params.is_some()); - let tp = verified_message.take_profit_order_params.unwrap(); - assert_eq!(tp.base_asset_amount, 3456000000u64); - assert_eq!(tp.trigger_price, 240000000u64); - - assert!(verified_message.stop_loss_order_params.is_some()); - let sl = verified_message.stop_loss_order_params.unwrap(); - assert_eq!(sl.base_asset_amount, 3456000000u64); - assert_eq!(sl.trigger_price, 225000000u64); - - // Verify order params - let order_params = &verified_message.signed_msg_order_params; - assert_eq!(order_params.user_order_id, 3); - assert_eq!(order_params.direction, PositionDirection::Long); - assert_eq!(order_params.base_asset_amount, 3456000000u64); - assert_eq!(order_params.price, 237000000u64); - assert_eq!(order_params.market_index, 0); - assert_eq!(order_params.reduce_only, false); - assert_eq!(order_params.auction_duration, Some(10)); - assert_eq!(order_params.auction_start_price, Some(230000000i64)); - assert_eq!(order_params.auction_end_price, Some(237000000i64)); - } - - #[test] - fn test_deserialize_into_verified_message_delegate() { - let signature = [1u8; 64]; - let payload = vec![ - 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, - 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, - 128, 151, 47, 14, 0, 0, 0, 0, 242, 208, 117, 159, 92, 135, 34, 224, 147, 14, 64, 92, 7, - 25, 145, 237, 79, 35, 72, 24, 140, 13, 25, 189, 134, 243, 232, 5, 89, 37, 166, 242, 41, - 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 0, 0, - ]; - - // Test deserialization with delegate signer - let result = deserialize_into_verified_message(payload, &signature, true); - assert!(result.is_ok()); - - let verified_message = result.unwrap(); - - // Verify the deserialized message has expected structure - assert_eq!(verified_message.signature, signature); - assert_eq!(verified_message.sub_account_id, None); - assert_eq!( - verified_message.delegate_signed_taker_pubkey, - Some(Pubkey::from_str("HLr2UfL422cakKkaBG4z1bMZrcyhmzX2pHdegjM6fYXB").unwrap()) - ); - assert_eq!(verified_message.slot, 2345); - assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); - assert!(verified_message.take_profit_order_params.is_none()); - assert!(verified_message.stop_loss_order_params.is_none()); - assert!(verified_message.max_margin_ratio.is_none()); - - // Verify order params - let order_params = &verified_message.signed_msg_order_params; - assert_eq!(order_params.user_order_id, 2); - assert_eq!(order_params.direction, PositionDirection::Short); - assert_eq!(order_params.base_asset_amount, 1000000000u64); - assert_eq!(order_params.price, 237000000u64); - assert_eq!(order_params.market_index, 0); - assert_eq!(order_params.reduce_only, false); - assert_eq!(order_params.auction_duration, Some(10)); - assert_eq!(order_params.auction_start_price, Some(240000000i64)); - assert_eq!(order_params.auction_end_price, Some(238000000i64)); - } - - #[test] - fn test_deserialize_into_verified_message_delegate_with_tpsl() { - let signature = [1u8; 64]; - let payload = vec![ - 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, - 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, - 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, - 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, - 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, - 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, - 59, 0, 0, 0, 0, - ]; - - // Test deserialization with delegate signer - let result = deserialize_into_verified_message(payload, &signature, true); - assert!(result.is_ok()); - - let verified_message = result.unwrap(); - - // Verify the deserialized message has expected structure - assert_eq!(verified_message.signature, signature); - assert_eq!(verified_message.sub_account_id, None); - assert_eq!( - verified_message.delegate_signed_taker_pubkey, - Some(Pubkey::from_str("HG2iQKnRkkasrLptwMZewV6wT7KPstw9wkA8yyu8Nx3m").unwrap()) - ); - assert_eq!(verified_message.slot, 2345); - assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); - assert!(verified_message.max_margin_ratio.is_none()); - - assert!(verified_message.take_profit_order_params.is_some()); - let tp = verified_message.take_profit_order_params.unwrap(); - assert_eq!(tp.base_asset_amount, 1000000000u64); - assert_eq!(tp.trigger_price, 230000000u64); - - assert!(verified_message.stop_loss_order_params.is_some()); - let sl = verified_message.stop_loss_order_params.unwrap(); - assert_eq!(sl.base_asset_amount, 1000000000u64); - assert_eq!(sl.trigger_price, 250000000u64); - - // Verify order params - let order_params = &verified_message.signed_msg_order_params; - assert_eq!(order_params.user_order_id, 2); - assert_eq!(order_params.direction, PositionDirection::Short); - assert_eq!(order_params.base_asset_amount, 1000000000u64); - assert_eq!(order_params.price, 237000000u64); - assert_eq!(order_params.market_index, 0); - assert_eq!(order_params.reduce_only, false); - assert_eq!(order_params.auction_duration, Some(10)); - assert_eq!(order_params.auction_start_price, Some(240000000i64)); - assert_eq!(order_params.auction_end_price, Some(238000000i64)); - } - - #[test] - fn test_deserialize_into_verified_message_delegate_with_max_margin_ratio() { - let signature = [1u8; 64]; - let payload = vec![ - 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, - 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, - 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, - 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, - 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, - 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, - 59, 0, 0, 0, 0, 1, 1, - ]; - - // Test deserialization with delegate signer - let result = deserialize_into_verified_message(payload, &signature, true); - assert!(result.is_ok()); - - let verified_message = result.unwrap(); - - // Verify the deserialized message has expected structure - assert_eq!(verified_message.signature, signature); - assert_eq!(verified_message.sub_account_id, None); - assert_eq!( - verified_message.delegate_signed_taker_pubkey, - Some(Pubkey::from_str("HG2iQKnRkkasrLptwMZewV6wT7KPstw9wkA8yyu8Nx3m").unwrap()) - ); - assert_eq!(verified_message.slot, 2345); - assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); - assert!(verified_message.max_margin_ratio.is_some()); - assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); - - assert!(verified_message.take_profit_order_params.is_some()); - let tp = verified_message.take_profit_order_params.unwrap(); - assert_eq!(tp.base_asset_amount, 1000000000u64); - assert_eq!(tp.trigger_price, 230000000u64); - - assert!(verified_message.stop_loss_order_params.is_some()); - let sl = verified_message.stop_loss_order_params.unwrap(); - assert_eq!(sl.base_asset_amount, 1000000000u64); - assert_eq!(sl.trigger_price, 250000000u64); - - // Verify order params - let order_params = &verified_message.signed_msg_order_params; - assert_eq!(order_params.user_order_id, 2); - assert_eq!(order_params.direction, PositionDirection::Short); - assert_eq!(order_params.base_asset_amount, 1000000000u64); - assert_eq!(order_params.price, 237000000u64); - assert_eq!(order_params.market_index, 0); - assert_eq!(order_params.reduce_only, false); - assert_eq!(order_params.auction_duration, Some(10)); - assert_eq!(order_params.auction_start_price, Some(240000000i64)); - assert_eq!(order_params.auction_end_price, Some(238000000i64)); - } -} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 4d39ae6d0f..0c2732dfa4 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -6541,10 +6541,6 @@ export class DriftClient { | SignedMsgOrderParamsDelegateMessage, delegateSigner?: boolean ): Buffer { - if (orderParamsMessage.maxMarginRatio === undefined) { - orderParamsMessage.maxMarginRatio = null; - } - const anchorIxName = delegateSigner ? 'global' + ':' + 'SignedMsgOrderParamsDelegateMessage' : 'global' + ':' + 'SignedMsgOrderParamsMessage'; @@ -6565,10 +6561,7 @@ export class DriftClient { } /* - * Decode signedMsg taker order params from borsh buffer. Zero pads the message in case the - * received message was encoded by an outdated IDL (size will be too small and decode will throw). - * Note: the 128 will be problematic if the type we are expecting to deserializze into is 128 bytes - * larger than the message we are receiving (unlikely, especially if all new fields are Options). + * Decode signedMsg taker order params from borsh buffer */ public decodeSignedMsgOrderParamsMessage( encodedMessage: Buffer, @@ -6579,10 +6572,7 @@ export class DriftClient { : 'SignedMsgOrderParamsMessage'; return this.program.coder.types.decode( decodeStr, - Buffer.concat([ - encodedMessage.slice(8), // strip out discriminator - Buffer.alloc(128), // pad on 128 bytes, this is most efficient way to messages that are too small - ]) + encodedMessage.slice(8) // assumes discriminator ); } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 6c25dcaf1c..9de5626ba3 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -10118,12 +10118,6 @@ "defined": "SignedMsgTriggerOrderParams" } } - }, - { - "name": "maxMarginRatio", - "type": { - "option": "u16" - } } ] } @@ -10171,12 +10165,6 @@ "defined": "SignedMsgTriggerOrderParams" } } - }, - { - "name": "maxMarginRatio", - "type": { - "option": "u16" - } } ] } @@ -16218,5 +16206,8 @@ "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" } - ] -} \ No newline at end of file + ], + "metadata": { + "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" + } +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 2d49e61b58..6ec791c873 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1309,7 +1309,6 @@ export type SignedMsgOrderParamsMessage = { uuid: Uint8Array; takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; - maxMarginRatio?: number | null; }; export type SignedMsgOrderParamsDelegateMessage = { @@ -1319,7 +1318,6 @@ export type SignedMsgOrderParamsDelegateMessage = { takerPubkey: PublicKey; takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; - maxMarginRatio?: number | null; }; export type SignedMsgTriggerOrderParams = { diff --git a/tests/placeAndMakeSignedMsgBankrun.ts b/tests/placeAndMakeSignedMsgBankrun.ts index 3b23722445..173e1364c4 100644 --- a/tests/placeAndMakeSignedMsgBankrun.ts +++ b/tests/placeAndMakeSignedMsgBankrun.ts @@ -1539,7 +1539,7 @@ describe('place and make signedMsg order', () => { ); assert.fail('should fail'); } catch (e) { - assert(e.toString().includes('0x18a5')); // SignedMsgUserContextUserMismatch + assert(e.toString().includes('0x1776')); const takerOrders = takerDriftClient.getUser().getOpenOrders(); assert(takerOrders.length == 0); } @@ -1604,105 +1604,6 @@ describe('place and make signedMsg order', () => { await takerDriftClientUser.unsubscribe(); await takerDriftClient.unsubscribe(); }); - - it('fills signedMsg with max margin ratio ', async () => { - slot = new BN( - await bankrunContextWrapper.connection.toConnection().getSlot() - ); - const [takerDriftClient, takerDriftClientUser] = - await initializeNewTakerClientAndUser( - bankrunContextWrapper, - chProgram, - usdcMint, - usdcAmount, - marketIndexes, - spotMarketIndexes, - oracleInfos, - bulkAccountLoader - ); - await takerDriftClientUser.fetchAccounts(); - - const marketIndex = 0; - const baseAssetAmount = BASE_PRECISION; - const takerOrderParams = getMarketOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount, - price: new BN(224).mul(PRICE_PRECISION), - auctionStartPrice: new BN(223).mul(PRICE_PRECISION), - auctionEndPrice: new BN(224).mul(PRICE_PRECISION), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - marketType: MarketType.PERP, - }) as OrderParams; - - await takerDriftClientUser.fetchAccounts(); - const makerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount, - price: new BN(223).mul(PRICE_PRECISION), - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: 1, - marketType: MarketType.PERP, - }) as OrderParams; - - const uuid = Uint8Array.from(Buffer.from(nanoid(8))); - const takerOrderParamsMessage: SignedMsgOrderParamsMessage = { - signedMsgOrderParams: takerOrderParams, - subAccountId: 0, - slot, - uuid, - stopLossOrderParams: null, - takeProfitOrderParams: null, - maxMarginRatio: 100, - }; - - const signedOrderParams = takerDriftClient.signSignedMsgOrderParamsMessage( - takerOrderParamsMessage - ); - - const ixs = await makerDriftClient.getPlaceAndMakeSignedMsgPerpOrderIxs( - signedOrderParams, - uuid, - { - taker: await takerDriftClient.getUserAccountPublicKey(), - takerUserAccount: takerDriftClient.getUserAccount(), - takerStats: takerDriftClient.getUserStatsAccountPublicKey(), - signingAuthority: takerDriftClient.wallet.publicKey, - }, - makerOrderParams, - undefined, - undefined, - undefined, - 2 - ); - - /* - Transaction size should be largest for filling with trigger orders w/ place and take - Max size: 1232 - We currently trade on sol market w/ sol oracle so would be better with LUT, so -64 bytes + 2 bytes - We dont have referrers for maker so need to add 64 bytes - We want to allow for positions to be full with maximally different markets for maker/taker and spot/perp, - so add 30 bytes for market/oracle for taker and 30 bytes for maker - Add 32 bytes for LUT - size of transaction + 32 + 2 + 30 + 30 < 1232 - */ - assert(getSizeOfTransaction(ixs, false) < 1138); - - const tx = await makerDriftClient.buildTransaction(ixs); - await makerDriftClient.sendTransaction(tx as Transaction); - - const takerPosition = takerDriftClient.getUser().getPerpPosition(0); - - // All orders are placed and one is - // @ts-ignore - assert(takerPosition.maxMarginRatio === 100); - - await takerDriftClientUser.unsubscribe(); - await takerDriftClient.unsubscribe(); - }); }); async function initializeNewTakerClientAndUser( From 77d5b5423312650369cef5f38244d49071e00715 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 05:44:17 +0000 Subject: [PATCH 023/247] sdk: release v2.139.0-beta.6 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index d009d382d1..f6ab36d511 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.5 \ No newline at end of file +2.139.0-beta.6 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index bc9fa4e4d0..c1db6e0e01 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.5", + "version": "2.139.0-beta.6", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From f0535c9654c3302784202b526367ccb7456a0ed4 Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 24 Sep 2025 09:13:20 -0700 Subject: [PATCH 024/247] =?UTF-8?q?Revert=20"Revert=20"Revert=20"Crispeane?= =?UTF-8?q?y/revert=20swift=20max=20margin=20ratio"=20(#1877)"=20(#?= =?UTF-8?q?=E2=80=A6"=20(#1910)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + programs/drift/src/instructions/keeper.rs | 4 + programs/drift/src/state/order_params.rs | 2 + .../drift/src/validation/sig_verification.rs | 108 ++++--- .../src/validation/sig_verification/tests.rs | 293 ++++++++++++++++++ sdk/src/driftClient.ts | 14 +- sdk/src/idl/drift.json | 19 +- sdk/src/types.ts | 2 + tests/placeAndMakeSignedMsgBankrun.ts | 101 +++++- 9 files changed, 497 insertions(+), 47 deletions(-) create mode 100644 programs/drift/src/validation/sig_verification/tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 214ac37868..f66acb9f71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - program: post only respects reduce only ([#1878](https://github.com/drift-labs/protocol-v2/pull/1878)) - program: add sequence id to exchange/mm oracle ([#1834](https://github.com/drift-labs/protocol-v2/pull/1834)) - program: perp position max margin ratio ([#1847](https://github.com/drift-labs/protocol-v2/pull/1847)) +- program: add padding to swift messages ([#1845](https://github.com/drift-labs/protocol-v2/pull/1845)) - program: rm lp ([#1755](https://github.com/drift-labs/protocol-v2/pull/1755)) ### Fixes diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index d5b0523d3e..4e66b0fcee 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -760,6 +760,10 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( return Ok(()); } + if let Some(max_margin_ratio) = verified_message_and_signature.max_margin_ratio { + taker.update_perp_position_max_margin_ratio(market_index, max_margin_ratio)?; + } + // Dont place order if signed msg order already exists let mut taker_order_id_to_use = taker.next_order_id; let mut signed_msg_order_id = diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 9c74fa1406..3b3431a38c 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -872,6 +872,7 @@ pub struct SignedMsgOrderParamsMessage { pub uuid: [u8; 8], pub take_profit_order_params: Option, pub stop_loss_order_params: Option, + pub max_margin_ratio: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] @@ -882,6 +883,7 @@ pub struct SignedMsgOrderParamsDelegateMessage { pub uuid: [u8; 8], pub take_profit_order_params: Option, pub stop_loss_order_params: Option, + pub max_margin_ratio: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index 3349c7d5d7..66ba1cab98 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -14,6 +14,9 @@ use solana_program::program_memory::sol_memcmp; use solana_program::sysvar; use std::convert::TryInto; +#[cfg(test)] +mod tests; + const ED25519_PROGRAM_INPUT_HEADER_LEN: usize = 2; const SIGNATURE_LEN: u16 = 64; @@ -45,6 +48,7 @@ pub struct Ed25519SignatureOffsets { pub message_instruction_index: u16, } +#[derive(Debug)] pub struct VerifiedMessage { pub signed_msg_order_params: OrderParams, pub sub_account_id: Option, @@ -53,6 +57,7 @@ pub struct VerifiedMessage { pub uuid: [u8; 8], pub take_profit_order_params: Option, pub stop_loss_order_params: Option, + pub max_margin_ratio: Option, pub signature: [u8; 64], } @@ -60,6 +65,69 @@ fn slice_eq(a: &[u8], b: &[u8]) -> bool { a.len() == b.len() && sol_memcmp(a, b, a.len()) == 0 } +pub fn deserialize_into_verified_message( + payload: Vec, + signature: &[u8; 64], + is_delegate_signer: bool, +) -> Result { + if is_delegate_signer { + if payload.len() < 8 { + return Err(SignatureVerificationError::InvalidMessageDataSize.into()); + } + let min_len: usize = std::mem::size_of::(); + let mut owned = payload; + if owned.len() < min_len { + owned.resize(min_len, 0); + } + let deserialized = SignedMsgOrderParamsDelegateMessage::deserialize( + &mut &owned[8..], // 8 byte manual discriminator + ) + .map_err(|_| { + msg!("Invalid message encoding for is_delegate_signer = true"); + SignatureVerificationError::InvalidMessageDataSize + })?; + + return Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: None, + delegate_signed_taker_pubkey: Some(deserialized.taker_pubkey), + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + max_margin_ratio: deserialized.max_margin_ratio, + signature: *signature, + }); + } else { + if payload.len() < 8 { + return Err(SignatureVerificationError::InvalidMessageDataSize.into()); + } + let min_len: usize = std::mem::size_of::(); + let mut owned = payload; + if owned.len() < min_len { + owned.resize(min_len, 0); + } + let deserialized = SignedMsgOrderParamsMessage::deserialize( + &mut &owned[8..], // 8 byte manual discriminator + ) + .map_err(|_| { + msg!("Invalid delegate message encoding for with is_delegate_signer = false"); + SignatureVerificationError::InvalidMessageDataSize + })?; + return Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: Some(deserialized.sub_account_id), + delegate_signed_taker_pubkey: None, + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + max_margin_ratio: deserialized.max_margin_ratio, + signature: *signature, + }); + } +} + /// Check Ed25519Program instruction data verifies the given msg /// /// `ix` an Ed25519Program instruction [see](https://github.com/solana-labs/solana/blob/master/sdk/src/ed25519_instruction.rs)) @@ -232,45 +300,7 @@ pub fn verify_and_decode_ed25519_msg( let payload = hex::decode(payload).map_err(|_| SignatureVerificationError::InvalidMessageHex)?; - if is_delegate_signer { - let deserialized = SignedMsgOrderParamsDelegateMessage::deserialize( - &mut &payload[8..], // 8 byte manual discriminator - ) - .map_err(|_| { - msg!("Invalid message encoding for is_delegate_signer = true"); - SignatureVerificationError::InvalidMessageDataSize - })?; - - return Ok(VerifiedMessage { - signed_msg_order_params: deserialized.signed_msg_order_params, - sub_account_id: None, - delegate_signed_taker_pubkey: Some(deserialized.taker_pubkey), - slot: deserialized.slot, - uuid: deserialized.uuid, - take_profit_order_params: deserialized.take_profit_order_params, - stop_loss_order_params: deserialized.stop_loss_order_params, - signature: *signature, - }); - } else { - let deserialized = SignedMsgOrderParamsMessage::deserialize( - &mut &payload[8..], // 8 byte manual discriminator - ) - .map_err(|_| { - msg!("Invalid delegate message encoding for with is_delegate_signer = false"); - SignatureVerificationError::InvalidMessageDataSize - })?; - - return Ok(VerifiedMessage { - signed_msg_order_params: deserialized.signed_msg_order_params, - sub_account_id: Some(deserialized.sub_account_id), - delegate_signed_taker_pubkey: None, - slot: deserialized.slot, - uuid: deserialized.uuid, - take_profit_order_params: deserialized.take_profit_order_params, - stop_loss_order_params: deserialized.stop_loss_order_params, - signature: *signature, - }); - } + deserialize_into_verified_message(payload, signature, is_delegate_signer) } #[error_code] diff --git a/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs new file mode 100644 index 0000000000..fae2456b43 --- /dev/null +++ b/programs/drift/src/validation/sig_verification/tests.rs @@ -0,0 +1,293 @@ +mod sig_verification { + use std::str::FromStr; + + use anchor_lang::prelude::Pubkey; + + use crate::controller::position::PositionDirection; + use crate::validation::sig_verification::deserialize_into_verified_message; + + #[test] + fn test_deserialize_into_verified_message_non_delegate() { + let signature = [1u8; 64]; + let payload = vec![ + 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 1, 0, 202, 154, 59, 0, 0, 0, 0, 0, 248, + 89, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 192, 181, 74, 13, 0, 0, 0, 0, + 1, 0, 248, 89, 13, 0, 0, 0, 0, 0, 0, 232, 3, 0, 0, 0, 0, 0, 0, 72, 112, 54, 84, 106, + 83, 48, 107, 0, 0, + ]; + + // Test deserialization with non-delegate signer + let result = deserialize_into_verified_message(payload, &signature, false); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, Some(0)); + assert_eq!(verified_message.delegate_signed_taker_pubkey, None); + assert_eq!(verified_message.slot, 1000); + assert_eq!(verified_message.uuid, [72, 112, 54, 84, 106, 83, 48, 107]); + assert!(verified_message.take_profit_order_params.is_none()); + assert!(verified_message.stop_loss_order_params.is_none()); + assert!(verified_message.max_margin_ratio.is_none()); + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 1); + assert_eq!(order_params.direction, PositionDirection::Long); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + assert_eq!(order_params.price, 224000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(223000000i64)); + assert_eq!(order_params.auction_end_price, Some(224000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_non_delegate_with_tpsl() { + let signature = [1u8; 64]; + let payload = vec![ + 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, + 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, + 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, false); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, Some(2)); + assert_eq!(verified_message.delegate_signed_taker_pubkey, None); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.max_margin_ratio.is_none()); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 3456000000u64); + assert_eq!(tp.trigger_price, 240000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 3456000000u64); + assert_eq!(sl.trigger_price, 225000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 3); + assert_eq!(order_params.direction, PositionDirection::Long); + assert_eq!(order_params.base_asset_amount, 3456000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(230000000i64)); + assert_eq!(order_params.auction_end_price, Some(237000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_non_delegate_with_max_margin_ratio() { + let signature = [1u8; 64]; + let payload = vec![ + 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, + 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, + 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 1, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, false); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, Some(2)); + assert_eq!(verified_message.delegate_signed_taker_pubkey, None); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.max_margin_ratio.is_some()); + assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 3456000000u64); + assert_eq!(tp.trigger_price, 240000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 3456000000u64); + assert_eq!(sl.trigger_price, 225000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 3); + assert_eq!(order_params.direction, PositionDirection::Long); + assert_eq!(order_params.base_asset_amount, 3456000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(230000000i64)); + assert_eq!(order_params.auction_end_price, Some(237000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_delegate() { + let signature = [1u8; 64]; + let payload = vec![ + 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 242, 208, 117, 159, 92, 135, 34, 224, 147, 14, 64, 92, 7, + 25, 145, 237, 79, 35, 72, 24, 140, 13, 25, 189, 134, 243, 232, 5, 89, 37, 166, 242, 41, + 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 0, 0, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, true); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, None); + assert_eq!( + verified_message.delegate_signed_taker_pubkey, + Some(Pubkey::from_str("HLr2UfL422cakKkaBG4z1bMZrcyhmzX2pHdegjM6fYXB").unwrap()) + ); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.take_profit_order_params.is_none()); + assert!(verified_message.stop_loss_order_params.is_none()); + assert!(verified_message.max_margin_ratio.is_none()); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 2); + assert_eq!(order_params.direction, PositionDirection::Short); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(240000000i64)); + assert_eq!(order_params.auction_end_price, Some(238000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_delegate_with_tpsl() { + let signature = [1u8; 64]; + let payload = vec![ + 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, + 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, + 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, + 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, + 59, 0, 0, 0, 0, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, true); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, None); + assert_eq!( + verified_message.delegate_signed_taker_pubkey, + Some(Pubkey::from_str("HG2iQKnRkkasrLptwMZewV6wT7KPstw9wkA8yyu8Nx3m").unwrap()) + ); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.max_margin_ratio.is_none()); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 1000000000u64); + assert_eq!(tp.trigger_price, 230000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 1000000000u64); + assert_eq!(sl.trigger_price, 250000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 2); + assert_eq!(order_params.direction, PositionDirection::Short); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(240000000i64)); + assert_eq!(order_params.auction_end_price, Some(238000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_delegate_with_max_margin_ratio() { + let signature = [1u8; 64]; + let payload = vec![ + 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, + 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, + 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, + 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, + 59, 0, 0, 0, 0, 1, 1, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, true); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, None); + assert_eq!( + verified_message.delegate_signed_taker_pubkey, + Some(Pubkey::from_str("HG2iQKnRkkasrLptwMZewV6wT7KPstw9wkA8yyu8Nx3m").unwrap()) + ); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.max_margin_ratio.is_some()); + assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 1000000000u64); + assert_eq!(tp.trigger_price, 230000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 1000000000u64); + assert_eq!(sl.trigger_price, 250000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 2); + assert_eq!(order_params.direction, PositionDirection::Short); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(240000000i64)); + assert_eq!(order_params.auction_end_price, Some(238000000i64)); + } +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 0c2732dfa4..4d39ae6d0f 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -6541,6 +6541,10 @@ export class DriftClient { | SignedMsgOrderParamsDelegateMessage, delegateSigner?: boolean ): Buffer { + if (orderParamsMessage.maxMarginRatio === undefined) { + orderParamsMessage.maxMarginRatio = null; + } + const anchorIxName = delegateSigner ? 'global' + ':' + 'SignedMsgOrderParamsDelegateMessage' : 'global' + ':' + 'SignedMsgOrderParamsMessage'; @@ -6561,7 +6565,10 @@ export class DriftClient { } /* - * Decode signedMsg taker order params from borsh buffer + * Decode signedMsg taker order params from borsh buffer. Zero pads the message in case the + * received message was encoded by an outdated IDL (size will be too small and decode will throw). + * Note: the 128 will be problematic if the type we are expecting to deserializze into is 128 bytes + * larger than the message we are receiving (unlikely, especially if all new fields are Options). */ public decodeSignedMsgOrderParamsMessage( encodedMessage: Buffer, @@ -6572,7 +6579,10 @@ export class DriftClient { : 'SignedMsgOrderParamsMessage'; return this.program.coder.types.decode( decodeStr, - encodedMessage.slice(8) // assumes discriminator + Buffer.concat([ + encodedMessage.slice(8), // strip out discriminator + Buffer.alloc(128), // pad on 128 bytes, this is most efficient way to messages that are too small + ]) ); } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 9de5626ba3..6c25dcaf1c 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -10118,6 +10118,12 @@ "defined": "SignedMsgTriggerOrderParams" } } + }, + { + "name": "maxMarginRatio", + "type": { + "option": "u16" + } } ] } @@ -10165,6 +10171,12 @@ "defined": "SignedMsgTriggerOrderParams" } } + }, + { + "name": "maxMarginRatio", + "type": { + "option": "u16" + } } ] } @@ -16206,8 +16218,5 @@ "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" } - ], - "metadata": { - "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" - } -} + ] +} \ No newline at end of file diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 6ec791c873..2d49e61b58 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1309,6 +1309,7 @@ export type SignedMsgOrderParamsMessage = { uuid: Uint8Array; takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; + maxMarginRatio?: number | null; }; export type SignedMsgOrderParamsDelegateMessage = { @@ -1318,6 +1319,7 @@ export type SignedMsgOrderParamsDelegateMessage = { takerPubkey: PublicKey; takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; + maxMarginRatio?: number | null; }; export type SignedMsgTriggerOrderParams = { diff --git a/tests/placeAndMakeSignedMsgBankrun.ts b/tests/placeAndMakeSignedMsgBankrun.ts index 173e1364c4..3b23722445 100644 --- a/tests/placeAndMakeSignedMsgBankrun.ts +++ b/tests/placeAndMakeSignedMsgBankrun.ts @@ -1539,7 +1539,7 @@ describe('place and make signedMsg order', () => { ); assert.fail('should fail'); } catch (e) { - assert(e.toString().includes('0x1776')); + assert(e.toString().includes('0x18a5')); // SignedMsgUserContextUserMismatch const takerOrders = takerDriftClient.getUser().getOpenOrders(); assert(takerOrders.length == 0); } @@ -1604,6 +1604,105 @@ describe('place and make signedMsg order', () => { await takerDriftClientUser.unsubscribe(); await takerDriftClient.unsubscribe(); }); + + it('fills signedMsg with max margin ratio ', async () => { + slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const [takerDriftClient, takerDriftClientUser] = + await initializeNewTakerClientAndUser( + bankrunContextWrapper, + chProgram, + usdcMint, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + await takerDriftClientUser.fetchAccounts(); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(224).mul(PRICE_PRECISION), + auctionStartPrice: new BN(223).mul(PRICE_PRECISION), + auctionEndPrice: new BN(224).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + + await takerDriftClientUser.fetchAccounts(); + const makerOrderParams = getLimitOrderParams({ + marketIndex, + direction: PositionDirection.SHORT, + baseAssetAmount, + price: new BN(223).mul(PRICE_PRECISION), + postOnly: PostOnlyParams.MUST_POST_ONLY, + bitFlags: 1, + marketType: MarketType.PERP, + }) as OrderParams; + + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + const takerOrderParamsMessage: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: takerOrderParams, + subAccountId: 0, + slot, + uuid, + stopLossOrderParams: null, + takeProfitOrderParams: null, + maxMarginRatio: 100, + }; + + const signedOrderParams = takerDriftClient.signSignedMsgOrderParamsMessage( + takerOrderParamsMessage + ); + + const ixs = await makerDriftClient.getPlaceAndMakeSignedMsgPerpOrderIxs( + signedOrderParams, + uuid, + { + taker: await takerDriftClient.getUserAccountPublicKey(), + takerUserAccount: takerDriftClient.getUserAccount(), + takerStats: takerDriftClient.getUserStatsAccountPublicKey(), + signingAuthority: takerDriftClient.wallet.publicKey, + }, + makerOrderParams, + undefined, + undefined, + undefined, + 2 + ); + + /* + Transaction size should be largest for filling with trigger orders w/ place and take + Max size: 1232 + We currently trade on sol market w/ sol oracle so would be better with LUT, so -64 bytes + 2 bytes + We dont have referrers for maker so need to add 64 bytes + We want to allow for positions to be full with maximally different markets for maker/taker and spot/perp, + so add 30 bytes for market/oracle for taker and 30 bytes for maker + Add 32 bytes for LUT + size of transaction + 32 + 2 + 30 + 30 < 1232 + */ + assert(getSizeOfTransaction(ixs, false) < 1138); + + const tx = await makerDriftClient.buildTransaction(ixs); + await makerDriftClient.sendTransaction(tx as Transaction); + + const takerPosition = takerDriftClient.getUser().getPerpPosition(0); + + // All orders are placed and one is + // @ts-ignore + assert(takerPosition.maxMarginRatio === 100); + + await takerDriftClientUser.unsubscribe(); + await takerDriftClient.unsubscribe(); + }); }); async function initializeNewTakerClientAndUser( From 3ff1240728891546ec2c420798cc7362a37d1f37 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:19:27 +0000 Subject: [PATCH 025/247] sdk: release v2.139.0-beta.7 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index f6ab36d511..641c911887 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.6 \ No newline at end of file +2.139.0-beta.7 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c1db6e0e01..525e9c223e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.6", + "version": "2.139.0-beta.7", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 2d4e30b5bfac835c2251b8640b898408714a7c13 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:17:37 -0700 Subject: [PATCH 026/247] more robust isDelegateSigner for swift orders --- sdk/src/driftClient.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 4d39ae6d0f..914a57008d 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -6639,14 +6639,21 @@ export class DriftClient { readablePerpMarketIndex: marketIndex, }); - const isDelegateSigner = takerInfo.signingAuthority.equals( - takerInfo.takerUserAccount.delegate - ); - const borshBuf = Buffer.from( signedSignedMsgOrderParams.orderParams.toString(), 'hex' ); + + const isDelegateSigner = borshBuf + .slice(0, 8) + .equals( + Uint8Array.from( + Buffer.from( + sha256('global' + ':' + 'SignedMsgOrderParamsDelegateMessage') + ).slice(0, 8) + ) + ); + try { const { signedMsgOrderParams } = this.decodeSignedMsgOrderParamsMessage( borshBuf, From d02d12633c68cef95c996c0a40a46b7083a81aa1 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:02:06 +0000 Subject: [PATCH 027/247] sdk: release v2.139.0-beta.8 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 641c911887..7abe5cc709 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.7 \ No newline at end of file +2.139.0-beta.8 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 525e9c223e..994366ebad 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.7", + "version": "2.139.0-beta.8", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From f941e0d337259b6c981f6de1ee50734c2c6e66ae Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:15:07 -0400 Subject: [PATCH 028/247] program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit (#1909) * program: update-resolve-perp-pnl-pool-validate * CHANGELOG --------- Co-authored-by: Chris Heaney --- CHANGELOG.md | 1 + programs/drift/src/controller/amm.rs | 4 ++-- programs/drift/src/controller/insurance.rs | 15 ++++++++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f66acb9f71..eaf9a44228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit ([#1909](https://github.com/drift-labs/protocol-v2/pull/1909)) - program: auction order params account for twap divergence ([#1882](https://github.com/drift-labs/protocol-v2/pull/1882)) - program: add delegate stake if ([#1859](https://github.com/drift-labs/protocol-v2/pull/1859)) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 1088b680e7..0012b4804f 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -11,7 +11,7 @@ use crate::controller::spot_balance::{ }; use crate::error::{DriftResult, ErrorCode}; use crate::get_then_update_id; -use crate::math::amm::calculate_quote_asset_amount_swapped; +use crate::math::amm::{calculate_net_user_pnl, calculate_quote_asset_amount_swapped}; use crate::math::amm_spread::{calculate_spread_reserves, get_spread_reserves}; use crate::math::casting::Cast; use crate::math::constants::{ @@ -942,7 +942,7 @@ pub fn calculate_perp_market_amm_summary_stats( .safe_add(fee_pool_token_amount)? .cast()?; - let net_user_pnl = amm::calculate_net_user_pnl(&perp_market.amm, perp_market_oracle_price)?; + let net_user_pnl = calculate_net_user_pnl(&perp_market.amm, perp_market_oracle_price)?; // amm's mm_fee can be incorrect with drifting integer math error let mut new_total_fee_minus_distributions = pnl_tokens_available.safe_sub(net_user_pnl)?; diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 04f98f7e89..05770be9cf 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -843,11 +843,20 @@ pub fn resolve_perp_pnl_deficit( &SpotBalanceType::Deposit, )?; + let net_user_pnl = calculate_net_user_pnl( + &market.amm, + market + .amm + .historical_oracle_data + .last_oracle_price_twap_5min, + )?; + validate!( - pnl_pool_token_amount == 0, + pnl_pool_token_amount.cast::()? < net_user_pnl, ErrorCode::SufficientPerpPnlPool, - "pnl_pool_token_amount > 0 (={})", - pnl_pool_token_amount + "pnl_pool_token_amount >= net_user_pnl ({} >= {})", + pnl_pool_token_amount, + net_user_pnl )?; update_spot_market_cumulative_interest(spot_market, None, now)?; From 065b633a5cc0969a9a2d2fb9150bebb223cdf643 Mon Sep 17 00:00:00 2001 From: lil perp Date: Wed, 24 Sep 2025 19:17:31 -0400 Subject: [PATCH 029/247] program: add immutable owner support for token 22 vaults (#1904) * program: add immutable owner support for token 22 vaults * cargo fmt -- * CHANGELOG --- CHANGELOG.md | 1 + programs/drift/src/controller/token.rs | 13 +++++++++++++ programs/drift/src/instructions/admin.rs | 12 +++++++++++- programs/drift/src/instructions/constraints.rs | 10 ++++------ 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf9a44228..124cacd73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: all token 22 use immutable owner ([#1904](https://github.com/drift-labs/protocol-v2/pull/1904)) - program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit ([#1909](https://github.com/drift-labs/protocol-v2/pull/1909)) - program: auction order params account for twap divergence ([#1882](https://github.com/drift-labs/protocol-v2/pull/1882)) - program: add delegate stake if ([#1859](https://github.com/drift-labs/protocol-v2/pull/1859)) diff --git a/programs/drift/src/controller/token.rs b/programs/drift/src/controller/token.rs index 6e5d03193e..d9fd82230a 100644 --- a/programs/drift/src/controller/token.rs +++ b/programs/drift/src/controller/token.rs @@ -217,3 +217,16 @@ pub fn initialize_token_account<'info>( Ok(()) } + +pub fn initialize_immutable_owner<'info>( + token_program: &Interface<'info, TokenInterface>, + account: &AccountInfo<'info>, +) -> Result<()> { + let accounts = ::anchor_spl::token_interface::InitializeImmutableOwner { + account: account.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(token_program.to_account_info(), accounts); + ::anchor_spl::token_interface::initialize_immutable_owner(cpi_ctx)?; + + Ok(()) +} diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 9f29229fe4..4707078481 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -10,7 +10,7 @@ use pyth_solana_receiver_sdk::cpi::accounts::InitPriceUpdate; use pyth_solana_receiver_sdk::program::PythSolanaReceiver; use serum_dex::state::ToAlignedBytes; -use crate::controller::token::{close_vault, initialize_token_account}; +use crate::controller::token::{close_vault, initialize_immutable_owner, initialize_token_account}; use crate::error::ErrorCode; use crate::ids::{admin_hot_wallet, amm_spread_adjust_wallet, mm_oracle_crank_wallet}; use crate::instructions::constraints::*; @@ -148,6 +148,16 @@ pub fn handle_initialize_spot_market( let state = &mut ctx.accounts.state; let spot_market_pubkey = ctx.accounts.spot_market.key(); + let is_token_2022 = *ctx.accounts.spot_market_mint.to_account_info().owner == Token2022::id(); + if is_token_2022 { + initialize_immutable_owner(&ctx.accounts.token_program, &ctx.accounts.spot_market_vault)?; + + initialize_immutable_owner( + &ctx.accounts.token_program, + &ctx.accounts.insurance_fund_vault, + )?; + } + initialize_token_account( &ctx.accounts.token_program, &ctx.accounts.spot_market_vault, diff --git a/programs/drift/src/instructions/constraints.rs b/programs/drift/src/instructions/constraints.rs index c983ea471f..23deccd715 100644 --- a/programs/drift/src/instructions/constraints.rs +++ b/programs/drift/src/instructions/constraints.rs @@ -159,15 +159,13 @@ pub fn get_vault_len(mint: &InterfaceAccount) -> anchor_lang::Result::unpack(&mint_data)?; let mint_extensions = match mint_state.get_extension_types() { Ok(extensions) => extensions, - // If we cant deserialize the mint, we use the default token account length + // If we cant deserialize the mint, try assuming no extensions // Init token will fail if this size doesnt work, so worst case init account just fails - Err(_) => { - msg!("Failed to deserialize mint. Falling back to default token account length"); - return Ok(::anchor_spl::token::TokenAccount::LEN); - } + Err(_) => vec![], }; - let required_extensions = + let mut required_extensions = ExtensionType::get_required_init_account_extensions(&mint_extensions); + required_extensions.push(ExtensionType::ImmutableOwner); ExtensionType::try_calculate_account_len::(&required_extensions)? } else { ::anchor_spl::token::TokenAccount::LEN From 6b7a80ab58c41bad3a76e0192e974ced87ce8fa5 Mon Sep 17 00:00:00 2001 From: lil perp Date: Wed, 24 Sep 2025 19:21:00 -0400 Subject: [PATCH 030/247] sdk: tweak math for filling triggers (#1880) * sdk: tweak math for filling triggers * add back line --- sdk/src/math/auction.ts | 4 ++++ sdk/src/math/orders.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/src/math/auction.ts b/sdk/src/math/auction.ts index 5a58f60054..cabf5f4860 100644 --- a/sdk/src/math/auction.ts +++ b/sdk/src/math/auction.ts @@ -26,6 +26,10 @@ export function isFallbackAvailableLiquiditySource( return true; } + if ((order.bitFlags & OrderBitFlag.SafeTriggerOrder) !== 0) { + return true; + } + return new BN(slot).sub(order.slot).gt(new BN(minAuctionDuration)); } diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index 0ef192717e..5bfa6370bd 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -245,7 +245,7 @@ export function isFillableByVAMM( market, mmOraclePriceData, slot - ).gte(market.amm.minOrderSize)) || + ).gt(ZERO)) || isOrderExpired(order, ts) ); } From 76e880da794fa00374a94479e4496f622c953f82 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 23:26:27 +0000 Subject: [PATCH 031/247] sdk: release v2.139.0-beta.9 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 7abe5cc709..f403c9219c 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.8 \ No newline at end of file +2.139.0-beta.9 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 994366ebad..9e31f57cc0 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.8", + "version": "2.139.0-beta.9", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 99e23cee4bf39951ba6444b205707950e130c6a8 Mon Sep 17 00:00:00 2001 From: lil perp Date: Wed, 24 Sep 2025 20:26:36 -0400 Subject: [PATCH 032/247] program: allow delegate to update user position max margin ratio (#1913) --- programs/drift/src/instructions/user.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 8bfdd10066..6bff0e6fb4 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -4448,14 +4448,9 @@ pub struct UpdateUser<'info> { } #[derive(Accounts)] -#[instruction( - sub_account_id: u16, -)] pub struct UpdateUserPerpPositionCustomMarginRatio<'info> { #[account( mut, - seeds = [b"user", authority.key.as_ref(), sub_account_id.to_le_bytes().as_ref()], - bump, constraint = can_sign_for_user(&user, &authority)? )] pub user: AccountLoader<'info, User>, From ce844755f2ec37d15970934ab52d6c341a08d790 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:40:24 -0700 Subject: [PATCH 033/247] Revert "more robust isDelegateSigner for swift orders" This reverts commit 2d4e30b5bfac835c2251b8640b898408714a7c13. --- sdk/src/driftClient.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 914a57008d..4d39ae6d0f 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -6639,21 +6639,14 @@ export class DriftClient { readablePerpMarketIndex: marketIndex, }); + const isDelegateSigner = takerInfo.signingAuthority.equals( + takerInfo.takerUserAccount.delegate + ); + const borshBuf = Buffer.from( signedSignedMsgOrderParams.orderParams.toString(), 'hex' ); - - const isDelegateSigner = borshBuf - .slice(0, 8) - .equals( - Uint8Array.from( - Buffer.from( - sha256('global' + ':' + 'SignedMsgOrderParamsDelegateMessage') - ).slice(0, 8) - ) - ); - try { const { signedMsgOrderParams } = this.decodeSignedMsgOrderParamsMessage( borshBuf, From cbfb1d67f79fff0d21c701e031958b8b856678a5 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:45:19 +0000 Subject: [PATCH 034/247] sdk: release v2.139.0-beta.10 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index f403c9219c..16ad24ae16 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.9 \ No newline at end of file +2.139.0-beta.10 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 9e31f57cc0..5df7dedbbb 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.9", + "version": "2.139.0-beta.10", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From cf478fc1e34ceb20b1ded423c74f04df5b653886 Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Thu, 25 Sep 2025 09:00:58 +0800 Subject: [PATCH 035/247] update SwiftOrderMessage type for missing fields (#1908) --- sdk/src/swift/swiftOrderSubscriber.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/src/swift/swiftOrderSubscriber.ts b/sdk/src/swift/swiftOrderSubscriber.ts index c0555b8111..540691521d 100644 --- a/sdk/src/swift/swiftOrderSubscriber.ts +++ b/sdk/src/swift/swiftOrderSubscriber.ts @@ -60,6 +60,10 @@ export interface SwiftOrderMessage { /** Base64 string of a prerequisite deposit tx. The swift order_message should be bundled * after the deposit when present */ depositTx?: string; + /** order market index */ + market_index: number; + /** order timestamp in unix ms */ + ts: number; } export class SwiftOrderSubscriber { From aebcdf8bf8d3d9c1eae9bdf51d46ac80b996811f Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:05:34 +0000 Subject: [PATCH 036/247] sdk: release v2.139.0-beta.11 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 16ad24ae16..cfe4089ad0 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.10 \ No newline at end of file +2.139.0-beta.11 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 5df7dedbbb..89b6ee3a05 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.10", + "version": "2.139.0-beta.11", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From fef28bafa799cdda10b96ee5b9bd3fc05100a825 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 24 Sep 2025 21:12:13 -0400 Subject: [PATCH 037/247] sdk: add getUpdateFeatureBitFlagsMedianTriggerPriceIx --- sdk/src/adminClient.ts | 29 +++++++++++++++++++++++++++++ sdk/src/idl/drift.json | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 030677e254..e43bb4972e 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4652,6 +4652,35 @@ export class AdminClient extends DriftClient { ); } + public async updateFeatureBitFlagsMedianTriggerPrice( + enable: boolean + ): Promise { + const updateFeatureBitFlagsMedianTriggerPriceIx = + await this.getUpdateFeatureBitFlagsMedianTriggerPriceIx(enable); + const tx = await this.buildTransaction( + updateFeatureBitFlagsMedianTriggerPriceIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsMedianTriggerPriceIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsMedianTriggerPrice( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + public async updateDelegateUserGovTokenInsuranceStake( authority: PublicKey, delegate: PublicKey diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 6c25dcaf1c..4d8eb76ee2 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -3141,6 +3141,42 @@ ], "args": [] }, + { + "name": "updateDelegateUserGovTokenInsuranceStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "initializeInsuranceFundStake", "accounts": [ @@ -16218,5 +16254,8 @@ "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" } - ] + ], + "metadata": { + "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" + } } \ No newline at end of file From f6e1b03797908e3c8f39ad88d3d5236e5c561614 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:17:30 +0000 Subject: [PATCH 038/247] sdk: release v2.139.0-beta.12 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index cfe4089ad0..824d57b064 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.11 \ No newline at end of file +2.139.0-beta.12 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 89b6ee3a05..03db2b4d27 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.11", + "version": "2.139.0-beta.12", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 629d7aec970cb48d12a628b836977e48133f88cf Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 25 Sep 2025 09:08:53 -0700 Subject: [PATCH 039/247] update devnet market constants (#1914) --- sdk/src/constants/spotMarkets.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 503607398c..3840a37d8a 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -129,6 +129,18 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ '0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a', pythLazerId: 7, }, + { + symbol: 'GLXY', + marketIndex: 7, + poolId: 0, + oracle: new PublicKey('4wFrjUQHzRBc6qjVtMDbt28aEVgn6GaNiWR6vEff4KxR'), + oracleSource: OracleSource.Prelaunch, + mint: new PublicKey('2vVfXmcWXEaFzp7iaTVnQ4y1gR41S6tJQQMo1S5asJyC'), + precision: new BN(10).pow(SIX), + precisionExp: SIX, + pythFeedId: + '0x67e031d1723e5c89e4a826d80b2f3b41a91b05ef6122d523b8829a02e0f563aa', + }, ]; export const MainnetSpotMarkets: SpotMarketConfig[] = [ From 10c9c14fb9aee0f9d5b25164063891b31900c331 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:15:17 +0000 Subject: [PATCH 040/247] sdk: release v2.139.0-beta.13 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 824d57b064..07462d3582 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.12 \ No newline at end of file +2.139.0-beta.13 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 03db2b4d27..356f0248dc 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.12", + "version": "2.139.0-beta.13", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 94af1bd13a708dc567f89a4ac3c18515efee571d Mon Sep 17 00:00:00 2001 From: lil perp Date: Thu, 25 Sep 2025 12:42:23 -0400 Subject: [PATCH 041/247] program: deposit into if stake from admin (#1899) * program: deposit into if stake from admin * add test * change action * cargo fmt -- * move depositIntoInsuranceFundStake to adminClient --------- Co-authored-by: wphan --- programs/drift/src/controller/insurance.rs | 7 +- .../drift/src/controller/insurance/tests.rs | 23 ++- programs/drift/src/instructions/if_staker.rs | 157 +++++++++++++++++- programs/drift/src/lib.rs | 8 + programs/drift/src/state/events.rs | 1 + sdk/src/adminClient.ts | 51 ++++++ sdk/src/idl/drift.json | 68 ++++++++ tests/insuranceFundStake.ts | 29 ++++ 8 files changed, 341 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 05770be9cf..17cb020405 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -112,6 +112,7 @@ pub fn add_insurance_fund_stake( user_stats: &mut UserStats, spot_market: &mut SpotMarket, now: i64, + admin_deposit: bool, ) -> DriftResult { validate!( !(insurance_vault_amount == 0 && spot_market.insurance_fund.total_shares != 0), @@ -161,7 +162,11 @@ pub fn add_insurance_fund_stake( emit!(InsuranceFundStakeRecord { ts: now, user_authority: user_stats.authority, - action: StakeAction::Stake, + action: if admin_deposit { + StakeAction::AdminDeposit + } else { + StakeAction::Stake + }, amount, market_index: spot_market.market_index, insurance_vault_amount_before: insurance_vault_amount, diff --git a/programs/drift/src/controller/insurance/tests.rs b/programs/drift/src/controller/insurance/tests.rs index 4301a3d26f..c71948c3d3 100644 --- a/programs/drift/src/controller/insurance/tests.rs +++ b/programs/drift/src/controller/insurance/tests.rs @@ -41,6 +41,7 @@ pub fn basic_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -104,6 +105,7 @@ pub fn basic_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 1234); @@ -141,6 +143,7 @@ pub fn basic_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -202,6 +205,7 @@ pub fn basic_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 1234); @@ -245,6 +249,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -334,6 +339,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 20, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 199033744205760); @@ -346,6 +352,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 30, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 398067488411520); @@ -378,6 +385,7 @@ pub fn gains_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -502,6 +510,7 @@ pub fn losses_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -631,6 +640,7 @@ pub fn escrow_losses_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -729,7 +739,8 @@ pub fn escrow_gains_stake_if_test() { &mut if_stake, &mut user_stats, &mut spot_market, - 0 + 0, + false, ) .is_err()); @@ -741,6 +752,7 @@ pub fn escrow_gains_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -858,6 +870,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut user_stats, &mut spot_market, 0, + false, ) .is_err()); @@ -877,6 +890,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); if_balance += amount; @@ -912,6 +926,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut orig_user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -1010,6 +1025,7 @@ pub fn drained_stake_if_test_rebase_on_old_remove_all() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -1210,6 +1226,7 @@ pub fn drained_stake_if_test_rebase_on_old_remove_all_2() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); if_balance += 10_000_000_000_000; @@ -1254,6 +1271,7 @@ pub fn multiple_if_stakes_and_rebase() { &mut user_stats_1, &mut spot_market, 0, + false, ) .unwrap(); @@ -1266,6 +1284,7 @@ pub fn multiple_if_stakes_and_rebase() { &mut user_stats_2, &mut spot_market, 0, + false, ) .unwrap(); @@ -1392,6 +1411,7 @@ pub fn multiple_if_stakes_and_rebase_and_admin_remove() { &mut user_stats_1, &mut spot_market, 0, + false, ) .unwrap(); @@ -1404,6 +1424,7 @@ pub fn multiple_if_stakes_and_rebase_and_admin_remove() { &mut user_stats_2, &mut spot_market, 0, + false, ) .unwrap(); diff --git a/programs/drift/src/instructions/if_staker.rs b/programs/drift/src/instructions/if_staker.rs index 51e4d7fd79..1d15b5ff6f 100644 --- a/programs/drift/src/instructions/if_staker.rs +++ b/programs/drift/src/instructions/if_staker.rs @@ -3,7 +3,7 @@ use anchor_lang::Discriminator; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use crate::error::ErrorCode; -use crate::ids::if_rebalance_wallet; +use crate::ids::{admin_hot_wallet, if_rebalance_wallet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::optional_accounts::get_token_mint; @@ -143,6 +143,7 @@ pub fn handle_add_insurance_fund_stake<'c: 'info, 'info>( user_stats, spot_market, clock.unix_timestamp, + false, )?; controller::token::receive( @@ -821,6 +822,114 @@ pub fn handle_transfer_protocol_if_shares_to_revenue_pool<'c: 'info, 'info>( Ok(()) } +pub fn handle_deposit_into_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoInsuranceFundStake<'info>>, + market_index: u16, + amount: u64, +) -> Result<()> { + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let insurance_fund_stake = &mut load_mut!(ctx.accounts.insurance_fund_stake)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; + let state = &ctx.accounts.state; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + + validate!( + !spot_market.is_insurance_fund_operation_paused(InsuranceFundOperation::Add), + ErrorCode::InsuranceFundOperationPaused, + "if staking add disabled", + )?; + + validate!( + insurance_fund_stake.market_index == market_index, + ErrorCode::IncorrectSpotMarketAccountPassed, + "insurance_fund_stake does not match market_index" + )?; + + validate!( + spot_market.status != MarketStatus::Initialized, + ErrorCode::InvalidSpotMarketState, + "spot market = {} not active for insurance_fund_stake", + spot_market.market_index + )?; + + validate!( + insurance_fund_stake.last_withdraw_request_shares == 0 + && insurance_fund_stake.last_withdraw_request_value == 0, + ErrorCode::IFWithdrawRequestInProgress, + "withdraw request in progress" + )?; + + { + if spot_market.has_transfer_hook() { + controller::insurance::attempt_settle_revenue_to_insurance_fund( + &ctx.accounts.spot_market_vault, + &ctx.accounts.insurance_fund_vault, + spot_market, + now, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + state, + &mint, + Some(&mut remaining_accounts_iter.clone()), + )?; + } else { + controller::insurance::attempt_settle_revenue_to_insurance_fund( + &ctx.accounts.spot_market_vault, + &ctx.accounts.insurance_fund_vault, + spot_market, + now, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + state, + &mint, + None, + )?; + }; + + // reload the vault balances so they're up-to-date + ctx.accounts.spot_market_vault.reload()?; + ctx.accounts.insurance_fund_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + } + + controller::insurance::add_insurance_fund_stake( + amount, + ctx.accounts.insurance_fund_vault.amount, + insurance_fund_stake, + user_stats, + spot_market, + clock.unix_timestamp, + true, + )?; + + controller::token::receive( + &ctx.accounts.token_program, + &ctx.accounts.user_token_account, + &ctx.accounts.insurance_fund_vault, + &ctx.accounts.signer.to_account_info(), + amount, + &mint, + if spot_market.has_transfer_hook() { + Some(remaining_accounts_iter) + } else { + None + }, + )?; + + Ok(()) +} + #[derive(Accounts)] #[instruction( market_index: u16, @@ -1082,3 +1191,49 @@ pub struct TransferProtocolIfSharesToRevenuePool<'info> { /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, } + +#[derive(Accounts)] +#[instruction(market_index: u16,)] +pub struct DepositIntoInsuranceFundStake<'info> { + pub signer: Signer<'info>, + #[account( + mut, + constraint = signer.key() == admin_hot_wallet::id() || signer.key() == state.admin + )] + pub state: Box>, + #[account( + mut, + seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], + bump + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + seeds = [b"insurance_fund_stake", user_stats.load()?.authority.as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_stake: AccountLoader<'info, InsuranceFundStake>, + #[account(mut)] + pub user_stats: AccountLoader<'info, UserStats>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + mut, + seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_vault: Box>, + #[account( + mut, + token::mint = insurance_fund_vault.mint, + token::authority = signer + )] + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 517ddb2c86..81c7794515 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -813,6 +813,14 @@ pub mod drift { handle_transfer_protocol_if_shares_to_revenue_pool(ctx, market_index, amount) } + pub fn deposit_into_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoInsuranceFundStake<'info>>, + market_index: u16, + amount: u64, + ) -> Result<()> { + handle_deposit_into_insurance_fund_stake(ctx, market_index, amount) + } + pub fn update_pyth_pull_oracle( ctx: Context, feed_id: [u8; 32], diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 55e9cecaeb..2d6c20fa7a 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -580,6 +580,7 @@ pub enum StakeAction { Unstake, UnstakeTransfer, StakeTransfer, + AdminDeposit, } #[event] diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index e43bb4972e..f6b5b96f41 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -15,6 +15,7 @@ import { AssetTier, SpotFulfillmentConfigStatus, IfRebalanceConfigParams, + TxParams, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -4729,4 +4730,54 @@ export class AdminClient extends DriftClient { return ix; } + + public async depositIntoInsuranceFundStake( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey, + txParams?: TxParams + ): Promise { + const tx = await this.buildTransaction( + await this.getDepositIntoInsuranceFundStakeIx( + marketIndex, + amount, + userStatsPublicKey, + insuranceFundStakePublicKey, + userTokenAccountPublicKey + ), + txParams + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositIntoInsuranceFundStakeIx( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey + ): Promise { + const spotMarket = this.getSpotMarketAccount(marketIndex); + return await this.program.instruction.depositIntoInsuranceFundStake( + marketIndex, + amount, + { + accounts: { + signer: this.wallet.publicKey, + state: await this.getStatePublicKey(), + spotMarket: spotMarket.pubkey, + insuranceFundStake: insuranceFundStakePublicKey, + userStats: userStatsPublicKey, + spotMarketVault: spotMarket.vault, + insuranceFundVault: spotMarket.insuranceFund.vault, + userTokenAccount: userTokenAccountPublicKey, + tokenProgram: this.getTokenProgramForSpotMarket(spotMarket), + driftSigner: this.getSignerPublicKey(), + }, + } + ); + } } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 4d8eb76ee2..bafefbacc5 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -3670,6 +3670,71 @@ } ] }, + { + "name": "depositIntoInsuranceFundStake", + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, { "name": "updatePythPullOracle", "accounts": [ @@ -12242,6 +12307,9 @@ }, { "name": "StakeTransfer" + }, + { + "name": "AdminDeposit" } ] } diff --git a/tests/insuranceFundStake.ts b/tests/insuranceFundStake.ts index c90a1795ce..0f67d1814c 100644 --- a/tests/insuranceFundStake.ts +++ b/tests/insuranceFundStake.ts @@ -29,6 +29,7 @@ import { unstakeSharesToAmount, MarketStatus, LIQUIDATION_PCT_PRECISION, + getUserStatsAccountPublicKey, } from '../sdk/src'; import { @@ -40,6 +41,7 @@ import { sleep, mockOracleNoProgram, setFeedPriceNoProgram, + mintUSDCToUser, } from './testHelpers'; import { ContractTier, PERCENTAGE_PRECISION, UserStatus } from '../sdk'; import { startAnchor } from 'solana-bankrun'; @@ -1163,6 +1165,33 @@ describe('insurance fund stake', () => { // assert(usdcBefore.eq(usdcAfter)); }); + it('admin deposit into insurance fund stake', async () => { + await mintUSDCToUser( + usdcMint, + userUSDCAccount.publicKey, + usdcAmount, + bankrunContextWrapper + ); + const marketIndex = 0; + const insuranceFundStakePublicKey = getInsuranceFundStakeAccountPublicKey( + driftClient.program.programId, + driftClient.wallet.publicKey, + marketIndex + ); + const userStatsPublicKey = getUserStatsAccountPublicKey( + driftClient.program.programId, + driftClient.wallet.publicKey + ); + const txSig = await driftClient.depositIntoInsuranceFundStake( + marketIndex, + usdcAmount, + userStatsPublicKey, + insuranceFundStakePublicKey, + userUSDCAccount.publicKey + ); + bankrunContextWrapper.printTxLogs(txSig); + }); + // it('settle spotMarket to insurance vault', async () => { // const marketIndex = new BN(0); From 0aa0da34d378278545612ccaef0fb7dd50da9c0c Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:48:43 +0000 Subject: [PATCH 042/247] sdk: release v2.139.0-beta.14 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 07462d3582..9d03327efa 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.13 \ No newline at end of file +2.139.0-beta.14 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 356f0248dc..6f3ad7117d 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.13", + "version": "2.139.0-beta.14", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 5355cdc53607af0104ece4099552fd710e21af69 Mon Sep 17 00:00:00 2001 From: lil perp Date: Thu, 25 Sep 2025 12:55:27 -0400 Subject: [PATCH 043/247] program: comment out unused ix (#1911) --- programs/drift/src/lib.rs | 75 ++++++++------- sdk/src/idl/drift.json | 197 -------------------------------------- 2 files changed, 38 insertions(+), 234 deletions(-) diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 81c7794515..0b3eedd16b 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -413,13 +413,13 @@ pub mod drift { handle_update_user_reduce_only(ctx, _sub_account_id, reduce_only) } - pub fn update_user_advanced_lp( - ctx: Context, - _sub_account_id: u16, - advanced_lp: bool, - ) -> Result<()> { - handle_update_user_advanced_lp(ctx, _sub_account_id, advanced_lp) - } + // pub fn update_user_advanced_lp( + // ctx: Context, + // _sub_account_id: u16, + // advanced_lp: bool, + // ) -> Result<()> { + // handle_update_user_advanced_lp(ctx, _sub_account_id, advanced_lp) + // } pub fn update_user_protected_maker_orders( ctx: Context, @@ -525,9 +525,9 @@ pub mod drift { handle_update_user_stats_referrer_info(ctx) } - pub fn update_user_open_orders_count(ctx: Context) -> Result<()> { - handle_update_user_open_orders_count(ctx) - } + // pub fn update_user_open_orders_count(ctx: Context) -> Result<()> { + // handle_update_user_open_orders_count(ctx) + // } pub fn admin_disable_update_perp_bid_ask_twap( ctx: Context, @@ -781,13 +781,14 @@ pub mod drift { handle_remove_insurance_fund_stake(ctx, market_index) } - pub fn transfer_protocol_if_shares( - ctx: Context, - market_index: u16, - shares: u128, - ) -> Result<()> { - handle_transfer_protocol_if_shares(ctx, market_index, shares) - } + // pub fn transfer_protocol_if_shares( + // ctx: Context, + // market_index: u16, + // shares: u128, + // ) -> Result<()> { + // handle_transfer_protocol_if_shares(ctx, market_index, shares) + // } + pub fn begin_insurance_fund_swap<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InsuranceFundSwap<'info>>, in_market_index: u16, @@ -950,9 +951,9 @@ pub mod drift { handle_update_phoenix_fulfillment_config_status(ctx, status) } - pub fn update_serum_vault(ctx: Context) -> Result<()> { - handle_update_serum_vault(ctx) - } + // pub fn update_serum_vault(ctx: Context) -> Result<()> { + // handle_update_serum_vault(ctx) + // } pub fn initialize_perp_market<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InitializePerpMarket<'info>>, @@ -1675,23 +1676,23 @@ pub mod drift { handle_update_spot_auction_duration(ctx, default_spot_auction_duration) } - pub fn initialize_protocol_if_shares_transfer_config( - ctx: Context, - ) -> Result<()> { - handle_initialize_protocol_if_shares_transfer_config(ctx) - } - - pub fn update_protocol_if_shares_transfer_config( - ctx: Context, - whitelisted_signers: Option<[Pubkey; 4]>, - max_transfer_per_epoch: Option, - ) -> Result<()> { - handle_update_protocol_if_shares_transfer_config( - ctx, - whitelisted_signers, - max_transfer_per_epoch, - ) - } + // pub fn initialize_protocol_if_shares_transfer_config( + // ctx: Context, + // ) -> Result<()> { + // handle_initialize_protocol_if_shares_transfer_config(ctx) + // } + + // pub fn update_protocol_if_shares_transfer_config( + // ctx: Context, + // whitelisted_signers: Option<[Pubkey; 4]>, + // max_transfer_per_epoch: Option, + // ) -> Result<()> { + // handle_update_protocol_if_shares_transfer_config( + // ctx, + // whitelisted_signers, + // max_transfer_per_epoch, + // ) + // } pub fn initialize_prelaunch_oracle( ctx: Context, diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index bafefbacc5..de56f7ab16 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1573,31 +1573,6 @@ } ] }, - { - "name": "updateUserAdvancedLp", - "accounts": [ - { - "name": "user", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [ - { - "name": "subAccountId", - "type": "u16" - }, - { - "name": "advancedLp", - "type": "bool" - } - ] - }, { "name": "updateUserProtectedMakerOrders", "accounts": [ @@ -2092,32 +2067,6 @@ ], "args": [] }, - { - "name": "updateUserOpenOrdersCount", - "accounts": [ - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, - { - "name": "filler", - "isMut": true, - "isSigner": false - }, - { - "name": "user", - "isMut": true, - "isSigner": false - } - ], - "args": [] - }, { "name": "adminDisableUpdatePerpBidAskTwap", "accounts": [ @@ -3425,61 +3374,6 @@ } ] }, - { - "name": "transferProtocolIfShares", - "accounts": [ - { - "name": "signer", - "isMut": false, - "isSigner": true - }, - { - "name": "transferConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "spotMarket", - "isMut": true, - "isSigner": false - }, - { - "name": "insuranceFundStake", - "isMut": true, - "isSigner": false - }, - { - "name": "userStats", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, - { - "name": "insuranceFundVault", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "shares", - "type": "u128" - } - ] - }, { "name": "beginInsuranceFundSwap", "accounts": [ @@ -4373,27 +4267,6 @@ } ] }, - { - "name": "updateSerumVault", - "accounts": [ - { - "name": "state", - "isMut": true, - "isSigner": false - }, - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "srmVault", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "initializePerpMarket", "accounts": [ @@ -6987,76 +6860,6 @@ } ] }, - { - "name": "initializeProtocolIfSharesTransferConfig", - "accounts": [ - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "protocolIfSharesTransferConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "updateProtocolIfSharesTransferConfig", - "accounts": [ - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "protocolIfSharesTransferConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "whitelistedSigners", - "type": { - "option": { - "array": [ - "publicKey", - 4 - ] - } - } - }, - { - "name": "maxTransferPerEpoch", - "type": { - "option": "u128" - } - } - ] - }, { "name": "initializePrelaunchOracle", "accounts": [ From db76d633a0796ca5473917a44d6b4d982141628b Mon Sep 17 00:00:00 2001 From: 0xbigz <83473873+0xbigz@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:55:34 -0400 Subject: [PATCH 044/247] program: raise MAX_BASE_ASSET_AMOUNT_WITH_AMM numerical invariant --- programs/drift/src/math/constants.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index 1c127315ae..dc6ffeb94f 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -168,7 +168,7 @@ pub const MAX_K_BPS_INCREASE: i128 = TEN_BPS; pub const MAX_K_BPS_DECREASE: i128 = TWO_PT_TWO_PCT; pub const MAX_UPDATE_K_PRICE_CHANGE: u128 = HUNDRENTH_OF_CENT; pub const MAX_SQRT_K: u128 = 1000000000000000000000; // 1e21 (count 'em!) -pub const MAX_BASE_ASSET_AMOUNT_WITH_AMM: u128 = 100000000000000000; // 1e17 (count 'em!) +pub const MAX_BASE_ASSET_AMOUNT_WITH_AMM: u128 = 400000000000000000; // 4e17 (count 'em!) pub const MAX_PEG_BPS_INCREASE: u128 = TEN_BPS as u128; // 10 bps increase pub const MAX_PEG_BPS_DECREASE: u128 = TEN_BPS as u128; // 10 bps decrease From 7ee2a8e3bbbdf17ab28891f0735cd793cf0a59d3 Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 25 Sep 2025 10:58:53 -0700 Subject: [PATCH 045/247] v2.139.0 --- CHANGELOG.md | 8 +++ Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 2 +- yarn.lock | 109 ++++++++++++++++++++++++++++++-------- 6 files changed, 100 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 124cacd73a..4e6a29b629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +### Fixes + +### Breaking + +## [2.139.0] - 2025-09-25 + +### Features + - program: all token 22 use immutable owner ([#1904](https://github.com/drift-labs/protocol-v2/pull/1904)) - program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit ([#1909](https://github.com/drift-labs/protocol-v2/pull/1909)) - program: auction order params account for twap divergence ([#1882](https://github.com/drift-labs/protocol-v2/pull/1882)) diff --git a/Cargo.lock b/Cargo.lock index 9ca9b1301f..cc1c58e9fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.138.0" +version = "2.139.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 39d0571545..6c21eefc2a 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.138.0" +version = "2.139.0" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index 6f3ad7117d..6651f5a05b 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0-beta.14", + "version": "2.139.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index de56f7ab16..7b9b4293a8 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.138.0", + "version": "2.139.0", "name": "drift", "instructions": [ { diff --git a/yarn.lock b/yarn.lock index 41c9b1a9d6..2678bc1b59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -895,12 +895,12 @@ anchor-bankrun@0.3.0: resolved "https://registry.yarnpkg.com/anchor-bankrun/-/anchor-bankrun-0.3.0.tgz#3789fcecbc201a2334cff228b99cc0da8ef0167e" integrity sha512-PYBW5fWX+iGicIS5MIM/omhk1tQPUc0ELAnI/IkLKQJ6d75De/CQRh8MF2bU/TgGyFi6zEel80wUe3uRol9RrQ== -ansi-regex@^5.0.1: +ansi-regex@5.0.1, ansi-regex@^5.0.1, ansi-regex@^6.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@4.3.0, ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -938,6 +938,11 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -967,6 +972,11 @@ axios@^1.5.1, axios@^1.8.3, axios@^1.9.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +backslash@<0.2.1: + version "0.2.0" + resolved "https://registry.yarnpkg.com/backslash/-/backslash-0.2.0.tgz#6c3c1fce7e7e714ccfc10fd74f0f73410677375f" + integrity sha512-Avs+8FUZ1HF/VFP4YWwHQZSGzRPm37ukU1JQYQWijuHhtXdOuAzcZ8PcAzfIw898a8PyBzdn+RtnKA6MzW0X2A== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1167,7 +1177,14 @@ chai@4.4.1: pathval "^1.1.1" type-detect "^4.0.8" -chalk@^4.0.0, chalk@~4.1.2: +chalk-template@<1.1.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-1.1.0.tgz#ffc55db6dd745e9394b85327c8ac8466edb7a7b1" + integrity sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg== + dependencies: + chalk "^5.2.0" + +chalk@4.1.2, chalk@^4.0.0, chalk@^5.2.0, chalk@^5.3.0, chalk@^5.4.1, chalk@~4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1175,11 +1192,6 @@ chalk@^4.0.0, chalk@~4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.3.0, chalk@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" - integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== - check-error@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" @@ -1196,17 +1208,24 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== +color-convert@<3.1.1, color-convert@^2.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-3.1.0.tgz#ce16ebb832f9d7522649ed9e11bc0ccb9433a524" + integrity sha512-TVoqAq8ZDIpK5lsQY874DDnu65CSsc9vzq0wLpNQ6UMBq81GSZocVazPiBbYGzngzBOIRahpkTzCLVe2at4MfA== dependencies: - color-name "~1.1.4" + color-name "^2.0.0" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@<2.0.1, color-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.0.0.tgz#03ff6b1b5aec9bb3cf1ed82400c2790dfcd01d2d" + integrity sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow== + +color-string@<2.1.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-2.1.0.tgz#a1cc4bb16a23032ff1048a2458a170323b15a23f" + integrity sha512-gNVoDzpaSwvftp6Y8nqk97FtZoXP9Yj7KGYB8yIXuv0JcfqbYihTrd1OU5iZW9btfXde4YAOCRySBHT7O910MA== + dependencies: + color-name "^2.0.0" combined-stream@^1.0.8: version "1.0.8" @@ -1270,7 +1289,7 @@ csvtojson@2.0.10: lodash "^4.17.3" strip-bom "^2.0.0" -debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@<4.4.2, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.4.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== @@ -1373,6 +1392,13 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +error-ex@<1.3.3: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" @@ -1761,11 +1787,23 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +has-ansi@<6.0.1: + version "6.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-6.0.0.tgz#8118b2fb548c062f9356c7d5013b192a238ce3b3" + integrity sha512-1AYj+gqAskFf9Skb7xuEYMfJqkW3TJ8lukw4Fczw+Y6jRkgxvcE4JiFWuTO4DsoleMvvHudryolA9ObJHJKHWQ== + dependencies: + ansi-regex "^6.0.1" + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-flag@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-5.0.1.tgz#5483db2ae02a472d1d0691462fc587d1843cd940" + integrity sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA== + has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" @@ -1848,6 +1886,11 @@ is-arguments@^1.0.4: call-bound "^1.0.2" has-tostringtag "^1.0.2" +is-arrayish@<0.3.3, is-arrayish@^0.2.1, is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -2491,11 +2534,27 @@ shiki@^0.11.1: vscode-oniguruma "^1.6.1" vscode-textmate "^6.0.0" +simple-swizzle@<0.2.3: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -2572,7 +2631,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -2619,13 +2678,21 @@ superstruct@^2.0.2: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== -supports-color@^7.1.0: +supports-color@7.2.0, supports-color@^10.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" +supports-hyperlinks@<4.1.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-4.1.0.tgz#f006d9e2f6b9b6672f86c86c6f76bf52a69f4d91" + integrity sha512-6lY0rDZ5bbZhAPrwpz/nMR6XmeaFmh2itk7YnIyph2jblPmDcKMCPkSdLFTlaX8snBvg7OJmaOL3WRLqMEqcJQ== + dependencies: + has-flag "^5.0.1" + supports-color "^10.0.0" + text-encoding-utf-8@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" @@ -2803,7 +2870,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -wrap-ansi@^7.0.0: +wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 1ba87a79002568559e00176101deec04b5a6cb0b Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:07:27 +0000 Subject: [PATCH 046/247] sdk: release v2.140.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 9d03327efa..9fb14cec6a 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.139.0-beta.14 \ No newline at end of file +2.140.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 6651f5a05b..bf99563351 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.139.0", + "version": "2.140.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 0f73d5054b2192b644b3ee3af27fbdaab8567e82 Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:15:58 -0400 Subject: [PATCH 047/247] sdk: update constants market index 77 (#1916) --- sdk/src/constants/perpMarkets.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 2e0f8aac86..bbb7a12423 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1325,6 +1325,19 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ '0xa903b5a82cb572397e3d47595d2889cf80513f5b4cf7a36b513ae10cc8b1e338', pythLazerId: 2310, }, + { + fullName: 'PLASMA', + category: ['DEX'], + symbol: 'XPL-PERP', + baseAssetSymbol: 'XPL', + marketIndex: 77, + oracle: new PublicKey('6kgE1KJcxTux4tkPLE8LL8GRyW2cAsvyZsDFWqCrhHVe'), + launchTs: 1758898862000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0x9873512f5cb33c77ad7a5af098d74812c62111166be395fd0941c8cedb9b00d4', + pythLazerId: 2312, + }, ]; export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = { From 6a8a7be0bb728745bb8b267c42e046d93e506917 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:21:27 +0000 Subject: [PATCH 048/247] sdk: release v2.140.0-beta.1 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 9fb14cec6a..ea9c93bfd5 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.140.0-beta.0 \ No newline at end of file +2.140.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index bf99563351..9ce87d3f79 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.140.0-beta.0", + "version": "2.140.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 923e4a1155d7cda2e742844ad5d73ae751278d4b Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 29 Sep 2025 12:06:42 -0700 Subject: [PATCH 049/247] Wphan/builder codes (#1805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * program: init lp pool * cargo fmt -- * add total fee fields * add update_target_weights math * program: use sparse matrix for constituent map and update tests * zero copy accounts, init ix (#1578) * update accounts (#1580) * zero copy + permissionless crank ixs (#1581) * program: support negative target weights for borrow-lend * fix tests to work with zero copy * few comment changes * remove discriminator from impl macro * add get_swap_amount, get_swap_fees, get_weight (#1579) * add get_swap_amount, get_swap_fees, get_weight * update accounts * add back ts * rebase * add constituent swap fees * fix swap fee calc (#1582) * add init amm mapping to lp context (#1583) * init constituent * add initializeLpPool test (#1585) * add initializeLpPool test * add check for constituent target weights * add add datum ix * add init tests and invariant checks * rename data to more useful names * dlp use spl token program (#1588) * add crank ix * update total_weight for validation_flags check * push test so far * overriding perp position works * remove message * fix dup total_weight add * constituent map remaining accounts * compiles * bankrun tests pass * compiles but casting failure in overflow protection test * address comment and change token arguments from u64 to u128 * bankrun tests pass * init constituent token account (#1596) * update aum calc * add update /remove mapping ixs * fix test - init constituent spot market * add crank improvements * passes tests * precision fix crank aum * precision fixes and constituent map check for account owner * add passthrough account logic (#1602) * add passthrough account logic * cant read yet * fix all zc alignment issues * make oracle source a u8 on zc struct * Wphan/dlp-swap-ixs (#1592) * add lp_swap ix * rebase * test helpers * swap works * fix swaps, add more cargo tests for fees n swap amt * remove console.logs * address PR comments * merge upstream * post-merge fixes * store bumps on accounts (#1604) * store bumps on accounts * do pda check in constituent map * address comments * Wphan/add liquidity (#1607) * add add remove liquidity fees calc * add liquidity ix * fix init mint and lppool token account, refactor test fees * add removeLiquidity bankrun test * merge upstream * add LPPool.next_mint_redeem_id * program: lp-pool-to-use-target-base-vector (#1615) * init lp pool target-base matrix * working target-base logic * add todos for add/remove liquidity aum * add renames + fix test * add beta and cost to trade in bps to target datum * add more tests * add fields to LP events, fix tests (#1620) * add fields to LP events, fix tests * revert target weight calc * add constituent.next_swap_id, fix cost_to_trade math * dlp jup swap (#1636) * dlp jup swap * add admin client ixs * almost fixed * test working? * update begin and end swap * tweaks * fix math on how much was swapped * remove unnecessary lp pool args * extra account validation * added token account pda checks in other ixs * stablecoin targets (#1638) * is stablecoin * address comments --------- Co-authored-by: Chris Heaney * cleanup * transfer oracle data ix to constituent (#1643) * transfer oracle data ix to constituent * add lib entrypoint * simplify more * add spot market constraint * big cargo test (#1644) * derivative constituents + better testing + bug fixes (#1657) * all tests technically pass * update tests + prettify * bug fixes and tests pass * fix many bugs and finalize logic * deposit/borrow working and changing positions (#1652) * sdk: allow custom coder * program: dlp add upnl for settles to amm cache (#1659) * program: dlp add-upnl-for-settles-to-amm-cache * finish up lp pool transfer from perp market * add amount_to_transfer using diff * merge * add pnl and fee pool accounting + transfer from dlp to perp market --------- Co-authored-by: Nour Alharithi * remove unused accounts coder * move customCoder into sdk, lint * testing: ix: settle perp to dlp, insufficient balance edge case and improvements (#1688) * finish edge case test * aum check also passes * prettify * added more settle test coverage and squash bugs (#1689) * dlp: add constituentMap (#1699) * Nour/gauntlet fee impl (#1698) * added correlation matrix infra * refactor builds * mint redeem handled for usdc * remove liquidity also should work * all tests pass * bankrun tests pass too * update aum considers amm cache (#1701) * prettify (#1702) * Wphan/merge master dlp (#1703) * feat: init swift user orders on user account creation if needed * fix: wrong pushing of swift user orders ixs * fix: broken swift tests * fix: swift -> signed msg * refactor(sdk): update jupiter's api url * fix(sdk): remove error thrown * indicative qutoes server changes * sdk: release v2.121.0-beta.7 * sdK: update market index 33 oracle rr (#1606) * sdk: add to spot constants market index 34 * revert adminClient.ts change * sdk: update spot market constants oracle index 33 * sdk: release v2.121.0-beta.8 * sdk: high leverage mode updates (#1605) * sdk: high leverage mode updates * add optional param for fee calc * update changelog * sdk: release v2.121.0-beta.9 * getPlaceSignedMsgTakerPerpOrderIxs infer HLM mode from bitflags (#1608) * sdk: release v2.121.0-beta.10 * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs (#1610) * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs * bankrun test * sdk: release v2.121.0-beta.11 * sdk: round tick/step size for getVammL2Generateor (#1612) * sdk: round tick/step size for etVammL2Generateor * use standard functions, include in all fcns * fix const declare, rm whitespace * fix posdir sign * sdk: release v2.121.0-beta.12 * sdk: release v2.121.0-beta.13 * sdk: constants market-index-45-46 (#1618) * sdk: release v2.121.0-beta.14 * robustness check for indicative quotes sender (#1621) * robustness check for indicative quotes sender * delete quote from market index of bad quote * sdk: release v2.121.0-beta.15 * Added launchTs for ZEUS, zBTC * sdk: release v2.121.0-beta.16 * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622) * sdk: release v2.121.0-beta.17 * sdk: fix vamm l2 generator base swapped (#1623) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * sdk: release v2.121.0-beta.18 * sdk: revert vamm l2 gen (#1624) * Revert "sdk: fix vamm l2 generator base swapped (#1623)" This reverts commit 56bc78d70e82cb35a90f12f73162bffb640cb655. * Revert "sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622)" This reverts commit e49cfd554cc44cd8d7770184f02f6ddb0bfc92f1. * Revert "sdk: round tick/step size for getVammL2Generateor (#1612)" This reverts commit f932a4ea2afcae314e406b7c7ee35e55b36043ad. * sdk: release v2.121.0-beta.19 * sdk: show protected-asset have zero-borrow-limit (#1603) * sdk: show protected-asset have zero-borrow-limit * rm unused AssetTier import * sdk: release v2.121.0-beta.20 * sdk: market-constants-index-74 (#1629) * sdk: release v2.121.0-beta.21 * program: use saturating_sub for number_of_users (#1616) * program: use saturating_sub for number_of_users * update CHANGELOG.md * program: allow fixing hlm num users (#1630) * sdk: release v2.121.0-beta.22 * sdk: fix switchboard on demand client to use landed at * sdk: release v2.121.0-beta.23 * sdk: spot-market-poolid-4 constants (#1631) * sdk: release v2.121.0-beta.24 * fix high lev mode liq price (#1632) * sdk: release v2.121.0-beta.25 * replace deprecated solana install scripts (#1634) * sdk: release v2.121.0-beta.26 * refactor(sdk): use ReturnType for Timeout types (#1637) * sdk: release v2.121.0-beta.27 * auction price sdk fix * sdk: release v2.121.0-beta.28 * program: multi piecewise interest rate curve (#1560) * program: multi-piecewise-interest-rate-curve * update tests * widen out borrow limits/healthy util check * add break, use array of array for borrow slope segments * program: fix cargo test * sdk: add segmented IR curve to interest rate calc * clean up unusded var, make interest rate segment logic a const * incorp efficiency feedback points * test: add sol realistic market example * cargo fmt -- * CHANGELOG --------- Co-authored-by: Chris Heaney * sdk: release v2.121.0-beta.29 * program: allow hot admin to update market fuel params (#1640) * v2.121.0 * sdk: release v2.122.0-beta.0 * sdk: fix nullish coalescing * sdk: release v2.122.0-beta.1 * program: add logging for wrong perp market mutability * sdk: check free collateral change in maxTradeSizeUsdcForPerp (#1645) * sdk: check free collateral change in maxTradeSizeUsdcForPerp * update changelog * sdk: release v2.122.0-beta.2 * refactor(sdk): emit newSlot event on initial subscribe call (#1646) * sdk: release v2.122.0-beta.3 * sdk: spot-market-constants-pool-id-2 (#1647) * sdk: release v2.122.0-beta.4 * sdk: add-spot-market-index-52-constants (#1649) * sdk: release v2.122.0-beta.5 * program: add existing position fields to order records (#1614) * program: add quote entry amount to order records * fix cargo fmt and test * more reusable code * more reusable code * add another comment * fix math * account for pos flip * fix typo * missed commit * more fixes * align naming * fix typo * CHANGELOG * program: check limit price after applying buffer in trigger limit ord… (#1648) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * fix tests * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: fix cargo tests * program: check limit price when setting auction for limit order (#1650) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * program: check limit price when setting limit auction params * cargo fmt -- * fix CHANGELOG * tests: updates switchboardTxCus.ts * program: try to fix iteration for max order size (#1651) * Revert "program: try to fix iteration for max order size (#1651)" This reverts commit 3f0eab39ed23fa4a9c41cbab9af793c60b50a239. * disable debug logging in bankrun tests * v2.122.0 * sdk: release v2.123.0-beta.0 * sdk: constants-spot-market-index-53 (#1655) * sdk: release v2.123.0-beta.1 * sdk: idl for new existing position order action records * fix: protocol test prettier fix * make ci lut checks not shit * sdk: release v2.123.0-beta.2 * sdk: fix vamm l2 generator base swapped and add new top of book (#1626) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * use proper quoteAmount with baseSwap for top of book orders * clean up console.log * sdk: getVammL2Generator reduce loc (#1628) * sdk: getVammL2Generator-reduce-loc * add MAJORS_TOP_OF_BOOK_QUOTE_AMOUNTS * add marketindex check topOfBookAmounts * yarn lint/prettier * sdk: release v2.123.0-beta.3 * program: allow all limit orders to go through swift (#1661) * program: allow all limit orders to go through swift * add anchor test * CHANGELOG * sdk: add optional initSwiftAccount on existing account deposits (#1660) * sdk: release v2.123.0-beta.4 * program: add taker_speed_bump_override and amm_spread_adjustment * Revert "program: add taker_speed_bump_override and amm_spread_adjustment" This reverts commit 1e19b7e7a6c5cecebdbfb3a9e224a0d4471ba6d2. * program: tests-fee-adjustment-neg-100 (#1656) * program: tests-fee-adjustment-neg-100 * add HLM field to test * cargo fmt -- --------- Co-authored-by: Chris Heaney * program: simplify user can skip duration (#1668) * program: simplify user can skip duration * update context * CHANGELOG * fix test * fix pmm tests --------- Co-authored-by: Chris Heaney * program: add taker_speed_bump_override and amm_spread_adjustment (#1665) * program: add taker_speed_bump_override and amm_spread_adjustment * add admin client * cargo test * add impl for amm_spread_adjustment * ensure no overflows * CHANGELOG * cargo fmt -- * sdk types * prettify --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: update-amm-spread-and-availability-constraints (#1663) * program: update-amm-spread-and-availability-constraints * fix cargo tests * program: use saturating mul for amm spread adj * nour/indic-quotes-sender-v2 (#1667) * nour/indic-quotes-sender-v2 * prettify * pass margin category into calculateEntriesEffectOnFreeCollateral (#1669) * fix cargo test * tests: fix oracle guardrail test * sdk: update idl * yarn prettify:fix * tests: fix a few more place and make tests * prettify fix * whitespace readme change * sdk: release v2.123.0-beta.5 * v2.123.0 * sdk: release v2.124.0-beta.0 * v2.123.0-1 * sdk: calculateVolSpreadBN-sync (#1671) * sdk: release v2.124.0-beta.1 * sdk: calculate-spread-bn-add-amm-spread-adjustment (#1672) * sdk: calculate-spread-bn-add-amm-spread-adjustment * corect sign * add math max 1 * prettify * sdk: release v2.124.0-beta.2 * sdk: correct calculateVolSpreadBN reversion * sdk: release v2.124.0-beta.3 * sdk: add getTriggerAuctionStartPrice (#1654) * sdk: add getTriggerAuctionStartPrice * updates * precisions * remove startBuffer param --------- Co-authored-by: Chris Heaney * sdk: release v2.124.0-beta.4 * feat: customized cadence account loader (#1666) * feat: customized cadence account loader bby * feat: method to read account cadence on custom cadence account loader * feat: PR feedback on customized loader cleaup code and better naming * fix: lint and prettify * feat: more efficient rpc polling on custom polling intervals * feat: custom cadence acct loader override load * chore: prettify * sdk: release v2.124.0-beta.5 * sdk: sync-user-trade-tier-calcs (#1673) * sdk: sync-user-trade-tier-calcs * prettify --------- Co-authored-by: Nick Caradonna * sdk: release v2.124.0-beta.6 * sdk: add new admin client fn * Revert "sdk: add new admin client fn" This reverts commit c7a4f0b174858048bd379f2f2bb0e63595949921. * sdk: release v2.124.0-beta.7 * refactor(ui): add callback logic, fix polling frequency update * sdk: release v2.124.0-beta.8 * program: less order param sanitization for long tail perps (#1680) * program: allow-auction-start-buffer-on-tail-mkt * fix test * cargo fmt -- * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * Wphan/custom coder (#1682) * sdk: allow custom coder * remove unused accounts coder * linter * move customCoder into sdk, lint * update test helpers * update testhelpers.ts * sdk: release v2.124.0-beta.9 * update sdk exports * sdk: release v2.124.0-beta.10 * sdk: safer-calculate-spread-reserve-math (#1681) * sdk: release v2.124.0-beta.11 * update getMaxLeverageForPerp to use usdc logic (#1678) * sdk: release v2.124.0-beta.12 * program: override for oracle delay (#1679) * programy: override for oracle delay * update impl * switch to i8 * CHANGELOG * program: programmatic rebalance between protocol owned if holdings (#1653) * program: if swap * program: add initial config * add update * more * moar * moar * moar * program: update how swap epoch works * add test * add an invariant * cargo fmt -- * add transfer to rev pool * add mint validation * cargo fmt -- * track in amount between tranfsers * add to ci tests * separate key * program: always transfer max amount to rev pool * CHANGELOG * sdk: release v2.124.0-beta.13 * sdk: improve-aclient-accounts-logic (#1684) * sdk: release v2.124.0-beta.14 * program: improve-amm-spread-validates (#1685) * program: let hot wallet update amm jit intensity * sdk: hot wallet can update amm jit intensity * program: hot wallet can update curve intensity * program: fix build * sdk: update idl * sdk: release v2.124.0-beta.15 * v2.124.0 * sdk: release v2.125.0-beta.0 * program: three-point-std-estimator (#1686) * program: three-point-std-estimator * update tests and add sdk * update changelog * sdk: add-updatePerpMarketOracleSlotDelayOverride (#1691) * sdk: release v2.125.0-beta.1 * program: add-amm-inventory-spread-adjustment-param (#1690) * program: add-amm-inventory-spread-adjustment-param * cargo fmt -- * update sdk * prettier * fix syntax { --------- Co-authored-by: Chris Heaney * program: max-apr-rev-settle-by-spot-market (#1692) * program: max-apr-rev-settle-by-spot-market * update max * default to u128 to avoid casts * changelog * sdk: release v2.125.0-beta.2 * program: better account for imf in calculate_max_perp_order_size (#1693) * program: better account for imf in calculate_max_perp_order_size * CHANGELOG * v2.125.0 * sdk: release v2.126.0-beta.0 * sdk: only count taker fee in calculateEntriesEffectOnFreeCollateral for maintenance (#1694) * sdk: release v2.126.0-beta.1 * Separate getAddInsuranceFundStakeIxs (#1695) * sdk: release v2.126.0-beta.2 * idl: amm-inv-adj-latest-idl (#1697) * sdk: release v2.126.0-beta.3 * sdk: spot-market-index-54 constants (#1696) * sdk: release v2.126.0-beta.4 * sdk: update spot market index 54 pythlazer id * sdk: release v2.126.0-beta.5 * Update spotMarkets.ts * sdk: release v2.126.0-beta.6 * prettify --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: Nour Alharithi Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: lil perp Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> * slot staleness checks (#1705) * slot staleness checks * update aum ix to use constituent oracles * Nour/derivative constituent testing (#1708) * slot staleness checks * update aum ix to use constituent oracles * constituent test works when adjusting derivative index * constituent depeg kill switch works * works with multiple derivatives on the same parent * remove incorrect usage of nav * fix adminClient and tests * Nour/fee grid search testing (#1714) * grid search * grid search swap test * Nour/address comments (#1715) * low hanging fruit comments * remove pda checks and store lp pool on zero copy accounts * parameterize depeg threshold * make description in lp pool event * update idl for event change * add swap fee unit tests (#1713) * add swap fee unit tests * remove linear inventory fee component * Nour/settle accounting (#1723) * fixing the main settle test and settle function * all current tests pass * update msg occurrences * dont update lp quote owed unless collateralized * Nour/settle testing (#1725) * refactor settle pnl to modularize and add tests * more cargo tests * prettify * Nour/address more comments (#1726) * use oracle staleness threshold for staleness * add spot market vault invariant * refactor update_aum, add unit tests (#1727) * refactor update_aum, add unit tests * add constituent target base tests * update doc * Nour/parameterize dlp (#1731) * add validates and test for withdraw limit * settlement max * update idl * merge conflicts * fixes * update idl * bug fixes * mostly sdk fixes * bug fixes * bug fix and deploy script * program: new amm oracle (#1738) * zero unused amm fields * cargo fmt * bare bones ix * minimal anchor mm oracle impl * update test file * only do admin validate when not anchor test * updates * generalize native entry * fix weird function name chop off * make it compile for --feature cpi (#1748) Co-authored-by: jordy25519 * more efficeint clock and state bit flags check * vamm uses mm oracle (#1747) * add offset * working tests * refactor to use MM oracle as its own type * remove weird preface * sdk updates * bankrun tests all pass * fix test * changes and fixes * widen confidence if mm oracle too diff * sdk side for confidence adjust * changelog * fix lint * fix cargo tests * address comments * add conf check * remove anchor ix and cache oracle confidence * only state admin can reenable mm oracle kill switch * cargo fmt --------- Co-authored-by: jordy25519 * fix tests (#1764) * Nour/move ixs around (#1766) * move around ixs * remove message * add devnet oracle crank wallet * refactored mm oracle * sdk changes + cargo fmt * fix tests * validate price bands with fill fix * normalize fill within price bands * add sdk warning * updated type * undefined guard so anchor tests pass * accept vec for update amm and view amm * adjust test to work with new price bands * Revert "adjust test to work with new price bands" This reverts commit ee40ac8799fa2f6222ea7d0e9b3e07014346a699. * remove price bands logic * add zero ix for mm oracle for reset * add new drift client ix grouping * v1 safety improvements * isolate funding from MM oracle * add cargo tests for amm availability * change oracle validity log bool to enum * address comment * make validate fill direction agnostic * fix liquidate borrow for perp pnl test * fix tests and address comments * add RevenueShare and RevenueShareEscrow accounts an init ixs * fix multiple array zc account, and handling different message types in place_signed_msg_taker_order * decoding error * commit constituent map to barrel file * add lp fields to perp market account * recording orders in RevenueShareEscrow workin * rearrange perp market struct for lp fields * cancel and fill orders * idl * fix sdk build * fix math * bug fix for notional position tracking * update RevenueShareOrder bitflags, store builder_idx instead of pubkey * view function * merge RevenueShareOrders on add * fee view functions * max aum + whitelist check and removing get_mint_redeem_fee for now * add wsol support for add liquidity * fix sdk and typing bugs * update lp pool params ix * admin override cache and disable settle functions * remove builder accounts from cancel ixs, wip settle impl * dont fail settlpnl if no builder users provided * devnet swap working * finish settle, rename RevenueShare->Builder, RevenueShareEscrow->BuilderEscrow * add more bankrun tests, clean up * clean up, fix tests * why test fail * dlp taker discovered bug fixes and sdk changes * add subaccountid to BuilderOrder * reduce diff * refactor last settle ts to last settle slot * add referrals * add test can fill settle user with no builderescrow * add referral builder feature flag and referral migration method * fix cargo tests, try fix bankrun test timing issue * Nour/settle pnl fix (#1817) * settle perp to lp pool bug fixes * update bankrun test to not use admin fee pool deposit * fix tests using update spot market balances too * add log msgs for withdraw and fix casting bug * add SignedMsgOrderParamsMessageV2 * check in for z (#1823) * feat: option for custom oracle ws subscriber * fix: pass custom oracle ws sub option in dc constructor * sdk: add spot-market-index-57 to constants (#1815) * sdk: release v2.134.0-beta.2 * lazer oracle migration (#1813) * lazer oracle migration * spot markets too * sdk: release v2.134.0-beta.3 * sdk: release v2.134.0-beta.4 * program: settle pnl invariants (#1812) * program: settle pnl invariants * add test * fix lint * lints * add msg * CHANGELOG * cargo fmt -- * program: add_update_perp_pnl_pool (#1810) * program: add_update_perp_pnl_pool * test * CHANGELOG * sdk: release v2.134.0-beta.5 * program: update-mark-twap-integer-bias (#1783) * program: update-mark-twap-integer-bias * changelog update * program: update-fee-tier-determine-fix5 (#1800) * program: update-fee-tier-determine-fix5 * update changelog * program: update-mark-twap-crank-use-5min-basis (#1769) * program: update-mark-twap-crank-use-5min-basis * changelog * program: update-min-margin-const-limit (#1802) * program: update-min-margin-const-limit * add CHANGELOG.md * sdk: release v2.134.0-beta.6 * program: rm-burn-lp-shares-invariant (#1816) * program: rm-burn-lp-shares-invariant * update changelog * fix test and cargo fmt * fix anchor tests * yarn prettify:fix * reenable settle_pnl mode test * v2.134.0 * sdk: release v2.135.0-beta.0 * Merge pull request #1820 from drift-labs/chester/fix-zod * sdk: release v2.135.0-beta.1 * mm oracle sdk change (#1806) * mm oracle sdk change * better conditional typing * DLOB bug fix * updated idl * rm getAmmBidAskPrice * sdk: release v2.135.0-beta.2 * sdk: fix isHighLeverageMode * sdk: release v2.135.0-beta.3 * refactor(sdk): add update delegate ix method, ovrride authority for settle multiple pnl (#1822) * check in for z * more logging changes * mm oracle sdk additions (#1824) * strict typing for more MM oracle contact points * add comments to auction.ts * prettify * sdk: release v2.135.0-beta.4 * init constituent bug fix and type change * add in invariant to be within 1 bp of balance before after * add strict typing for getPrice and new auction trigger function (#1826) * add strict typing for getPrice and new auction trigger function * refactor getTriggerAuctionStartAndExecutionPrice * sdk: release v2.135.0-beta.5 * update tests and enforce atomic settles for withdraw * add failing withdraw test * withdraw fix * bring diff in validate back to 1 * make lp pool test fail * better failed test * only check after < before, do to spot precision limits * add balance check to be < 1 cent --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: LukasDeco Co-authored-by: lil perp Co-authored-by: wphan Co-authored-by: Chester Sim * zero pad swift messages to make backwards compatible * PR feedback * add price for lp validates (#1833) * update tests/placeAndMakeSignedMsgBankrun.ts to handle client side errors * add missing token account reloads and syncs * add disabled lp pool swaps by default * refactor account logic for borrows * remove double fee count, update tests to check filled position and quote amounts fda * more extensive aum logging * rename Builder -> RevenueShare * add test check accumulated builder/ref fees * fix settle multiple pnl accounts, test ref rewards in multiple markets * express builder fees in tenth of bps * update referral migration params * PR feedback * add builder code feature gate * fix tests * add referral fields * run all tests * kickoff build * disable extra instructions, fix builder code feature flag selection * update driftclient * Revert recent builder codes chain and merge (#1848) * Revert recent builder codes chain and merge * update driftclient * disable extra instructions, fix builder code feature flag selection * clean up account inclusion rules in settle pnl for builder codes * cargo fmt * PR comments, featureflag clean up * move authority check into get_revenue_share_escrow_account * clean up referrer eligibility check, support placeAndTake/Make referral fees * skip builder fee accrual on full escrow account, dont throw * add feature flag sdk fn * program: builder codes dont throw tx on missing acc * placeAndMake respect builder codes * ensure update userstats referrerstatus on migration * hold back OrderActionRecord idl changes * update CHANGELOG.md --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> Co-authored-by: moosecat Co-authored-by: Chris Heaney Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> --- CHANGELOG.md | 2 + programs/drift/src/controller/liquidation.rs | 5 + programs/drift/src/controller/mod.rs | 1 + programs/drift/src/controller/orders.rs | 208 ++- .../src/controller/orders/amm_jit_tests.rs | 26 + .../src/controller/orders/amm_lp_jit_tests.rs | 20 + .../drift/src/controller/orders/fuel_tests.rs | 2 + programs/drift/src/controller/orders/tests.rs | 82 + .../drift/src/controller/revenue_share.rs | 197 ++ programs/drift/src/error.rs | 16 + programs/drift/src/instructions/admin.rs | 44 + programs/drift/src/instructions/keeper.rs | 221 ++- .../src/instructions/optional_accounts.rs | 42 +- programs/drift/src/instructions/user.rs | 287 +++ programs/drift/src/lib.rs | 49 + programs/drift/src/math/fees.rs | 26 + programs/drift/src/math/fees/tests.rs | 21 + programs/drift/src/state/events.rs | 28 +- programs/drift/src/state/mod.rs | 2 + programs/drift/src/state/order_params.rs | 4 + programs/drift/src/state/revenue_share.rs | 572 ++++++ programs/drift/src/state/revenue_share_map.rs | 209 +++ programs/drift/src/state/state.rs | 10 + programs/drift/src/state/user.rs | 18 + .../drift/src/validation/sig_verification.rs | 6 + .../src/validation/sig_verification/tests.rs | 63 + sdk/src/addresses/pda.ts | 26 + sdk/src/adminClient.ts | 55 + sdk/src/driftClient.ts | 556 +++++- sdk/src/idl/drift.json | 571 ++++++ sdk/src/index.ts | 1 + sdk/src/math/builder.ts | 20 + sdk/src/math/orders.ts | 5 + sdk/src/math/state.ts | 8 + sdk/src/memcmp.ts | 11 + sdk/src/swift/swiftOrderSubscriber.ts | 15 +- sdk/src/types.ts | 54 + sdk/src/userMap/revenueShareEscrowMap.ts | 306 ++++ test-scripts/run-anchor-tests.sh | 1 + test-scripts/run-til-failure.sh | 13 + test-scripts/single-anchor-test.sh | 3 +- tests/builderCodes.ts | 1612 +++++++++++++++++ tests/placeAndMakeSignedMsgBankrun.ts | 2 +- tests/subaccounts.ts | 1 + tests/switchboardTxCus.ts | 2 +- tests/testHelpers.ts | 13 +- 46 files changed, 5371 insertions(+), 65 deletions(-) create mode 100644 programs/drift/src/controller/revenue_share.rs create mode 100644 programs/drift/src/state/revenue_share.rs create mode 100644 programs/drift/src/state/revenue_share_map.rs create mode 100644 sdk/src/math/builder.ts create mode 100644 sdk/src/userMap/revenueShareEscrowMap.ts create mode 100644 test-scripts/run-til-failure.sh create mode 100644 tests/builderCodes.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e6a29b629..e6391cb4f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: builder codes ([#1805](https://github.com/drift-labs/protocol-v2/pull/1805)) + ### Fixes ### Breaking diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 1fd2f548dc..9503f24966 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -675,6 +675,8 @@ pub fn liquidate_perp( maker_existing_quote_entry_amount: maker_existing_quote_entry_amount, maker_existing_base_asset_amount: maker_existing_base_asset_amount, trigger_price: None, + builder_idx: None, + builder_fee: None, }; emit!(fill_record); @@ -1038,6 +1040,7 @@ pub fn liquidate_perp_with_fill( clock, order_params, PlaceOrderOptions::default().explanation(OrderActionExplanation::Liquidation), + &mut None, )?; drop(user); @@ -1058,6 +1061,8 @@ pub fn liquidate_perp_with_fill( None, clock, FillMode::Liquidation, + &mut None, + false, )?; let mut user = load_mut!(user_loader)?; diff --git a/programs/drift/src/controller/mod.rs b/programs/drift/src/controller/mod.rs index 1565eb1174..5ebdb9772a 100644 --- a/programs/drift/src/controller/mod.rs +++ b/programs/drift/src/controller/mod.rs @@ -7,6 +7,7 @@ pub mod pda; pub mod pnl; pub mod position; pub mod repeg; +pub mod revenue_share; pub mod spot_balance; pub mod spot_position; pub mod token; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index e623000af3..57431d991c 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -5,6 +5,9 @@ use std::u64; use crate::msg; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; +use crate::state::revenue_share::{ + RevenueShareEscrowZeroCopyMut, RevenueShareOrder, RevenueShareOrderBitFlag, +}; use anchor_lang::prelude::*; use crate::controller::funding::settle_funding_payment; @@ -103,6 +106,7 @@ pub fn place_perp_order( clock: &Clock, mut params: OrderParams, mut options: PlaceOrderOptions, + rev_share_order: &mut Option<&mut RevenueShareOrder>, ) -> DriftResult { let now = clock.unix_timestamp; let slot: u64 = clock.slot; @@ -298,6 +302,10 @@ pub fn place_perp_order( OrderBitFlag::NewTriggerReduceOnly, ); + if rev_share_order.is_some() { + bit_flags = set_order_bit_flag(bit_flags, true, OrderBitFlag::HasBuilder); + } + let new_order = Order { status: OrderStatus::Open, order_type: params.order_type, @@ -438,6 +446,8 @@ pub fn place_perp_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -718,6 +728,8 @@ pub fn cancel_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; } @@ -844,6 +856,7 @@ pub fn modify_order( clock, order_params, PlaceOrderOptions::default(), + &mut None, )?; } else { place_spot_order( @@ -968,6 +981,8 @@ pub fn fill_perp_order( jit_maker_order_id: Option, clock: &Clock, fill_mode: FillMode, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64)> { let now = clock.unix_timestamp; let slot = clock.slot; @@ -1304,6 +1319,8 @@ pub fn fill_perp_order( amm_availability, fill_mode, oracle_stale_for_margin, + rev_share_escrow, + builder_referral_feature_enabled, )?; if base_asset_amount != 0 { @@ -1714,6 +1731,37 @@ fn get_referrer_info( Ok(Some((referrer_authority_key, referrer_user_key))) } +#[inline(always)] +fn get_builder_escrow_info( + escrow_opt: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + sub_account_id: u16, + order_id: u32, + market_index: u16, + builder_referral_feature_enabled: bool, +) -> (Option, Option, Option, Option) { + if let Some(escrow) = escrow_opt { + let builder_order_idx = escrow.find_order_index(sub_account_id, order_id); + let referrer_builder_order_idx = if builder_referral_feature_enabled { + escrow.find_or_create_referral_index(market_index) + } else { + None + }; + + let builder_order = builder_order_idx.and_then(|idx| escrow.get_order(idx).ok()); + let builder_order_fee_bps = builder_order.map(|order| order.fee_tenth_bps); + let builder_idx = builder_order.map(|order| order.builder_idx); + + ( + builder_order_idx, + referrer_builder_order_idx, + builder_order_fee_bps, + builder_idx, + ) + } else { + (None, None, None, None) + } +} + fn fulfill_perp_order( user: &mut User, user_order_index: usize, @@ -1738,6 +1786,8 @@ fn fulfill_perp_order( amm_availability: AMMAvailability, fill_mode: FillMode, oracle_stale_for_margin: bool, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64)> { let market_index = user.orders[user_order_index].market_index; @@ -1836,6 +1886,8 @@ fn fulfill_perp_order( None, *maker_price, fill_mode.is_liquidation(), + rev_share_escrow, + builder_referral_feature_enabled, )?; (fill_base_asset_amount, fill_quote_asset_amount) @@ -1880,6 +1932,8 @@ fn fulfill_perp_order( fee_structure, oracle_map, fill_mode.is_liquidation(), + rev_share_escrow, + builder_referral_feature_enabled, )?; if maker_fill_base_asset_amount != 0 { @@ -2123,6 +2177,8 @@ pub fn fulfill_perp_order_with_amm( override_base_asset_amount: Option, override_fill_price: Option, is_liquidation: bool, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64)> { let position_index = get_position_index(&user.perp_positions, market.market_index)?; let existing_base_asset_amount = user.perp_positions[position_index].base_asset_amount; @@ -2180,8 +2236,13 @@ pub fn fulfill_perp_order_with_amm( return Ok((0, 0)); } - let (order_post_only, order_slot, order_direction) = - get_struct_values!(user.orders[order_index], post_only, slot, direction); + let (order_post_only, order_slot, order_direction, order_id) = get_struct_values!( + user.orders[order_index], + post_only, + slot, + direction, + order_id + ); validation::perp_market::validate_amm_account_for_fill(&market.amm, order_direction)?; @@ -2223,10 +2284,24 @@ pub fn fulfill_perp_order_with_amm( )?; } - let reward_referrer = can_reward_user_with_perp_pnl(referrer, market.market_index); + let reward_referrer = can_reward_user_with_referral_reward( + referrer, + market.market_index, + rev_share_escrow, + builder_referral_feature_enabled, + ); let reward_filler = can_reward_user_with_perp_pnl(filler, market.market_index) || can_reward_user_with_perp_pnl(maker, market.market_index); + let (builder_order_idx, referrer_builder_order_idx, builder_order_fee_bps, builder_idx) = + get_builder_escrow_info( + rev_share_escrow, + user.sub_account_id, + order_id, + market.market_index, + builder_referral_feature_enabled, + ); + let FillFees { user_fee, fee_to_market, @@ -2235,6 +2310,7 @@ pub fn fulfill_perp_order_with_amm( referrer_reward, fee_to_market_for_lp: _fee_to_market_for_lp, maker_rebate, + builder_fee: builder_fee_option, } = fees::calculate_fee_for_fulfillment_with_amm( user_stats, quote_asset_amount, @@ -2248,8 +2324,20 @@ pub fn fulfill_perp_order_with_amm( order_post_only, market.fee_adjustment, user.is_high_leverage_mode(MarginRequirementType::Initial), + builder_order_fee_bps, )?; + let builder_fee = builder_fee_option.unwrap_or(0); + + if builder_fee != 0 { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_mut()) { + let mut order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(builder_fee)?; + } else { + msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); + } + } + // Increment the protocol's total fee variables market.amm.total_fee = market.amm.total_fee.safe_add(fee_to_market.cast()?)?; market.amm.total_exchange_fee = market.amm.total_exchange_fee.safe_add(user_fee.cast()?)?; @@ -2271,7 +2359,12 @@ pub fn fulfill_perp_order_with_amm( user_stats.increment_total_rebate(maker_rebate)?; user_stats.increment_total_referee_discount(referee_discount)?; - if let (Some(referrer), Some(referrer_stats)) = (referrer.as_mut(), referrer_stats.as_mut()) { + if let (Some(idx), Some(escrow)) = (referrer_builder_order_idx, rev_share_escrow.as_mut()) { + let order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(referrer_reward)?; + } else if let (Some(referrer), Some(referrer_stats)) = + (referrer.as_mut(), referrer_stats.as_mut()) + { if let Ok(referrer_position) = referrer.force_get_perp_position_mut(market.market_index) { if referrer_reward > 0 { update_quote_asset_amount(referrer_position, market, referrer_reward.cast()?)?; @@ -2282,11 +2375,11 @@ pub fn fulfill_perp_order_with_amm( let position_index = get_position_index(&user.perp_positions, market.market_index)?; - if user_fee != 0 { + if user_fee != 0 || builder_fee != 0 { controller::position::update_quote_asset_and_break_even_amount( &mut user.perp_positions[position_index], market, - -user_fee.cast()?, + -(user_fee.safe_add(builder_fee)?).cast()?, )?; } @@ -2326,11 +2419,18 @@ pub fn fulfill_perp_order_with_amm( )?; } - update_order_after_fill( + let is_filled = update_order_after_fill( &mut user.orders[order_index], base_asset_amount, quote_asset_amount, )?; + if is_filled { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_mut()) { + let _ = escrow + .get_order_mut(idx) + .map(|order| order.add_bit_flag(RevenueShareOrderBitFlag::Completed)); + } + } decrease_open_bids_and_asks( &mut user.perp_positions[position_index], @@ -2391,7 +2491,7 @@ pub fn fulfill_perp_order_with_amm( Some(filler_reward), Some(base_asset_amount), Some(quote_asset_amount), - Some(user_fee), + Some(user_fee.safe_add(builder_fee)?), if maker_rebate != 0 { Some(maker_rebate) } else { @@ -2411,6 +2511,8 @@ pub fn fulfill_perp_order_with_amm( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, None, + builder_idx, + builder_fee_option, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2479,6 +2581,8 @@ pub fn fulfill_perp_order_with_match( fee_structure: &FeeStructure, oracle_map: &mut OracleMap, is_liquidation: bool, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], @@ -2591,6 +2695,8 @@ pub fn fulfill_perp_order_with_match( Some(jit_base_asset_amount), Some(maker_price), // match the makers price is_liquidation, + rev_share_escrow, + builder_referral_feature_enabled, )?; total_base_asset_amount = base_asset_amount_filled_by_amm; @@ -2683,9 +2789,23 @@ pub fn fulfill_perp_order_with_match( taker_stats.update_taker_volume_30d(market.fuel_boost_taker, quote_asset_amount, now)?; - let reward_referrer = can_reward_user_with_perp_pnl(referrer, market.market_index); + let reward_referrer = can_reward_user_with_referral_reward( + referrer, + market.market_index, + rev_share_escrow, + builder_referral_feature_enabled, + ); let reward_filler = can_reward_user_with_perp_pnl(filler, market.market_index); + let (builder_order_idx, referrer_builder_order_idx, builder_order_fee_bps, builder_idx) = + get_builder_escrow_info( + rev_share_escrow, + taker.sub_account_id, + taker.orders[taker_order_index].order_id, + market.market_index, + builder_referral_feature_enabled, + ); + let filler_multiplier = if reward_filler { calculate_filler_multiplier_for_matched_orders(maker_price, maker_direction, oracle_price)? } else { @@ -2699,6 +2819,7 @@ pub fn fulfill_perp_order_with_match( filler_reward, referrer_reward, referee_discount, + builder_fee: builder_fee_option, .. } = fees::calculate_fee_for_fulfillment_with_match( taker_stats, @@ -2713,7 +2834,18 @@ pub fn fulfill_perp_order_with_match( &MarketType::Perp, market.fee_adjustment, taker.is_high_leverage_mode(MarginRequirementType::Initial), + builder_order_fee_bps, )?; + let builder_fee = builder_fee_option.unwrap_or(0); + + if builder_fee != 0 { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_deref_mut()) { + let mut order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(builder_fee)?; + } else { + msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); + } + } // Increment the markets house's total fee variables market.amm.total_fee = market.amm.total_fee.safe_add(fee_to_market.cast()?)?; @@ -2733,7 +2865,7 @@ pub fn fulfill_perp_order_with_match( controller::position::update_quote_asset_and_break_even_amount( &mut taker.perp_positions[taker_position_index], market, - -taker_fee.cast()?, + -(taker_fee.safe_add(builder_fee)?).cast()?, )?; taker_stats.increment_total_fees(taker_fee)?; @@ -2772,7 +2904,13 @@ pub fn fulfill_perp_order_with_match( filler.update_last_active_slot(slot); } - if let (Some(referrer), Some(referrer_stats)) = (referrer.as_mut(), referrer_stats.as_mut()) { + if let (Some(idx), Some(escrow)) = (referrer_builder_order_idx, rev_share_escrow.as_deref_mut()) + { + let mut order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(referrer_reward)?; + } else if let (Some(referrer), Some(referrer_stats)) = + (referrer.as_mut(), referrer_stats.as_mut()) + { if let Ok(referrer_position) = referrer.force_get_perp_position_mut(market.market_index) { if referrer_reward > 0 { update_quote_asset_amount(referrer_position, market, referrer_reward.cast()?)?; @@ -2781,12 +2919,20 @@ pub fn fulfill_perp_order_with_match( } } - update_order_after_fill( + let is_filled = update_order_after_fill( &mut taker.orders[taker_order_index], base_asset_amount_fulfilled_by_maker, quote_asset_amount, )?; + if is_filled { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_deref_mut()) { + escrow + .get_order_mut(idx)? + .add_bit_flag(RevenueShareOrderBitFlag::Completed); + } + } + decrease_open_bids_and_asks( &mut taker.perp_positions[taker_position_index], &taker.orders[taker_order_index].direction, @@ -2841,7 +2987,7 @@ pub fn fulfill_perp_order_with_match( Some(filler_reward), Some(base_asset_amount_fulfilled_by_maker), Some(quote_asset_amount), - Some(taker_fee), + Some(taker_fee.safe_add(builder_fee)?), Some(maker_rebate), Some(referrer_reward), None, @@ -2857,6 +3003,8 @@ pub fn fulfill_perp_order_with_match( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, None, + builder_idx, + builder_fee_option, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2885,18 +3033,19 @@ pub fn update_order_after_fill( order: &mut Order, base_asset_amount: u64, quote_asset_amount: u64, -) -> DriftResult { +) -> DriftResult { order.base_asset_amount_filled = order.base_asset_amount_filled.safe_add(base_asset_amount)?; order.quote_asset_amount_filled = order .quote_asset_amount_filled .safe_add(quote_asset_amount)?; - if order.get_base_asset_amount_unfilled(None)? == 0 { + let is_filled = order.get_base_asset_amount_unfilled(None)? == 0; + if is_filled { order.status = OrderStatus::Filled; } - Ok(()) + Ok(is_filled) } #[allow(clippy::type_complexity)] @@ -3092,6 +3241,8 @@ pub fn trigger_order( None, None, Some(trigger_price), + None, + None, )?; emit!(order_action_record); @@ -3299,6 +3450,22 @@ pub fn can_reward_user_with_perp_pnl(user: &mut Option<&mut User>, market_index: } } +pub fn can_reward_user_with_referral_reward( + user: &mut Option<&mut User>, + market_index: u16, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, +) -> bool { + if builder_referral_feature_enabled { + if let Some(escrow) = rev_share_escrow { + return escrow.find_or_create_referral_index(market_index).is_some(); + } + false + } else { + can_reward_user_with_perp_pnl(user, market_index) + } +} + pub fn pay_keeper_flat_reward_for_perps( user: &mut User, filler: Option<&mut User>, @@ -3655,6 +3822,8 @@ pub fn place_spot_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -4730,6 +4899,7 @@ pub fn fulfill_spot_order_with_match( &MarketType::Spot, base_market.fee_adjustment, false, + None, )?; // Update taker state @@ -4897,6 +5067,8 @@ pub fn fulfill_spot_order_with_match( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5171,6 +5343,8 @@ pub fn fulfill_spot_order_with_external_market( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5375,6 +5549,8 @@ pub fn trigger_spot_order( None, None, Some(oracle_price.unsigned_abs()), + None, + None, )?; emit!(order_action_record); diff --git a/programs/drift/src/controller/orders/amm_jit_tests.rs b/programs/drift/src/controller/orders/amm_jit_tests.rs index 6d8ff77eef..c14fd58b62 100644 --- a/programs/drift/src/controller/orders/amm_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_jit_tests.rs @@ -300,6 +300,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -490,6 +492,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -688,6 +692,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -885,6 +891,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1084,6 +1092,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1292,6 +1302,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1498,6 +1510,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::Unavailable, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1698,6 +1712,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1886,6 +1902,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2086,6 +2104,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2337,6 +2357,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2621,6 +2643,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2850,6 +2874,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs index 550574a1c7..ae6666328e 100644 --- a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs @@ -504,6 +504,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -706,6 +708,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -908,6 +912,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1119,6 +1125,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1322,6 +1330,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1513,6 +1523,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1716,6 +1728,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1967,6 +1981,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2251,6 +2267,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2483,6 +2501,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/fuel_tests.rs b/programs/drift/src/controller/orders/fuel_tests.rs index f29b54addd..b4021e6b7b 100644 --- a/programs/drift/src/controller/orders/fuel_tests.rs +++ b/programs/drift/src/controller/orders/fuel_tests.rs @@ -245,6 +245,8 @@ pub mod fuel_scoring { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index b5e681f090..c8428f01d0 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -264,6 +264,8 @@ pub mod fill_order_protected_maker { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -373,6 +375,8 @@ pub mod fill_order_protected_maker { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -486,6 +490,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -609,6 +615,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -732,6 +740,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -855,6 +865,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -977,6 +989,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1066,6 +1080,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1156,6 +1172,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1246,6 +1264,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1336,6 +1356,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1446,6 +1468,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1561,6 +1585,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1681,6 +1707,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1802,6 +1830,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1947,6 +1977,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -2067,6 +2099,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -2197,6 +2231,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2348,6 +2384,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2497,6 +2535,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2647,6 +2687,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2778,6 +2820,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -2908,6 +2952,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -3296,6 +3342,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -3540,6 +3588,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -3730,6 +3780,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -3936,6 +3988,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4102,6 +4156,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4300,6 +4356,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ); assert!(result.is_ok()); @@ -4487,6 +4545,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ); assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); @@ -4627,6 +4687,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::Immediate, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4794,6 +4856,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4970,6 +5034,8 @@ pub mod fulfill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -4995,6 +5061,8 @@ pub mod fulfill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -5144,6 +5212,8 @@ pub mod fulfill_order { // slot, // false, // true, + // &mut None, + // false, // ) // .unwrap(); // @@ -5377,6 +5447,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -5621,6 +5693,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -5878,6 +5952,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -6080,6 +6156,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -6209,6 +6287,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -6371,6 +6451,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ); assert_eq!(err, Err(ErrorCode::MaxOpenInterest)); diff --git a/programs/drift/src/controller/revenue_share.rs b/programs/drift/src/controller/revenue_share.rs new file mode 100644 index 0000000000..61681cd7a4 --- /dev/null +++ b/programs/drift/src/controller/revenue_share.rs @@ -0,0 +1,197 @@ +use anchor_lang::prelude::*; + +use crate::controller::spot_balance; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; +use crate::state::events::{emit_stack, RevenueShareSettleRecord}; +use crate::state::perp_market_map::PerpMarketMap; +use crate::state::revenue_share::{RevenueShareEscrowZeroCopyMut, RevenueShareOrder}; +use crate::state::revenue_share_map::RevenueShareMap; +use crate::state::spot_market::SpotBalance; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::traits::Size; +use crate::state::user::MarketType; + +/// Runs through the user's RevenueShareEscrow account and sweeps any accrued fees to the corresponding +/// builders and referrer. +pub fn sweep_completed_revenue_share_for_market<'a>( + market_index: u16, + revenue_share_escrow: &mut RevenueShareEscrowZeroCopyMut, + perp_market_map: &PerpMarketMap<'a>, + spot_market_map: &SpotMarketMap<'a>, + revenue_share_map: &RevenueShareMap<'a>, + now_ts: i64, + builder_codes_feature_enabled: bool, + builder_referral_feature_enabled: bool, +) -> crate::error::DriftResult<()> { + let perp_market = &mut perp_market_map.get_ref_mut(&market_index)?; + let quote_spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; + + spot_balance::update_spot_market_cumulative_interest(quote_spot_market, None, now_ts)?; + + let orders_len = revenue_share_escrow.orders_len(); + for i in 0..orders_len { + let ( + is_completed, + is_referral_order, + order_market_type, + order_market_index, + fees_accrued, + builder_idx, + ) = { + let ord_ro = match revenue_share_escrow.get_order(i) { + Ok(o) => o, + Err(_) => { + continue; + } + }; + ( + ord_ro.is_completed(), + ord_ro.is_referral_order(), + ord_ro.market_type, + ord_ro.market_index, + ord_ro.fees_accrued, + ord_ro.builder_idx, + ) + }; + + if is_referral_order { + if fees_accrued == 0 + || !(order_market_type == MarketType::Perp && order_market_index == market_index) + { + continue; + } + } else if !(is_completed + && order_market_type == MarketType::Perp + && order_market_index == market_index + && fees_accrued > 0) + { + continue; + } + + let pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.scaled_balance, + quote_spot_market, + perp_market.pnl_pool.balance_type(), + )?; + + // TODO: should we add buffer on pnl pool? + if pnl_pool_token_amount < fees_accrued as u128 { + msg!( + "market {} PNL pool has insufficient balance to sweep fees for builder. pnl_pool_token_amount: {}, fees_accrued: {}", + market_index, + pnl_pool_token_amount, + fees_accrued + ); + break; + } + + if is_referral_order { + if builder_referral_feature_enabled { + let referrer_authority = + if let Some(referrer_authority) = revenue_share_escrow.get_referrer() { + referrer_authority + } else { + continue; + }; + + let referrer_user = revenue_share_map.get_user_ref_mut(&referrer_authority); + let referrer_rev_share = + revenue_share_map.get_revenue_share_account_mut(&referrer_authority); + + if referrer_user.is_ok() && referrer_rev_share.is_ok() { + let mut referrer_user = referrer_user.unwrap(); + let mut referrer_rev_share = referrer_rev_share.unwrap(); + + spot_balance::transfer_spot_balances( + fees_accrued as i128, + quote_spot_market, + &mut perp_market.pnl_pool, + referrer_user.get_quote_spot_position_mut(), + )?; + + referrer_rev_share.total_referrer_rewards = referrer_rev_share + .total_referrer_rewards + .safe_add(fees_accrued as u64)?; + + emit_stack::<_, { RevenueShareSettleRecord::SIZE }>( + RevenueShareSettleRecord { + ts: now_ts, + builder: None, + referrer: Some(referrer_authority), + fee_settled: fees_accrued as u64, + market_index: order_market_index, + market_type: order_market_type, + builder_total_referrer_rewards: referrer_rev_share + .total_referrer_rewards, + builder_total_builder_rewards: referrer_rev_share.total_builder_rewards, + builder_sub_account_id: referrer_user.sub_account_id, + }, + )?; + + // zero out the order + if let Ok(builder_order) = revenue_share_escrow.get_order_mut(i) { + builder_order.fees_accrued = 0; + } + } + } + } else if builder_codes_feature_enabled { + let builder_authority = match revenue_share_escrow + .get_approved_builder_mut(builder_idx) + .map(|builder| builder.authority) + { + Ok(auth) => auth, + Err(_) => { + msg!("failed to get approved_builder from escrow account"); + continue; + } + }; + + let builder_user = revenue_share_map.get_user_ref_mut(&builder_authority); + let builder_rev_share = + revenue_share_map.get_revenue_share_account_mut(&builder_authority); + + if builder_user.is_ok() && builder_rev_share.is_ok() { + let mut builder_user = builder_user.unwrap(); + let mut builder_revenue_share = builder_rev_share.unwrap(); + + spot_balance::transfer_spot_balances( + fees_accrued as i128, + quote_spot_market, + &mut perp_market.pnl_pool, + builder_user.get_quote_spot_position_mut(), + )?; + + builder_revenue_share.total_builder_rewards = builder_revenue_share + .total_builder_rewards + .safe_add(fees_accrued as u64)?; + + emit_stack::<_, { RevenueShareSettleRecord::SIZE }>(RevenueShareSettleRecord { + ts: now_ts, + builder: Some(builder_authority), + referrer: None, + fee_settled: fees_accrued as u64, + market_index: order_market_index, + market_type: order_market_type, + builder_total_referrer_rewards: builder_revenue_share.total_referrer_rewards, + builder_total_builder_rewards: builder_revenue_share.total_builder_rewards, + builder_sub_account_id: builder_user.sub_account_id, + })?; + + // remove order + if let Ok(builder_order) = revenue_share_escrow.get_order_mut(i) { + *builder_order = RevenueShareOrder::default(); + } + } else { + msg!( + "Builder user or builder not found for builder authority: {}", + builder_authority + ); + } + } else { + msg!("Builder codes nor builder referral feature is not enabled"); + } + } + + Ok(()) +} diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 61dee9f5f8..d09e3bfd1c 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -639,6 +639,22 @@ pub enum ErrorCode { InvalidIfRebalanceConfig, #[msg("Invalid If Rebalance Swap")] InvalidIfRebalanceSwap, + #[msg("Invalid RevenueShare resize")] + InvalidRevenueShareResize, + #[msg("Builder has been revoked")] + BuilderRevoked, + #[msg("Builder fee is greater than max fee bps")] + InvalidBuilderFee, + #[msg("RevenueShareEscrow authority mismatch")] + RevenueShareEscrowAuthorityMismatch, + #[msg("RevenueShareEscrow has too many active orders")] + RevenueShareEscrowOrdersAccountFull, + #[msg("Invalid RevenueShareAccount")] + InvalidRevenueShareAccount, + #[msg("Cannot revoke builder with open orders")] + CannotRevokeBuilderWithOpenOrders, + #[msg("Unable to load builder account")] + UnableToLoadRevenueShareAccount, } #[macro_export] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 4707078481..94fe90f15f 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4960,6 +4960,50 @@ pub fn handle_update_delegate_user_gov_token_insurance_stake( Ok(()) } +pub fn handle_update_feature_bit_flags_builder_codes( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can enable feature bit flags" + )?; + + msg!("Setting 3rd bit to 1, enabling builder codes"); + state.feature_bit_flags = state.feature_bit_flags | (FeatureBitFlags::BuilderCodes as u8); + } else { + msg!("Setting 3rd bit to 0, disabling builder codes"); + state.feature_bit_flags = state.feature_bit_flags & !(FeatureBitFlags::BuilderCodes as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_builder_referral( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can enable feature bit flags" + )?; + + msg!("Setting 4th bit to 1, enabling builder referral"); + state.feature_bit_flags = + state.feature_bit_flags | (FeatureBitFlags::BuilderReferral as u8); + } else { + msg!("Setting 4th bit to 0, disabling builder referral"); + state.feature_bit_flags = + state.feature_bit_flags & !(FeatureBitFlags::BuilderReferral as u8); + } + Ok(()) +} + #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 4e66b0fcee..6bcac5b669 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -23,6 +23,7 @@ use crate::error::ErrorCode; use crate::ids::admin_hot_wallet; use crate::ids::{jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, serum_program}; use crate::instructions::constraints::*; +use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; @@ -49,6 +50,10 @@ use crate::state::perp_market_map::{ get_market_set_for_spot_positions, get_market_set_for_user_positions, get_market_set_from_list, get_writable_perp_market_set, get_writable_perp_market_set_from_vec, MarketSet, PerpMarketMap, }; +use crate::state::revenue_share::RevenueShareEscrowZeroCopyMut; +use crate::state::revenue_share::RevenueShareOrder; +use crate::state::revenue_share::RevenueShareOrderBitFlag; +use crate::state::revenue_share_map::load_revenue_share_map; use crate::state::settle_pnl_mode::SettlePnlMode; use crate::state::signed_msg_user::{ SignedMsgOrderId, SignedMsgUserOrdersLoader, SignedMsgUserOrdersZeroCopyMut, @@ -124,7 +129,7 @@ fn fill_order<'c: 'info, 'info>( let clock = &Clock::get()?; let state = &ctx.accounts.state; - let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mut remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, @@ -140,6 +145,17 @@ fn fill_order<'c: 'info, 'info>( let (makers_and_referrer, makers_and_referrer_stats) = load_user_maps(remaining_accounts_iter, true)?; + let builder_codes_enabled = state.builder_codes_enabled(); + let builder_referral_enabled = state.builder_referral_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + &mut remaining_accounts_iter, + &load!(ctx.accounts.user)?.authority, + )? + } else { + None + }; + controller::repeg::update_amm( market_index, &perp_market_map, @@ -163,6 +179,8 @@ fn fill_order<'c: 'info, 'info>( None, clock, FillMode::Fill, + &mut escrow.as_mut(), + builder_referral_enabled, )?; Ok(()) @@ -632,6 +650,12 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( let mut taker = load_mut!(ctx.accounts.user)?; let mut signed_msg_taker = ctx.accounts.signed_msg_user_orders.load_mut()?; + let escrow = if state.builder_codes_enabled() { + get_revenue_share_escrow_account(&mut remaining_accounts, &taker.authority)? + } else { + None + }; + place_signed_msg_taker_order( taker_key, &mut taker, @@ -642,6 +666,7 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, high_leverage_mode_config, + escrow, state, is_delegate_signer, )?; @@ -658,6 +683,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, high_leverage_mode_config: Option>, + escrow: Option>, state: &State, is_delegate_signer: bool, ) -> Result<()> { @@ -686,6 +712,43 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( is_delegate_signer, )?; + let mut escrow_zc: Option> = None; + let mut builder_fee_bps: Option = None; + if state.builder_codes_enabled() + && verified_message_and_signature.builder_idx.is_some() + && verified_message_and_signature + .builder_fee_tenth_bps + .is_some() + { + if let Some(mut escrow) = escrow { + let builder_idx = verified_message_and_signature.builder_idx.unwrap(); + let builder_fee = verified_message_and_signature + .builder_fee_tenth_bps + .unwrap(); + + validate!( + escrow.fixed.authority == taker.authority, + ErrorCode::InvalidUserAccount, + "RevenueShareEscrow account must be owned by taker", + )?; + + let builder = escrow.get_approved_builder_mut(builder_idx)?; + + if builder.is_revoked() { + return Err(ErrorCode::BuilderRevoked.into()); + } + + if builder_fee > builder.max_fee_tenth_bps { + return Err(ErrorCode::InvalidBuilderFee.into()); + } + + builder_fee_bps = Some(builder_fee); + escrow_zc = Some(escrow); + } else { + msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); + } + } + if is_delegate_signer { validate!( verified_message_and_signature.delegate_signed_taker_pubkey == Some(taker_key), @@ -794,6 +857,33 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ..OrderParams::default() }; + let mut builder_order = if let Some(ref mut escrow) = escrow_zc { + let new_order_id = taker_order_id_to_use - 1; + let new_order_index = taker + .orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + match escrow.add_order(RevenueShareOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => escrow.get_order_mut(order_idx).ok(), + Err(_) => { + msg!("Failed to add stop loss order, escrow is full"); + None + } + } + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -809,6 +899,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( existing_position_direction_override: Some(matching_taker_order_params.direction), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; } @@ -831,6 +922,33 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ..OrderParams::default() }; + let mut builder_order = if let Some(ref mut escrow) = escrow_zc { + let new_order_id = taker_order_id_to_use - 1; + let new_order_index = taker + .orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + match escrow.add_order(RevenueShareOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => escrow.get_order_mut(order_idx).ok(), + Err(_) => { + msg!("Failed to add take profit order, escrow is full"); + None + } + } + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -846,11 +964,39 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( existing_position_direction_override: Some(matching_taker_order_params.direction), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; } signed_msg_order_id.order_id = taker_order_id_to_use; signed_msg_account.add_signed_msg_order_id(signed_msg_order_id)?; + let mut builder_order = if let Some(ref mut escrow) = escrow_zc { + let new_order_id = taker_order_id_to_use; + let new_order_index = taker + .orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + match escrow.add_order(RevenueShareOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => escrow.get_order_mut(order_idx).ok(), + Err(_) => { + msg!("Failed to add order, escrow is full"); + None + } + } + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -866,6 +1012,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( signed_msg_taker_order_slot: Some(order_slot), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; let order_params_hash = @@ -881,6 +1028,10 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ts: clock.unix_timestamp, }); + if let Some(ref mut escrow) = escrow_zc { + escrow.revoke_completed_orders(taker)?; + }; + Ok(()) } @@ -903,18 +1054,30 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( "user have pool_id 0" )?; + let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); + let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set(market_index), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, Some(state.oracle_guard_rails), )?; + let (mut builder_escrow, maybe_rev_share_map) = + if state.builder_codes_enabled() || state.builder_referral_enabled() { + ( + get_revenue_share_escrow_account(&mut remaining_accounts, &user.authority)?, + load_revenue_share_map(&mut remaining_accounts).ok(), + ) + } else { + (None, None) + }; + let market_in_settlement = perp_market_map.get_ref(&market_index)?.status == MarketStatus::Settlement; @@ -959,6 +1122,26 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; } + if state.builder_codes_enabled() || state.builder_referral_enabled() { + if let Some(ref mut escrow) = builder_escrow { + escrow.revoke_completed_orders(user)?; + if let Some(ref builder_map) = maybe_rev_share_map { + controller::revenue_share::sweep_completed_revenue_share_for_market( + market_index, + escrow, + &perp_market_map, + &spot_market_map, + builder_map, + clock.unix_timestamp, + state.builder_codes_enabled(), + state.builder_referral_enabled(), + )?; + } else { + msg!("Builder Users not provided, but RevenueEscrow was provided"); + } + } + } + let spot_market = spot_market_map.get_quote_spot_market()?; validate_spot_market_vault_amount(&spot_market, ctx.accounts.spot_market_vault.amount)?; @@ -979,18 +1162,30 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( let user_key = ctx.accounts.user.key(); let user = &mut load_mut!(ctx.accounts.user)?; + let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); + let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set_from_vec(&market_indexes), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, Some(state.oracle_guard_rails), )?; + let (mut builder_escrow, maybe_rev_share_map) = + if state.builder_codes_enabled() || state.builder_referral_enabled() { + ( + get_revenue_share_escrow_account(&mut remaining_accounts, &user.authority)?, + load_revenue_share_map(&mut remaining_accounts).ok(), + ) + } else { + (None, None) + }; + let meets_margin_requirement = meets_settle_pnl_maintenance_margin_requirement( user, &perp_market_map, @@ -1042,6 +1237,26 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( ) .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; } + + if state.builder_codes_enabled() || state.builder_referral_enabled() { + if let Some(ref mut escrow) = builder_escrow { + escrow.revoke_completed_orders(user)?; + if let Some(ref builder_map) = maybe_rev_share_map { + controller::revenue_share::sweep_completed_revenue_share_for_market( + *market_index, + escrow, + &perp_market_map, + &spot_market_map, + builder_map, + clock.unix_timestamp, + state.builder_codes_enabled(), + state.builder_referral_enabled(), + )?; + } else { + msg!("Builder Users not provided, but RevenueEscrow was provided"); + } + } + } } let spot_market = spot_market_map.get_quote_spot_market()?; diff --git a/programs/drift/src/instructions/optional_accounts.rs b/programs/drift/src/instructions/optional_accounts.rs index 7abe5c33d2..c2365bf0ec 100644 --- a/programs/drift/src/instructions/optional_accounts.rs +++ b/programs/drift/src/instructions/optional_accounts.rs @@ -1,5 +1,8 @@ use crate::error::{DriftResult, ErrorCode}; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; +use crate::state::revenue_share::{ + RevenueShareEscrow, RevenueShareEscrowLoader, RevenueShareEscrowZeroCopyMut, +}; use std::cell::RefMut; use std::convert::TryFrom; @@ -17,7 +20,7 @@ use crate::state::traits::Size; use crate::state::user::{User, UserStats}; use crate::{validate, OracleSource}; use anchor_lang::accounts::account::Account; -use anchor_lang::prelude::{AccountInfo, Interface}; +use anchor_lang::prelude::{AccountInfo, Interface, Pubkey}; use anchor_lang::prelude::{AccountLoader, InterfaceAccount}; use anchor_lang::Discriminator; use anchor_spl::token::TokenAccount; @@ -273,3 +276,40 @@ pub fn get_high_leverage_mode_config<'a>( Ok(Some(high_leverage_mode_config)) } + +pub fn get_revenue_share_escrow_account<'a>( + account_info_iter: &mut Peekable>>, + expected_authority: &Pubkey, +) -> DriftResult>> { + let account_info = account_info_iter.peek(); + if account_info.is_none() { + return Ok(None); + } + + let account_info = account_info.safe_unwrap()?; + + // Check size and discriminator without borrowing + if account_info.data_len() < 80 { + return Ok(None); + } + + let discriminator: [u8; 8] = RevenueShareEscrow::discriminator(); + let borrowed_data = account_info.data.borrow(); + let account_discriminator = array_ref![&borrowed_data, 0, 8]; + if account_discriminator != &discriminator { + return Ok(None); + } + + let account_info = account_info_iter.next().safe_unwrap()?; + + drop(borrowed_data); + let escrow: RevenueShareEscrowZeroCopyMut<'a> = account_info.load_zc_mut()?; + + validate!( + escrow.fixed.authority == *expected_authority, + ErrorCode::RevenueShareEscrowAuthorityMismatch, + "invalid RevenueShareEscrow authority" + )?; + + Ok(Some(escrow)) +} diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 6bff0e6fb4..6d42cd4c4b 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -27,6 +27,7 @@ use crate::ids::{ serum_program, }; use crate::instructions::constraints::*; +use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{ get_referrer_and_referrer_stats, get_whitelist_token, load_maps, AccountMaps, }; @@ -79,6 +80,12 @@ use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::MarketStatus; use crate::state::perp_market_map::{get_writable_perp_market_set, MarketSet}; use crate::state::protected_maker_mode_config::ProtectedMakerModeConfig; +use crate::state::revenue_share::BuilderInfo; +use crate::state::revenue_share::RevenueShare; +use crate::state::revenue_share::RevenueShareEscrow; +use crate::state::revenue_share::RevenueShareOrder; +use crate::state::revenue_share::REVENUE_SHARE_ESCROW_PDA_SEED; +use crate::state::revenue_share::REVENUE_SHARE_PDA_SEED; use crate::state::signed_msg_user::SignedMsgOrderId; use crate::state::signed_msg_user::SignedMsgUserOrdersLoader; use crate::state::signed_msg_user::SignedMsgWsDelegates; @@ -493,6 +500,140 @@ pub fn handle_reset_fuel_season<'c: 'info, 'info>( Ok(()) } +pub fn handle_initialize_revenue_share<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShare<'info>>, +) -> Result<()> { + let mut revenue_share = ctx + .accounts + .revenue_share + .load_init() + .or(Err(ErrorCode::UnableToLoadAccountLoader))?; + revenue_share.authority = ctx.accounts.authority.key(); + revenue_share.total_referrer_rewards = 0; + revenue_share.total_builder_rewards = 0; + Ok(()) +} + +pub fn handle_initialize_revenue_share_escrow<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShareEscrow<'info>>, + num_orders: u16, +) -> Result<()> { + let escrow = &mut ctx.accounts.escrow; + escrow.authority = ctx.accounts.authority.key(); + escrow + .orders + .resize_with(num_orders as usize, RevenueShareOrder::default); + + let state = &mut ctx.accounts.state; + if state.builder_referral_enabled() { + let mut user_stats = ctx.accounts.user_stats.load_mut()?; + escrow.referrer = user_stats.referrer; + user_stats.update_builder_referral_status(); + } + + escrow.validate()?; + Ok(()) +} + +pub fn handle_migrate_referrer<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, MigrateReferrer<'info>>, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if !state.builder_referral_enabled() { + if state.admin != ctx.accounts.payer.key() + || ctx.accounts.payer.key() == admin_hot_wallet::id() + { + msg!("Only admin can migrate referrer until builder referral feature is enabled"); + return Err(anchor_lang::error::ErrorCode::ConstraintSigner.into()); + } + } + + let escrow = &mut ctx.accounts.escrow; + let mut user_stats = ctx.accounts.user_stats.load_mut()?; + escrow.referrer = user_stats.referrer; + user_stats.update_builder_referral_status(); + + escrow.validate()?; + Ok(()) +} + +pub fn handle_resize_revenue_share_escrow_orders<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeRevenueShareEscrowOrders<'info>>, + num_orders: u16, +) -> Result<()> { + let escrow = &mut ctx.accounts.escrow; + validate!( + num_orders as usize >= escrow.orders.len(), + ErrorCode::InvalidRevenueShareResize, + "Invalid shrinking resize for revenue share escrow" + )?; + + escrow + .orders + .resize_with(num_orders as usize, RevenueShareOrder::default); + escrow.validate()?; + Ok(()) +} + +pub fn handle_change_approved_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeApprovedBuilder<'info>>, + builder: Pubkey, + max_fee_tenth_bps: u16, + add: bool, +) -> Result<()> { + let existing_builder_index = ctx + .accounts + .escrow + .approved_builders + .iter() + .position(|b| b.authority == builder); + if let Some(index) = existing_builder_index { + if add { + msg!( + "Updated builder: {} with max fee tenth bps: {} -> {}", + builder, + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps, + max_fee_tenth_bps + ); + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps = max_fee_tenth_bps; + } else { + if ctx + .accounts + .escrow + .orders + .iter() + .any(|o| (o.builder_idx == index as u8) && (!o.is_available())) + { + msg!("Builder has open orders, must cancel orders and settle_pnl before revoking"); + return Err(ErrorCode::CannotRevokeBuilderWithOpenOrders.into()); + } + msg!( + "Revoking builder: {}, max fee tenth bps: {} -> 0", + builder, + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps, + ); + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps = 0; + } + } else { + if add { + ctx.accounts.escrow.approved_builders.push(BuilderInfo { + authority: builder, + max_fee_tenth_bps, + ..BuilderInfo::default() + }); + msg!( + "Added builder: {} with max fee tenth bps: {}", + builder, + max_fee_tenth_bps + ); + } else { + msg!("Tried to revoke builder: {}, but it was not found", builder); + } + } + + Ok(()) +} + #[access_control( deposit_not_paused(&ctx.accounts.state) )] @@ -1889,6 +2030,8 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( maker_existing_quote_entry_amount: from_existing_quote_entry_amount, maker_existing_base_asset_amount: from_existing_base_asset_amount, trigger_price: None, + builder_idx: None, + builder_fee: None, }; emit_stack::<_, { OrderActionRecord::SIZE }>(fill_record)?; @@ -1940,6 +2083,7 @@ pub fn handle_place_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; Ok(()) @@ -2242,6 +2386,7 @@ pub fn handle_place_orders<'c: 'info, 'info>( clock, *params, options, + &mut None, )?; } else { controller::orders::place_spot_order( @@ -2322,6 +2467,7 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( &clock, params, PlaceOrderOptions::default(), + &mut None, )?; drop(user); @@ -2329,6 +2475,14 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( let user = &mut ctx.accounts.user; let order_id = load!(user)?.get_last_order_id(); + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account(remaining_accounts_iter, &load!(user)?.authority)? + } else { + None + }; + let (base_asset_amount_filled, _) = controller::orders::fill_perp_order( order_id, &ctx.accounts.state, @@ -2347,6 +2501,8 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( is_immediate_or_cancel || optional_params.is_some(), auction_duration_percentage, ), + &mut escrow.as_mut(), + builder_referral_enabled, )?; let order_unfilled = load!(ctx.accounts.user)? @@ -2436,6 +2592,7 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -2447,6 +2604,17 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( makers_and_referrer.insert(ctx.accounts.user.key(), ctx.accounts.user.clone())?; makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + remaining_accounts_iter, + &load!(ctx.accounts.taker)?.authority, + )? + } else { + None + }; + controller::orders::fill_perp_order( taker_order_id, state, @@ -2462,6 +2630,8 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, + &mut escrow.as_mut(), + builder_referral_enabled, )?; let order_exists = load!(ctx.accounts.user)? @@ -2537,6 +2707,7 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -2548,6 +2719,17 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( makers_and_referrer.insert(ctx.accounts.user.key(), ctx.accounts.user.clone())?; makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + remaining_accounts_iter, + &load!(ctx.accounts.taker)?.authority, + )? + } else { + None + }; + let taker_signed_msg_account = ctx.accounts.taker_signed_msg_user_orders.load()?; let taker_order_id = taker_signed_msg_account .iter() @@ -2570,6 +2752,8 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, + &mut escrow.as_mut(), + builder_referral_enabled, )?; let order_exists = load!(ctx.accounts.user)? @@ -4586,3 +4770,106 @@ pub struct UpdateUserProtectedMakerMode<'info> { #[account(mut)] pub protected_maker_mode_config: AccountLoader<'info, ProtectedMakerModeConfig>, } + +#[derive(Accounts)] +#[instruction()] +pub struct InitializeRevenueShare<'info> { + #[account( + init, + seeds = [REVENUE_SHARE_PDA_SEED.as_ref(), authority.key().as_ref()], + space = RevenueShare::space(), + bump, + payer = payer + )] + pub revenue_share: AccountLoader<'info, RevenueShare>, + /// CHECK: The builder and/or referrer authority, beneficiary of builder/ref fees + pub authority: AccountInfo<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(num_orders: u16)] +pub struct InitializeRevenueShareEscrow<'info> { + #[account( + init, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + space = RevenueShareEscrow::space(num_orders as usize, 1), + bump, + payer = payer + )] + pub escrow: Box>, + /// CHECK: The auth owning this account, payer of builder/ref fees + pub authority: AccountInfo<'info>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub state: Box>, + #[account(mut)] + pub payer: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct MigrateReferrer<'info> { + #[account( + mut, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + )] + pub escrow: Box>, + /// CHECK: The auth owning this account, payer of builder/ref fees + pub authority: AccountInfo<'info>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub state: Box>, + pub payer: Signer<'info>, +} + +#[derive(Accounts)] +#[instruction(num_orders: u16)] +pub struct ResizeRevenueShareEscrowOrders<'info> { + #[account( + mut, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + realloc = RevenueShareEscrow::space(num_orders as usize, escrow.approved_builders.len()), + realloc::payer = payer, + realloc::zero = false, + has_one = authority + )] + pub escrow: Box>, + /// CHECK: The owner of RevenueShareEscrow + pub authority: AccountInfo<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(builder: Pubkey, max_fee_tenth_bps: u16, add: bool)] +pub struct ChangeApprovedBuilder<'info> { + #[account( + mut, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + // revoking a builder does not remove the slot to avoid unintended reuse + realloc = RevenueShareEscrow::space(escrow.orders.len(), if add { escrow.approved_builders.len() + 1 } else { escrow.approved_builders.len() }), + realloc::payer = payer, + realloc::zero = false, + has_one = authority + )] + pub escrow: Box>, + pub authority: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 0b3eedd16b..e71b6de2a1 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1807,6 +1807,55 @@ pub mod drift { ) -> Result<()> { handle_update_feature_bit_flags_median_trigger_price(ctx, enable) } + + // pub fn update_feature_bit_flags_builder_referral( + // ctx: Context, + // enable: bool, + // ) -> Result<()> { + // handle_update_feature_bit_flags_builder_referral(ctx, enable) + // } + + pub fn update_feature_bit_flags_builder_codes( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_builder_codes(ctx, enable) + } + + pub fn initialize_revenue_share<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShare<'info>>, + ) -> Result<()> { + handle_initialize_revenue_share(ctx) + } + + pub fn initialize_revenue_share_escrow<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShareEscrow<'info>>, + num_orders: u16, + ) -> Result<()> { + handle_initialize_revenue_share_escrow(ctx, num_orders) + } + + // pub fn migrate_referrer<'c: 'info, 'info>( + // ctx: Context<'_, '_, 'c, 'info, MigrateReferrer<'info>>, + // ) -> Result<()> { + // handle_migrate_referrer(ctx) + // } + + pub fn resize_revenue_share_escrow_orders<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeRevenueShareEscrowOrders<'info>>, + num_orders: u16, + ) -> Result<()> { + handle_resize_revenue_share_escrow_orders(ctx, num_orders) + } + + pub fn change_approved_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeApprovedBuilder<'info>>, + builder: Pubkey, + max_fee_bps: u16, + add: bool, + ) -> Result<()> { + handle_change_approved_builder(ctx, builder, max_fee_bps, add) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/drift/src/math/fees.rs b/programs/drift/src/math/fees.rs index 3e23e718f3..4b358b071a 100644 --- a/programs/drift/src/math/fees.rs +++ b/programs/drift/src/math/fees.rs @@ -30,6 +30,7 @@ pub struct FillFees { pub filler_reward: u64, pub referrer_reward: u64, pub referee_discount: u64, + pub builder_fee: Option, } pub fn calculate_fee_for_fulfillment_with_amm( @@ -45,6 +46,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( is_post_only: bool, fee_adjustment: i16, user_high_leverage_mode: bool, + builder_fee_bps: Option, ) -> DriftResult { let fee_tier = determine_user_fee_tier( user_stats, @@ -92,6 +94,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( filler_reward, referrer_reward: 0, referee_discount: 0, + builder_fee: None, }) } else { let mut fee = calculate_taker_fee(quote_asset_amount, &fee_tier, fee_adjustment)?; @@ -131,6 +134,16 @@ pub fn calculate_fee_for_fulfillment_with_amm( let fee_to_market_for_lp = fee_to_market.safe_sub(quote_asset_amount_surplus)?; + let builder_fee = if let Some(builder_fee_bps) = builder_fee_bps { + Some( + quote_asset_amount + .safe_mul(builder_fee_bps.cast()?)? + .safe_div(100_000)?, + ) + } else { + None + }; + // must be non-negative Ok(FillFees { user_fee: fee, @@ -140,6 +153,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( filler_reward, referrer_reward, referee_discount, + builder_fee, }) } } @@ -286,6 +300,7 @@ pub fn calculate_fee_for_fulfillment_with_match( market_type: &MarketType, fee_adjustment: i16, user_high_leverage_mode: bool, + builder_fee_bps: Option, ) -> DriftResult { let taker_fee_tier = determine_user_fee_tier( taker_stats, @@ -337,6 +352,16 @@ pub fn calculate_fee_for_fulfillment_with_match( .safe_sub(maker_rebate)? .cast::()?; + let builder_fee = if let Some(builder_fee_bps) = builder_fee_bps { + Some( + quote_asset_amount + .safe_mul(builder_fee_bps.cast()?)? + .safe_div(100_000)?, + ) + } else { + None + }; + Ok(FillFees { user_fee: taker_fee, maker_rebate, @@ -345,6 +370,7 @@ pub fn calculate_fee_for_fulfillment_with_match( referrer_reward, fee_to_market_for_lp: 0, referee_discount, + builder_fee, }) } diff --git a/programs/drift/src/math/fees/tests.rs b/programs/drift/src/math/fees/tests.rs index 82188b62b9..296f3bfce5 100644 --- a/programs/drift/src/math/fees/tests.rs +++ b/programs/drift/src/math/fees/tests.rs @@ -31,6 +31,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -75,6 +76,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -118,6 +120,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -161,6 +164,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -202,6 +206,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -240,6 +245,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -271,6 +277,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 50, false, + None, ) .unwrap(); @@ -303,6 +310,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -335,6 +343,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -373,6 +382,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -404,6 +414,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -436,6 +447,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, true, + None, ) .unwrap(); @@ -468,6 +480,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -500,6 +513,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -538,6 +552,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, true, + None, ) .unwrap(); @@ -583,6 +598,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, 0, false, + None, ) .unwrap(); @@ -620,6 +636,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -649,6 +666,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, 50, false, + None, ) .unwrap(); @@ -679,6 +697,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -709,6 +728,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -746,6 +766,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, true, + None, ) .unwrap(); diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 2d6c20fa7a..9ebe15e0fd 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -256,10 +256,15 @@ pub struct OrderActionRecord { pub maker_existing_base_asset_amount: Option, /// precision: PRICE_PRECISION pub trigger_price: Option, + + /// the idx of the builder in the taker's [`RevenueShareEscrow`] account + pub builder_idx: Option, + /// precision: QUOTE_PRECISION builder fee paid by the taker + pub builder_fee: Option, } impl Size for OrderActionRecord { - const SIZE: usize = 464; + const SIZE: usize = 480; } pub fn get_order_action_record( @@ -288,6 +293,8 @@ pub fn get_order_action_record( maker_existing_quote_entry_amount: Option, maker_existing_base_asset_amount: Option, trigger_price: Option, + builder_idx: Option, + builder_fee: Option, ) -> DriftResult { Ok(OrderActionRecord { ts, @@ -341,6 +348,8 @@ pub fn get_order_action_record( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, trigger_price, + builder_idx, + builder_fee, }) } @@ -698,6 +707,23 @@ pub struct FuelSeasonRecord { pub fuel_total: u128, } +#[event] +pub struct RevenueShareSettleRecord { + pub ts: i64, + pub builder: Option, + pub referrer: Option, + pub fee_settled: u64, + pub market_index: u16, + pub market_type: MarketType, + pub builder_sub_account_id: u16, + pub builder_total_referrer_rewards: u64, + pub builder_total_builder_rewards: u64, +} + +impl Size for RevenueShareSettleRecord { + const SIZE: usize = 140; +} + pub fn emit_stack(event: T) -> DriftResult { #[cfg(not(feature = "drift-rs"))] { diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index a9c9724757..9237c008cd 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -15,6 +15,8 @@ pub mod perp_market; pub mod perp_market_map; pub mod protected_maker_mode_config; pub mod pyth_lazer_oracle; +pub mod revenue_share; +pub mod revenue_share_map; pub mod settle_pnl_mode; pub mod signed_msg_user; pub mod spot_fulfillment_params; diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 3b3431a38c..95ccf8424d 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -873,6 +873,8 @@ pub struct SignedMsgOrderParamsMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] @@ -884,6 +886,8 @@ pub struct SignedMsgOrderParamsDelegateMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] diff --git a/programs/drift/src/state/revenue_share.rs b/programs/drift/src/state/revenue_share.rs new file mode 100644 index 0000000000..6d99427054 --- /dev/null +++ b/programs/drift/src/state/revenue_share.rs @@ -0,0 +1,572 @@ +use std::cell::{Ref, RefMut}; + +use anchor_lang::prelude::Pubkey; +use anchor_lang::*; +use anchor_lang::{account, zero_copy}; +use borsh::{BorshDeserialize, BorshSerialize}; +use prelude::AccountInfo; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::user::{MarketType, OrderStatus, User}; +use crate::validate; +use crate::{msg, ID}; + +pub const REVENUE_SHARE_PDA_SEED: &str = "REV_SHARE"; +pub const REVENUE_SHARE_ESCROW_PDA_SEED: &str = "REV_ESCROW"; + +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] +pub enum RevenueShareOrderBitFlag { + #[default] + Init = 0b00000000, + Open = 0b00000001, + Completed = 0b00000010, + Referral = 0b00000100, +} + +#[account(zero_copy(unsafe))] +#[derive(Eq, PartialEq, Debug, Default)] +pub struct RevenueShare { + /// the owner of this account, a builder or referrer + pub authority: Pubkey, + pub total_referrer_rewards: u64, + pub total_builder_rewards: u64, + pub padding: [u8; 18], +} + +impl RevenueShare { + pub fn space() -> usize { + 8 + 32 + 8 + 8 + 18 + } +} + +#[zero_copy] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +pub struct RevenueShareOrder { + /// fees accrued so far for this order slot. This is not exclusively fees from this order_id + /// and may include fees from other orders in the same market. This may be swept to the + /// builder's SpotPosition during settle_pnl. + pub fees_accrued: u64, + /// the order_id of the current active order in this slot. It's only relevant while bit_flag = Open + pub order_id: u32, + /// the builder fee on this order, in tenths of a bps, e.g. 100 = 0.01% + pub fee_tenth_bps: u16, + pub market_index: u16, + /// the subaccount_id of the user who created this order. It's only relevant while bit_flag = Open + pub sub_account_id: u16, + /// the index of the RevenueShareEscrow.approved_builders list, that this order's fee will settle to. Ignored + /// if bit_flag = Referral. + pub builder_idx: u8, + /// bitflags that describe the state of the order. + /// [`RevenueShareOrderBitFlag::Init`]: this order slot is available for use. + /// [`RevenueShareOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order. + /// [`RevenueShareOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into. + /// the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders. + /// [`RevenueShareOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled for this market. + /// If it is set, no other bitflag should be set. + pub bit_flags: u8, + /// the index into the User's orders list when this RevenueShareOrder was created, make sure to verify that order_id matches. + pub user_order_index: u8, + pub market_type: MarketType, + pub padding: [u8; 10], +} + +impl RevenueShareOrder { + pub fn new( + builder_idx: u8, + sub_account_id: u16, + order_id: u32, + fee_tenth_bps: u16, + market_type: MarketType, + market_index: u16, + bit_flags: u8, + user_order_index: u8, + ) -> Self { + Self { + builder_idx, + order_id, + fee_tenth_bps, + market_type, + market_index, + fees_accrued: 0, + bit_flags, + sub_account_id, + user_order_index, + padding: [0; 10], + } + } + + pub fn space() -> usize { + std::mem::size_of::() + } + + pub fn add_bit_flag(&mut self, flag: RevenueShareOrderBitFlag) { + self.bit_flags |= flag as u8; + } + + pub fn is_bit_flag_set(&self, flag: RevenueShareOrderBitFlag) -> bool { + (self.bit_flags & flag as u8) != 0 + } + + // An order is Open after it is created, the slot is considered occupied + // and it is waiting to become `Completed` (filled or canceled). + pub fn is_open(&self) -> bool { + self.is_bit_flag_set(RevenueShareOrderBitFlag::Open) + } + + // An order is Completed after it is filled or canceled. It is waiting to be settled + // into the builder's account + pub fn is_completed(&self) -> bool { + self.is_bit_flag_set(RevenueShareOrderBitFlag::Completed) + } + + /// An order slot is available (can be written to) if it is neither Completed nor Open. + pub fn is_available(&self) -> bool { + !self.is_completed() && !self.is_open() && !self.is_referral_order() + } + + pub fn is_referral_order(&self) -> bool { + self.is_bit_flag_set(RevenueShareOrderBitFlag::Referral) + } + + /// Checks if `self` can be merged with `other`. Merged orders track cumulative fees accrued + /// and are settled together, making more efficient use of the orders list. + pub fn is_mergeable(&self, other: &RevenueShareOrder) -> bool { + (self.is_referral_order() == other.is_referral_order()) + && other.is_completed() + && other.market_index == self.market_index + && other.market_type == self.market_type + && other.builder_idx == self.builder_idx + } + + /// Merges `other` into `self`. The orders must be mergeable. + pub fn merge(mut self, other: &RevenueShareOrder) -> DriftResult { + validate!( + self.is_mergeable(other), + ErrorCode::DefaultError, + "Orders are not mergeable" + )?; + self.fees_accrued = self + .fees_accrued + .checked_add(other.fees_accrued) + .ok_or(ErrorCode::MathError)?; + Ok(self) + } +} + +#[zero_copy] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct BuilderInfo { + pub authority: Pubkey, // builder authority + pub max_fee_tenth_bps: u16, + pub padding: [u8; 6], +} + +impl BuilderInfo { + pub fn space() -> usize { + std::mem::size_of::() + } + + pub fn is_revoked(&self) -> bool { + self.max_fee_tenth_bps == 0 + } +} + +#[account] +#[derive(Eq, PartialEq, Debug)] +#[repr(C)] +pub struct RevenueShareEscrow { + /// the owner of this account, a user + pub authority: Pubkey, + pub referrer: Pubkey, + pub referrer_boost_expire_ts: u32, + pub referrer_reward_offset: i8, + pub referee_fee_numerator_offset: i8, + pub referrer_boost_numerator: i8, + pub reserved_fixed: [u8; 17], + pub padding0: u32, // align with [`RevenueShareEscrow::orders`] 4 bytes len prefix + pub orders: Vec, + pub padding1: u32, // align with [`RevenueShareEscrow::approved_builders`] 4 bytes len prefix + pub approved_builders: Vec, +} + +impl RevenueShareEscrow { + pub fn space(num_orders: usize, num_builders: usize) -> usize { + 8 + // discriminator + std::mem::size_of::() + // fixed header + 4 + // orders Vec length prefix + 4 + // padding0 + num_orders * std::mem::size_of::() + // orders data + 4 + // approved_builders Vec length prefix + 4 + // padding1 + num_builders * std::mem::size_of::() // builders data + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.orders.len() <= 128 && self.approved_builders.len() <= 128, + ErrorCode::DefaultError, + "RevenueShareEscrow orders and approved_builders len must be between 1 and 128" + )?; + Ok(()) + } +} + +#[zero_copy] +#[derive(Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct RevenueShareEscrowFixed { + pub authority: Pubkey, + pub referrer: Pubkey, + pub referrer_boost_expire_ts: u32, + pub referrer_reward_offset: i8, + pub referee_fee_numerator_offset: i8, + pub referrer_boost_numerator: i8, + pub reserved_fixed: [u8; 17], +} + +impl Default for RevenueShareEscrowFixed { + fn default() -> Self { + Self { + authority: Pubkey::default(), + referrer: Pubkey::default(), + referrer_boost_expire_ts: 0, + referrer_reward_offset: 0, + referee_fee_numerator_offset: 0, + referrer_boost_numerator: 0, + reserved_fixed: [0; 17], + } + } +} + +impl Default for RevenueShareEscrow { + fn default() -> Self { + Self { + authority: Pubkey::default(), + referrer: Pubkey::default(), + referrer_boost_expire_ts: 0, + referrer_reward_offset: 0, + referee_fee_numerator_offset: 0, + referrer_boost_numerator: 0, + reserved_fixed: [0; 17], + padding0: 0, + orders: Vec::new(), + padding1: 0, + approved_builders: Vec::new(), + } + } +} + +pub struct RevenueShareEscrowZeroCopy<'a> { + pub fixed: Ref<'a, RevenueShareEscrowFixed>, + pub data: Ref<'a, [u8]>, +} + +impl<'a> RevenueShareEscrowZeroCopy<'a> { + pub fn orders_len(&self) -> u32 { + let length_bytes = &self.data[4..8]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + pub fn approved_builders_len(&self) -> u32 { + let orders_data_size = + self.orders_len() as usize * std::mem::size_of::(); + let offset = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + orders_data_size + 4; // RevenueShareEscrow.padding1 + let length_bytes = &self.data[offset..offset + 4]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + + pub fn get_order(&self, index: u32) -> DriftResult<&RevenueShareOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + index as usize * size; // orders data + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn get_approved_builder(&self, index: u32) -> DriftResult<&BuilderInfo> { + validate!( + index < self.approved_builders_len(), + ErrorCode::DefaultError, + "Builder index out of bounds" + )?; + let size = std::mem::size_of::(); + let offset = 4 + 4 + // Skip orders Vec length prefix + padding0 + self.orders_len() as usize * std::mem::size_of::() + // orders data + 4; // Skip approved_builders Vec length prefix + padding1 + let start = offset + index as usize * size; + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn iter_orders(&self) -> impl Iterator> + '_ { + (0..self.orders_len()).map(move |i| self.get_order(i)) + } + + pub fn iter_approved_builders(&self) -> impl Iterator> + '_ { + (0..self.approved_builders_len()).map(move |i| self.get_approved_builder(i)) + } +} + +pub struct RevenueShareEscrowZeroCopyMut<'a> { + pub fixed: RefMut<'a, RevenueShareEscrowFixed>, + pub data: RefMut<'a, [u8]>, +} + +impl<'a> RevenueShareEscrowZeroCopyMut<'a> { + pub fn has_referrer(&self) -> bool { + self.fixed.referrer != Pubkey::default() + } + + pub fn get_referrer(&self) -> Option { + if self.has_referrer() { + Some(self.fixed.referrer) + } else { + None + } + } + + pub fn orders_len(&self) -> u32 { + // skip RevenueShareEscrow.padding0 + let length_bytes = &self.data[4..8]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + pub fn approved_builders_len(&self) -> u32 { + // Calculate offset to the approved_builders Vec length + let orders_data_size = + self.orders_len() as usize * std::mem::size_of::(); + let offset = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + orders_data_size + + 4; // RevenueShareEscrow.padding1 + let length_bytes = &self.data[offset..offset + 4]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + + pub fn get_order_mut(&mut self, index: u32) -> DriftResult<&mut RevenueShareOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + index as usize * size; + Ok(bytemuck::from_bytes_mut( + &mut self.data[start..(start + size)], + )) + } + + /// Returns the index of an order for a given sub_account_id and order_id, if present. + pub fn find_order_index(&self, sub_account_id: u16, order_id: u32) -> Option { + for i in 0..self.orders_len() { + if let Ok(existing_order) = self.get_order(i) { + if existing_order.order_id == order_id + && existing_order.sub_account_id == sub_account_id + { + return Some(i); + } + } + } + None + } + + /// Returns the index for the referral order, creating one if necessary. Returns None if a new order + /// cannot be created. + pub fn find_or_create_referral_index(&mut self, market_index: u16) -> Option { + // look for an existing referral order + for i in 0..self.orders_len() { + if let Ok(existing_order) = self.get_order(i) { + if existing_order.is_referral_order() && existing_order.market_index == market_index + { + return Some(i); + } + } + } + + // try to create a referral order in an available order slot + match self.add_order(RevenueShareOrder::new( + 0, + 0, + 0, + 0, + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Referral as u8, + 0, + )) { + Ok(idx) => Some(idx), + Err(_) => { + msg!("Failed to add referral order, RevenueShareEscrow is full"); + None + } + } + } + + pub fn get_order(&self, index: u32) -> DriftResult<&RevenueShareOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + index as usize * size; // orders data + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn get_approved_builder_mut(&mut self, index: u8) -> DriftResult<&mut BuilderInfo> { + validate!( + index < self.approved_builders_len().cast::()?, + ErrorCode::DefaultError, + "Builder index out of bounds, index: {}, orderslen: {}, builderslen: {}", + index, + self.orders_len(), + self.approved_builders_len() + )?; + let size = std::mem::size_of::(); + let offset = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + self.orders_len() as usize * std::mem::size_of::() + // orders data + 4 + // RevenueShareEscrow.padding1 + 4; // vec len + let start = offset + index as usize * size; + Ok(bytemuck::from_bytes_mut( + &mut self.data[start..start + size], + )) + } + + pub fn add_order(&mut self, order: RevenueShareOrder) -> DriftResult { + for i in 0..self.orders_len() { + let existing_order = self.get_order_mut(i)?; + if existing_order.is_mergeable(&order) { + *existing_order = existing_order.merge(&order)?; + return Ok(i); + } else if existing_order.is_available() { + *existing_order = order; + return Ok(i); + } + } + + Err(ErrorCode::RevenueShareEscrowOrdersAccountFull.into()) + } + + /// Marks any [`RevenueShareOrder`]s as Complete if there is no longer a corresponding + /// open order in the user's account. This is used to lazily reconcile state when + /// in place_order and settle_pnl instead of requiring explicit updates on cancels. + pub fn revoke_completed_orders(&mut self, user: &User) -> DriftResult<()> { + for i in 0..self.orders_len() { + if let Ok(rev_share_order) = self.get_order_mut(i) { + if rev_share_order.is_referral_order() { + continue; + } + if user.sub_account_id != rev_share_order.sub_account_id { + continue; + } + if rev_share_order.is_open() && !rev_share_order.is_completed() { + let user_order = user.orders[rev_share_order.user_order_index as usize]; + let still_open = user_order.status == OrderStatus::Open + && user_order.order_id == rev_share_order.order_id; + if !still_open { + if rev_share_order.fees_accrued > 0 { + rev_share_order.add_bit_flag(RevenueShareOrderBitFlag::Completed); + } else { + // order had no fees accrued, we can just clear out the slot + *rev_share_order = RevenueShareOrder::default(); + } + } + } + } + } + + Ok(()) + } +} + +pub trait RevenueShareEscrowLoader<'a> { + fn load_zc(&self) -> DriftResult; + fn load_zc_mut(&self) -> DriftResult; +} + +impl<'a> RevenueShareEscrowLoader<'a> for AccountInfo<'a> { + fn load_zc(&self) -> DriftResult { + let owner = self.owner; + + validate!( + owner == &ID, + ErrorCode::DefaultError, + "invalid RevenueShareEscrow owner", + )?; + + let data = self.try_borrow_data().safe_unwrap()?; + + let (discriminator, data) = Ref::map_split(data, |d| d.split_at(8)); + validate!( + *discriminator == RevenueShareEscrow::discriminator(), + ErrorCode::DefaultError, + "invalid signed_msg user orders discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (fixed, data) = Ref::map_split(data, |d| d.split_at(hdr_size)); + Ok(RevenueShareEscrowZeroCopy { + fixed: Ref::map(fixed, |b| bytemuck::from_bytes(b)), + data, + }) + } + + fn load_zc_mut(&self) -> DriftResult { + let owner = self.owner; + + validate!( + owner == &ID, + ErrorCode::DefaultError, + "invalid RevenueShareEscrow owner", + )?; + + let data = self.try_borrow_mut_data().safe_unwrap()?; + + let (discriminator, data) = RefMut::map_split(data, |d| d.split_at_mut(8)); + validate!( + *discriminator == RevenueShareEscrow::discriminator(), + ErrorCode::DefaultError, + "invalid signed_msg user orders discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (fixed, data) = RefMut::map_split(data, |d| d.split_at_mut(hdr_size)); + Ok(RevenueShareEscrowZeroCopyMut { + fixed: RefMut::map(fixed, |b| bytemuck::from_bytes_mut(b)), + data, + }) + } +} diff --git a/programs/drift/src/state/revenue_share_map.rs b/programs/drift/src/state/revenue_share_map.rs new file mode 100644 index 0000000000..2e45195040 --- /dev/null +++ b/programs/drift/src/state/revenue_share_map.rs @@ -0,0 +1,209 @@ +use crate::error::{DriftResult, ErrorCode}; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::msg; +use crate::state::revenue_share::RevenueShare; +use crate::state::traits::Size; +use crate::state::user::User; +use crate::validate; +use anchor_lang::prelude::AccountLoader; +use anchor_lang::Discriminator; +use arrayref::array_ref; +use solana_program::account_info::AccountInfo; +use solana_program::pubkey::Pubkey; +use std::cell::RefMut; +use std::collections::BTreeMap; +use std::iter::Peekable; +use std::panic::Location; +use std::slice::Iter; + +pub struct RevenueShareEntry<'a> { + pub user: Option>, + pub revenue_share: Option>, +} + +impl<'a> Default for RevenueShareEntry<'a> { + fn default() -> Self { + Self { + user: None, + revenue_share: None, + } + } +} + +pub struct RevenueShareMap<'a>(pub BTreeMap>); + +impl<'a> RevenueShareMap<'a> { + pub fn empty() -> Self { + RevenueShareMap(BTreeMap::new()) + } + + pub fn insert_user( + &mut self, + authority: Pubkey, + user_loader: AccountLoader<'a, User>, + ) -> DriftResult { + let entry = self.0.entry(authority).or_default(); + validate!( + entry.user.is_none(), + ErrorCode::DefaultError, + "Duplicate User for authority {:?}", + authority + )?; + entry.user = Some(user_loader); + Ok(()) + } + + pub fn insert_revenue_share( + &mut self, + authority: Pubkey, + revenue_share_loader: AccountLoader<'a, RevenueShare>, + ) -> DriftResult { + let entry = self.0.entry(authority).or_default(); + validate!( + entry.revenue_share.is_none(), + ErrorCode::DefaultError, + "Duplicate RevenueShare for authority {:?}", + authority + )?; + entry.revenue_share = Some(revenue_share_loader); + Ok(()) + } + + #[track_caller] + #[inline(always)] + pub fn get_user_ref_mut(&self, authority: &Pubkey) -> DriftResult> { + let loader = match self.0.get(authority).and_then(|e| e.user.as_ref()) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find user for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + return Err(ErrorCode::UserNotFound); + } + }; + + match loader.load_mut() { + Ok(user) => Ok(user), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load user for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + Err(ErrorCode::UnableToLoadUserAccount) + } + } + } + + #[track_caller] + #[inline(always)] + pub fn get_revenue_share_account_mut( + &self, + authority: &Pubkey, + ) -> DriftResult> { + let loader = match self.0.get(authority).and_then(|e| e.revenue_share.as_ref()) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find revenue share for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + return Err(ErrorCode::UnableToLoadRevenueShareAccount); + } + }; + + match loader.load_mut() { + Ok(revenue_share) => Ok(revenue_share), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load revenue share for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + Err(ErrorCode::UnableToLoadRevenueShareAccount) + } + } + } +} + +pub fn load_revenue_share_map<'a: 'b, 'b>( + account_info_iter: &mut Peekable>>, +) -> DriftResult> { + let mut revenue_share_map = RevenueShareMap::empty(); + + let user_discriminator: [u8; 8] = User::discriminator(); + let rev_share_discriminator: [u8; 8] = RevenueShare::discriminator(); + + while let Some(account_info) = account_info_iter.peek() { + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::DefaultError))?; + + if data.len() < 8 { + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + + if account_discriminator == &user_discriminator { + let user_account_info = account_info_iter.next().safe_unwrap()?; + let is_writable = user_account_info.is_writable; + if !is_writable { + return Err(ErrorCode::UserWrongMutability); + } + + // Extract authority from User account data (after discriminator) + let data = user_account_info + .try_borrow_data() + .or(Err(ErrorCode::CouldNotLoadUserData))?; + let expected_data_len = User::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::CouldNotLoadUserData); + } + let authority_slice = array_ref![data, 8, 32]; + let authority = Pubkey::from(*authority_slice); + + let user_account_loader: AccountLoader = + AccountLoader::try_from(user_account_info) + .or(Err(ErrorCode::InvalidUserAccount))?; + + revenue_share_map.insert_user(authority, user_account_loader)?; + continue; + } + + if account_discriminator == &rev_share_discriminator { + let revenue_share_account_info = account_info_iter.next().safe_unwrap()?; + let is_writable = revenue_share_account_info.is_writable; + if !is_writable { + return Err(ErrorCode::DefaultError); + } + + let authority_slice = array_ref![data, 8, 32]; + let authority = Pubkey::from(*authority_slice); + + let revenue_share_account_loader: AccountLoader = + AccountLoader::try_from(revenue_share_account_info) + .or(Err(ErrorCode::InvalidRevenueShareAccount))?; + + revenue_share_map.insert_revenue_share(authority, revenue_share_account_loader)?; + continue; + } + + break; + } + + Ok(revenue_share_map) +} diff --git a/programs/drift/src/state/state.rs b/programs/drift/src/state/state.rs index 9e8b0961df..331a7ffa38 100644 --- a/programs/drift/src/state/state.rs +++ b/programs/drift/src/state/state.rs @@ -120,12 +120,22 @@ impl State { pub fn use_median_trigger_price(&self) -> bool { (self.feature_bit_flags & (FeatureBitFlags::MedianTriggerPrice as u8)) > 0 } + + pub fn builder_codes_enabled(&self) -> bool { + (self.feature_bit_flags & (FeatureBitFlags::BuilderCodes as u8)) > 0 + } + + pub fn builder_referral_enabled(&self) -> bool { + (self.feature_bit_flags & (FeatureBitFlags::BuilderReferral as u8)) > 0 + } } #[derive(Clone, Copy, PartialEq, Debug, Eq)] pub enum FeatureBitFlags { MmOracleUpdate = 0b00000001, MedianTriggerPrice = 0b00000010, + BuilderCodes = 0b00000100, + BuilderReferral = 0b00001000, } impl Size for State { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index db6755822a..363efeab3f 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -29,6 +29,7 @@ use crate::{safe_increment, SPOT_WEIGHT_PRECISION}; use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; +use bytemuck::{Pod, Zeroable}; use std::cmp::max; use std::fmt; use std::ops::Neg; @@ -1602,12 +1603,16 @@ impl fmt::Display for MarketType { } } +unsafe impl Zeroable for MarketType {} +unsafe impl Pod for MarketType {} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] pub enum OrderBitFlag { SignedMessage = 0b00000001, OracleTriggerMarket = 0b00000010, SafeTriggerOrder = 0b00000100, NewTriggerReduceOnly = 0b00001000, + HasBuilder = 0b00010000, } #[account(zero_copy(unsafe))] @@ -1684,6 +1689,7 @@ pub struct UserStats { pub enum ReferrerStatus { IsReferrer = 0b00000001, IsReferred = 0b00000010, + BuilderReferral = 0b00000100, } impl ReferrerStatus { @@ -1694,6 +1700,10 @@ impl ReferrerStatus { pub fn is_referred(status: u8) -> bool { status & ReferrerStatus::IsReferred as u8 != 0 } + + pub fn has_builder_referral(status: u8) -> bool { + status & ReferrerStatus::BuilderReferral as u8 != 0 + } } impl Size for UserStats { @@ -1900,6 +1910,14 @@ impl UserStats { } } + pub fn update_builder_referral_status(&mut self) { + if !self.referrer.eq(&Pubkey::default()) { + self.referrer_status |= ReferrerStatus::BuilderReferral as u8; + } else { + self.referrer_status &= !(ReferrerStatus::BuilderReferral as u8); + } + } + pub fn update_fuel_overflow_status(&mut self, has_overflow: bool) { if has_overflow { self.fuel_overflow_status |= FuelOverflowStatus::Exists as u8; diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index 66ba1cab98..a7b1fbcf0a 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -58,6 +58,8 @@ pub struct VerifiedMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, pub signature: [u8; 64], } @@ -96,6 +98,8 @@ pub fn deserialize_into_verified_message( take_profit_order_params: deserialized.take_profit_order_params, stop_loss_order_params: deserialized.stop_loss_order_params, max_margin_ratio: deserialized.max_margin_ratio, + builder_idx: deserialized.builder_idx, + builder_fee_tenth_bps: deserialized.builder_fee_tenth_bps, signature: *signature, }); } else { @@ -123,6 +127,8 @@ pub fn deserialize_into_verified_message( take_profit_order_params: deserialized.take_profit_order_params, stop_loss_order_params: deserialized.stop_loss_order_params, max_margin_ratio: deserialized.max_margin_ratio, + builder_idx: deserialized.builder_idx, + builder_fee_tenth_bps: deserialized.builder_fee_tenth_bps, signature: *signature, }); } diff --git a/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs index fae2456b43..3c5c2d1c66 100644 --- a/programs/drift/src/validation/sig_verification/tests.rs +++ b/programs/drift/src/validation/sig_verification/tests.rs @@ -31,6 +31,9 @@ mod sig_verification { assert!(verified_message.take_profit_order_params.is_none()); assert!(verified_message.stop_loss_order_params.is_none()); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); + // Verify order params let order_params = &verified_message.signed_msg_order_params; assert_eq!(order_params.user_order_id, 1); @@ -68,6 +71,8 @@ mod sig_verification { assert_eq!(verified_message.slot, 2345); assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -117,6 +122,8 @@ mod sig_verification { assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_some()); assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -170,6 +177,8 @@ mod sig_verification { assert!(verified_message.take_profit_order_params.is_none()); assert!(verified_message.stop_loss_order_params.is_none()); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); // Verify order params let order_params = &verified_message.signed_msg_order_params; @@ -213,6 +222,8 @@ mod sig_verification { assert_eq!(verified_message.slot, 2345); assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -267,6 +278,11 @@ mod sig_verification { assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_some()); assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); + + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -290,4 +306,51 @@ mod sig_verification { assert_eq!(order_params.auction_start_price, Some(240000000i64)); assert_eq!(order_params.auction_end_price, Some(238000000i64)); } + + #[test] + fn test_deserialize_into_verified_message_delegate_with_max_margin_ratio_and_builder_params() { + let signature = [1u8; 64]; + let payload = vec![ + 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, + 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 0, 1, 255, 255, 1, + 1, 1, 58, 0, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, false); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, Some(2)); + assert_eq!(verified_message.delegate_signed_taker_pubkey, None); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert_eq!(verified_message.max_margin_ratio.unwrap(), 65535); + assert_eq!(verified_message.builder_idx.unwrap(), 1); + assert_eq!(verified_message.builder_fee_tenth_bps.unwrap(), 58); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 3456000000u64); + assert_eq!(tp.trigger_price, 240000000u64); + + assert!(verified_message.stop_loss_order_params.is_none()); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 3); + assert_eq!(order_params.direction, PositionDirection::Long); + assert_eq!(order_params.base_asset_amount, 3456000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(230000000i64)); + assert_eq!(order_params.auction_end_price, Some(237000000i64)); + } } diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index 2dd95c417a..098e37dde2 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -394,3 +394,29 @@ export function getIfRebalanceConfigPublicKey( programId )[0]; } + +export function getRevenueShareAccountPublicKey( + programId: PublicKey, + authority: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('REV_SHARE')), + authority.toBuffer(), + ], + programId + )[0]; +} + +export function getRevenueShareEscrowAccountPublicKey( + programId: PublicKey, + authority: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('REV_ESCROW')), + authority.toBuffer(), + ], + programId + )[0]; +} diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index f6b5b96f41..efca781880 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4780,4 +4780,59 @@ export class AdminClient extends DriftClient { } ); } + + public async updateFeatureBitFlagsBuilderCodes( + enable: boolean + ): Promise { + const updateFeatureBitFlagsBuilderCodesIx = + await this.getUpdateFeatureBitFlagsBuilderCodesIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsBuilderCodesIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsBuilderCodesIx( + enable: boolean + ): Promise { + return this.program.instruction.updateFeatureBitFlagsBuilderCodes(enable, { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + }); + } + + public async updateFeatureBitFlagsBuilderReferral( + enable: boolean + ): Promise { + const updateFeatureBitFlagsBuilderReferralIx = + await this.getUpdateFeatureBitFlagsBuilderReferralIx(enable); + + const tx = await this.buildTransaction( + updateFeatureBitFlagsBuilderReferralIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsBuilderReferralIx( + enable: boolean + ): Promise { + return this.program.instruction.updateFeatureBitFlagsBuilderReferral( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 4d39ae6d0f..596598f9bd 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -113,6 +113,8 @@ import { getUserStatsAccountPublicKey, getSignedMsgWsDelegatesAccountPublicKey, getIfRebalanceConfigPublicKey, + getRevenueShareAccountPublicKey, + getRevenueShareEscrowAccountPublicKey, } from './addresses/pda'; import { DataAndSlot, @@ -195,6 +197,12 @@ import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; import { Commitment } from 'gill'; import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; +import { hasBuilder } from './math/orders'; +import { RevenueShareEscrowMap } from './userMap/revenueShareEscrowMap'; +import { + isBuilderOrderReferral, + isBuilderOrderCompleted, +} from './math/builder'; type RemainingAccountParams = { userAccounts: UserAccount[]; @@ -1232,6 +1240,176 @@ export class DriftClient { return ix; } + public async initializeRevenueShare( + authority: PublicKey, + txParams?: TxParams + ): Promise { + const ix = await this.getInitializeRevenueShareIx(authority); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getInitializeRevenueShareIx( + authority: PublicKey + ): Promise { + const revenueShare = getRevenueShareAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.initializeRevenueShare({ + accounts: { + revenueShare, + authority, + payer: this.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async initializeRevenueShareEscrow( + authority: PublicKey, + numOrders: number, + txParams?: TxParams + ): Promise { + const ix = await this.getInitializeRevenueShareEscrowIx( + authority, + numOrders + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getInitializeRevenueShareEscrowIx( + authority: PublicKey, + numOrders: number + ): Promise { + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.initializeRevenueShareEscrow(numOrders, { + accounts: { + escrow, + authority, + payer: this.wallet.publicKey, + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + state: await this.getStatePublicKey(), + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async migrateReferrer( + authority: PublicKey, + txParams?: TxParams + ): Promise { + const ix = await this.getMigrateReferrerIx(authority); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getMigrateReferrerIx( + authority: PublicKey + ): Promise { + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.migrateReferrer({ + accounts: { + escrow, + authority, + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + state: await this.getStatePublicKey(), + payer: this.wallet.publicKey, + }, + }); + } + + public async resizeRevenueShareEscrowOrders( + authority: PublicKey, + numOrders: number, + txParams?: TxParams + ): Promise { + const ix = await this.getResizeRevenueShareEscrowOrdersIx( + authority, + numOrders + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getResizeRevenueShareEscrowOrdersIx( + authority: PublicKey, + numOrders: number + ): Promise { + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.resizeRevenueShareEscrowOrders(numOrders, { + accounts: { + escrow, + authority, + payer: this.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async changeApprovedBuilder( + builder: PublicKey, + maxFeeTenthBps: number, + add: boolean, + txParams?: TxParams + ): Promise { + const ix = await this.getChangeApprovedBuilderIx( + builder, + maxFeeTenthBps, + add + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getChangeApprovedBuilderIx( + builder: PublicKey, + maxFeeTenthBps: number, + add: boolean + ): Promise { + const authority = this.wallet.publicKey; + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.changeApprovedBuilder( + builder, + maxFeeTenthBps, + add, + { + accounts: { + escrow, + authority, + payer: this.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }, + } + ); + } + public async addSignedMsgWsDelegate( authority: PublicKey, delegate: PublicKey, @@ -1959,6 +2137,20 @@ export class DriftClient { writableSpotMarketIndexes, }); + for (const order of userAccount.orders) { + if (hasBuilder(order)) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + break; + } + } + const tokenPrograms = new Set(); for (const spotPosition of userAccount.spotPositions) { if (isSpotPositionAvailable(spotPosition)) { @@ -2460,6 +2652,35 @@ export class DriftClient { } } + addBuilderToRemainingAccounts( + builders: PublicKey[], + remainingAccounts: AccountMeta[] + ): void { + for (const builder of builders) { + // Add User account for the builder + const builderUserAccount = getUserAccountPublicKeySync( + this.program.programId, + builder, + 0 // subAccountId 0 for builder user account + ); + remainingAccounts.push({ + pubkey: builderUserAccount, + isSigner: false, + isWritable: true, + }); + + const builderAccount = getRevenueShareAccountPublicKey( + this.program.programId, + builder + ); + remainingAccounts.push({ + pubkey: builderAccount, + isSigner: false, + isWritable: true, + }); + } + } + getRemainingAccountMapsForUsers(userAccounts: UserAccount[]): { oracleAccountMap: Map; spotMarketAccountMap: Map; @@ -4732,7 +4953,8 @@ export class DriftClient { referrerInfo?: ReferrerInfo, txParams?: TxParams, fillerSubAccountId?: number, - fillerAuthority?: PublicKey + fillerAuthority?: PublicKey, + hasBuilderFee?: boolean ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( @@ -4744,7 +4966,8 @@ export class DriftClient { referrerInfo, fillerSubAccountId, undefined, - fillerAuthority + fillerAuthority, + hasBuilderFee ), txParams ), @@ -4762,7 +4985,8 @@ export class DriftClient { referrerInfo?: ReferrerInfo, fillerSubAccountId?: number, isSignedMsg?: boolean, - fillerAuthority?: PublicKey + fillerAuthority?: PublicKey, + hasBuilderFee?: boolean ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, @@ -4844,6 +5068,36 @@ export class DriftClient { } } + let withBuilder = false; + if (hasBuilderFee) { + withBuilder = true; + } else { + // figure out if we need builder account or not + if (order && !isSignedMsg) { + const userOrder = userAccount.orders.find( + (o) => o.orderId === order.orderId + ); + if (userOrder) { + withBuilder = hasBuilder(userOrder); + } + } else if (isSignedMsg) { + // Order hasn't been placed yet, we cant tell if it has a builder or not. + // Include it optimistically + withBuilder = true; + } + } + + if (withBuilder) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + const orderId = isSignedMsg ? null : order.orderId; return await this.program.instruction.fillPerpOrder(orderId, null, { accounts: { @@ -6463,7 +6717,26 @@ export class DriftClient { }); } + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + const takerOrderId = takerInfo.order.orderId; + if (hasBuilder(takerInfo.order)) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } return await this.program.instruction.placeAndMakePerpOrder( orderParams, takerOrderId, @@ -6549,16 +6822,29 @@ export class DriftClient { ? 'global' + ':' + 'SignedMsgOrderParamsDelegateMessage' : 'global' + ':' + 'SignedMsgOrderParamsMessage'; const prefix = Buffer.from(sha256(anchorIxName).slice(0, 8)); + + // Backwards-compat: normalize optional builder fields to null for encoding + const withBuilderDefaults = { + ...orderParamsMessage, + builderIdx: + orderParamsMessage.builderIdx !== undefined + ? orderParamsMessage.builderIdx + : null, + builderFeeTenthBps: + orderParamsMessage.builderFeeTenthBps !== undefined + ? orderParamsMessage.builderFeeTenthBps + : null, + }; const buf = Buffer.concat([ prefix, delegateSigner ? this.program.coder.types.encode( 'SignedMsgOrderParamsDelegateMessage', - orderParamsMessage as SignedMsgOrderParamsDelegateMessage + withBuilderDefaults as SignedMsgOrderParamsDelegateMessage ) : this.program.coder.types.encode( 'SignedMsgOrderParamsMessage', - orderParamsMessage as SignedMsgOrderParamsMessage + withBuilderDefaults as SignedMsgOrderParamsMessage ), ]); return buf; @@ -6647,20 +6933,30 @@ export class DriftClient { signedSignedMsgOrderParams.orderParams.toString(), 'hex' ); - try { - const { signedMsgOrderParams } = this.decodeSignedMsgOrderParamsMessage( - borshBuf, - isDelegateSigner - ); - if (isUpdateHighLeverageMode(signedMsgOrderParams.bitFlags)) { - remainingAccounts.push({ - pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), - isWritable: true, - isSigner: false, - }); - } - } catch (err) { - console.error('invalid signed order encoding'); + + const signedMessage = this.decodeSignedMsgOrderParamsMessage( + borshBuf, + isDelegateSigner + ); + if (isUpdateHighLeverageMode(signedMessage.signedMsgOrderParams.bitFlags)) { + remainingAccounts.push({ + pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), + isWritable: true, + isSigner: false, + }); + } + if ( + signedMessage.builderFeeTenthBps !== null && + signedMessage.builderIdx !== null + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); } const messageLengthBuffer = Buffer.alloc(2); @@ -6791,6 +7087,32 @@ export class DriftClient { }); } + const isDelegateSigner = takerInfo.signingAuthority.equals( + takerInfo.takerUserAccount.delegate + ); + const borshBuf = Buffer.from( + signedSignedMsgOrderParams.orderParams.toString(), + 'hex' + ); + + const signedMessage = this.decodeSignedMsgOrderParamsMessage( + borshBuf, + isDelegateSigner + ); + if ( + signedMessage.builderFeeTenthBps !== null && + signedMessage.builderIdx !== null + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + const placeAndMakeIx = await this.program.instruction.placeAndMakeSignedMsgPerpOrder( orderParams, @@ -7447,7 +7769,8 @@ export class DriftClient { settleeUserAccount: UserAccount, marketIndex: number, txParams?: TxParams, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + escrowMap?: RevenueShareEscrowMap ): Promise { const lookupTableAccounts = await this.fetchAllLookupTableAccounts(); @@ -7456,7 +7779,8 @@ export class DriftClient { await this.settlePNLIx( settleeUserAccountPublicKey, settleeUserAccount, - marketIndex + marketIndex, + escrowMap ), txParams, undefined, @@ -7474,7 +7798,8 @@ export class DriftClient { public async settlePNLIx( settleeUserAccountPublicKey: PublicKey, settleeUserAccount: UserAccount, - marketIndex: number + marketIndex: number, + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], @@ -7482,6 +7807,89 @@ export class DriftClient { writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); + if (revenueShareEscrowMap) { + const escrow = revenueShareEscrowMap.get( + settleeUserAccount.authority.toBase58() + ); + if (escrow) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + + const builders = new Map(); + for (const order of escrow.orders) { + const eligibleBuilder = + isBuilderOrderCompleted(order) && + !isBuilderOrderReferral(order) && + order.feesAccrued.gt(ZERO) && + order.marketIndex === marketIndex; + if (eligibleBuilder && !builders.has(order.builderIdx)) { + builders.set( + order.builderIdx, + escrow.approvedBuilders[order.builderIdx].authority + ); + } + } + if (builders.size > 0) { + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + this.addBuilderToRemainingAccounts( + Array.from(builders.values()), + remainingAccounts + ); + } + + // Include escrow and referrer accounts if referral rewards exist for this market + const hasReferralForMarket = escrow.orders.some( + (o) => + isBuilderOrderReferral(o) && + o.feesAccrued.gt(ZERO) && + o.marketIndex === marketIndex + ); + + if (hasReferralForMarket) { + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + if (!escrow.referrer.equals(PublicKey.default)) { + this.addBuilderToRemainingAccounts( + [escrow.referrer], + remainingAccounts + ); + } + } + } else { + // Stale-cache fallback: if the user has any builder orders, include escrow PDA. This allows + // the program to lazily clean up any completed builder orders. + for (const order of settleeUserAccount.orders) { + if (hasBuilder(order)) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + break; + } + } + } + } + return await this.program.instruction.settlePnl(marketIndex, { accounts: { state: await this.getStatePublicKey(), @@ -7498,6 +7906,7 @@ export class DriftClient { settleeUserAccount: UserAccount, marketIndexes: number[], mode: SettlePnlMode, + revenueShareEscrowMap?: RevenueShareEscrowMap, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( @@ -7506,7 +7915,9 @@ export class DriftClient { settleeUserAccountPublicKey, settleeUserAccount, marketIndexes, - mode + mode, + undefined, + revenueShareEscrowMap ), txParams ), @@ -7522,7 +7933,8 @@ export class DriftClient { marketIndexes: number[], mode: SettlePnlMode, txParams?: TxParams, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { // need multiple TXs because settling more than 4 markets won't fit in a single TX const txsToSign: (Transaction | VersionedTransaction)[] = []; @@ -7536,7 +7948,9 @@ export class DriftClient { settleeUserAccountPublicKey, settleeUserAccount, marketIndexes, - mode + mode, + undefined, + revenueShareEscrowMap ); const computeUnits = Math.min(300_000 * marketIndexes.length, 1_400_000); const tx = await this.buildTransaction( @@ -7581,7 +7995,8 @@ export class DriftClient { mode: SettlePnlMode, overrides?: { authority?: PublicKey; - } + }, + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], @@ -7589,6 +8004,95 @@ export class DriftClient { writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); + if (revenueShareEscrowMap) { + const escrow = revenueShareEscrowMap.get( + settleeUserAccount.authority.toBase58() + ); + const builders = new Map(); + if (escrow) { + for (const order of escrow.orders) { + const eligibleBuilder = + isBuilderOrderCompleted(order) && + !isBuilderOrderReferral(order) && + order.feesAccrued.gt(ZERO) && + marketIndexes.includes(order.marketIndex); + if (eligibleBuilder && !builders.has(order.builderIdx)) { + builders.set( + order.builderIdx, + escrow.approvedBuilders[order.builderIdx].authority + ); + } + } + if (builders.size > 0) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + this.addBuilderToRemainingAccounts( + Array.from(builders.values()), + remainingAccounts + ); + } + + // Include escrow and referrer accounts when there are referral rewards + // for any of the markets we are settling, so on-chain sweep can find them. + const hasReferralForRequestedMarkets = escrow.orders.some( + (o) => + isBuilderOrderReferral(o) && + o.feesAccrued.gt(ZERO) && + marketIndexes.includes(o.marketIndex) + ); + + if (hasReferralForRequestedMarkets) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + + // Add referrer's User and RevenueShare accounts + if (!escrow.referrer.equals(PublicKey.default)) { + this.addBuilderToRemainingAccounts( + [escrow.referrer], + remainingAccounts + ); + } + } + } else { + // Stale-cache fallback: if the user has any builder orders, include escrow PDA. This allows + // the program to lazily clean up any completed builder orders. + for (const order of settleeUserAccount.orders) { + if (hasBuilder(order)) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + break; + } + } + } + } + return await this.program.instruction.settleMultiplePnls( marketIndexes, mode, diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 7b9b4293a8..8400a22666 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -7382,6 +7382,174 @@ "type": "bool" } ] + }, + { + "name": "updateFeatureBitFlagsBuilderCodes", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "initializeRevenueShare", + "accounts": [ + { + "name": "revenueShare", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeRevenueShareEscrow", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "numOrders", + "type": "u16" + } + ] + }, + { + "name": "resizeRevenueShareEscrowOrders", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "numOrders", + "type": "u16" + } + ] + }, + { + "name": "changeApprovedBuilder", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "builder", + "type": "publicKey" + }, + { + "name": "maxFeeBps", + "type": "u16" + }, + { + "name": "add", + "type": "bool" + } + ] } ], "accounts": [ @@ -8234,6 +8402,106 @@ ] } }, + { + "name": "RevenueShare", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "the owner of this account, a builder or referrer" + ], + "type": "publicKey" + }, + { + "name": "totalReferrerRewards", + "type": "u64" + }, + { + "name": "totalBuilderRewards", + "type": "u64" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 18 + ] + } + } + ] + } + }, + { + "name": "RevenueShareEscrow", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "the owner of this account, a user" + ], + "type": "publicKey" + }, + { + "name": "referrer", + "type": "publicKey" + }, + { + "name": "referrerBoostExpireTs", + "type": "u32" + }, + { + "name": "referrerRewardOffset", + "type": "i8" + }, + { + "name": "refereeFeeNumeratorOffset", + "type": "i8" + }, + { + "name": "referrerBoostNumerator", + "type": "i8" + }, + { + "name": "reservedFixed", + "type": { + "array": [ + "u8", + 17 + ] + } + }, + { + "name": "padding0", + "type": "u32" + }, + { + "name": "orders", + "type": { + "vec": { + "defined": "RevenueShareOrder" + } + } + }, + { + "name": "padding1", + "type": "u32" + }, + { + "name": "approvedBuilders", + "type": { + "vec": { + "defined": "BuilderInfo" + } + } + } + ] + } + }, { "name": "SignedMsgUserOrders", "docs": [ @@ -10028,6 +10296,18 @@ "type": { "option": "u16" } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFeeTenthBps", + "type": { + "option": "u16" + } } ] } @@ -10081,6 +10361,18 @@ "type": { "option": "u16" } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFeeTenthBps", + "type": { + "option": "u16" + } } ] } @@ -10933,6 +11225,157 @@ ] } }, + { + "name": "RevenueShareOrder", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feesAccrued", + "docs": [ + "fees accrued so far for this order slot. This is not exclusively fees from this order_id", + "and may include fees from other orders in the same market. This may be swept to the", + "builder's SpotPosition during settle_pnl." + ], + "type": "u64" + }, + { + "name": "orderId", + "docs": [ + "the order_id of the current active order in this slot. It's only relevant while bit_flag = Open" + ], + "type": "u32" + }, + { + "name": "feeTenthBps", + "docs": [ + "the builder fee on this order, in tenths of a bps, e.g. 100 = 0.01%" + ], + "type": "u16" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "subAccountId", + "docs": [ + "the subaccount_id of the user who created this order. It's only relevant while bit_flag = Open" + ], + "type": "u16" + }, + { + "name": "builderIdx", + "docs": [ + "the index of the RevenueShareEscrow.approved_builders list, that this order's fee will settle to. Ignored", + "if bit_flag = Referral." + ], + "type": "u8" + }, + { + "name": "bitFlags", + "docs": [ + "bitflags that describe the state of the order.", + "[`RevenueShareOrderBitFlag::Init`]: this order slot is available for use.", + "[`RevenueShareOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order.", + "[`RevenueShareOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into.", + "the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders.", + "[`RevenueShareOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled for this market.", + "If it is set, no other bitflag should be set." + ], + "type": "u8" + }, + { + "name": "userOrderIndex", + "docs": [ + "the index into the User's orders list when this RevenueShareOrder was created, make sure to verify that order_id matches." + ], + "type": "u8" + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 10 + ] + } + } + ] + } + }, + { + "name": "BuilderInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "maxFeeTenthBps", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 6 + ] + } + } + ] + } + }, + { + "name": "RevenueShareEscrowFixed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "referrer", + "type": "publicKey" + }, + { + "name": "referrerBoostExpireTs", + "type": "u32" + }, + { + "name": "referrerRewardOffset", + "type": "i8" + }, + { + "name": "refereeFeeNumeratorOffset", + "type": "i8" + }, + { + "name": "referrerBoostNumerator", + "type": "i8" + }, + { + "name": "reservedFixed", + "type": { + "array": [ + "u8", + 17 + ] + } + } + ] + } + }, { "name": "SignedMsgOrderId", "type": { @@ -12498,6 +12941,26 @@ ] } }, + { + "name": "RevenueShareOrderBitFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Init" + }, + { + "name": "Open" + }, + { + "name": "Completed" + }, + { + "name": "Referral" + } + ] + } + }, { "name": "SettlePnlMode", "type": { @@ -12619,6 +13082,12 @@ }, { "name": "MedianTriggerPrice" + }, + { + "name": "BuilderCodes" + }, + { + "name": "BuilderReferral" } ] } @@ -12753,6 +13222,9 @@ }, { "name": "NewTriggerReduceOnly" + }, + { + "name": "HasBuilder" } ] } @@ -12767,6 +13239,9 @@ }, { "name": "IsReferred" + }, + { + "name": "BuilderReferral" } ] } @@ -14273,6 +14748,62 @@ } ] }, + { + "name": "RevenueShareSettleRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "builder", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "referrer", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "feeSettled", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + }, + "index": false + }, + { + "name": "builderSubAccountId", + "type": "u16", + "index": false + }, + { + "name": "builderTotalReferrerRewards", + "type": "u64", + "index": false + }, + { + "name": "builderTotalBuilderRewards", + "type": "u64", + "index": false + } + ] + }, { "name": "LPSettleRecord", "fields": [ @@ -16124,6 +16655,46 @@ "code": 6316, "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" + }, + { + "code": 6317, + "name": "InvalidRevenueShareResize", + "msg": "Invalid RevenueShare resize" + }, + { + "code": 6318, + "name": "BuilderRevoked", + "msg": "Builder has been revoked" + }, + { + "code": 6319, + "name": "InvalidBuilderFee", + "msg": "Builder fee is greater than max fee bps" + }, + { + "code": 6320, + "name": "RevenueShareEscrowAuthorityMismatch", + "msg": "RevenueShareEscrow authority mismatch" + }, + { + "code": 6321, + "name": "RevenueShareEscrowOrdersAccountFull", + "msg": "RevenueShareEscrow has too many active orders" + }, + { + "code": 6322, + "name": "InvalidRevenueShareAccount", + "msg": "Invalid RevenueShareAccount" + }, + { + "code": 6323, + "name": "CannotRevokeBuilderWithOpenOrders", + "msg": "Cannot revoke builder with open orders" + }, + { + "code": 6324, + "name": "UnableToLoadRevenueShareAccount", + "msg": "Unable to load builder account" } ], "metadata": { diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 7f30e2afa0..0a261c0030 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -121,6 +121,7 @@ export * from './dlob/orderBookLevels'; export * from './userMap/userMap'; export * from './userMap/referrerMap'; export * from './userMap/userStatsMap'; +export * from './userMap/revenueShareEscrowMap'; export * from './userMap/userMapConfig'; export * from './math/bankruptcy'; export * from './orderSubscriber'; diff --git a/sdk/src/math/builder.ts b/sdk/src/math/builder.ts new file mode 100644 index 0000000000..75681a1823 --- /dev/null +++ b/sdk/src/math/builder.ts @@ -0,0 +1,20 @@ +import { RevenueShareOrder } from '../types'; + +const FLAG_IS_OPEN = 0x01; +export function isBuilderOrderOpen(order: RevenueShareOrder): boolean { + return (order.bitFlags & FLAG_IS_OPEN) !== 0; +} + +const FLAG_IS_COMPLETED = 0x02; +export function isBuilderOrderCompleted(order: RevenueShareOrder): boolean { + return (order.bitFlags & FLAG_IS_COMPLETED) !== 0; +} + +const FLAG_IS_REFERRAL = 0x04; +export function isBuilderOrderReferral(order: RevenueShareOrder): boolean { + return (order.bitFlags & FLAG_IS_REFERRAL) !== 0; +} + +export function isBuilderOrderAvailable(order: RevenueShareOrder): boolean { + return !isBuilderOrderOpen(order) && !isBuilderOrderCompleted(order); +} diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index 5bfa6370bd..de5486694b 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -389,6 +389,11 @@ export function isSignedMsgOrder(order: Order): boolean { return (order.bitFlags & FLAG_IS_SIGNED_MSG) !== 0; } +const FLAG_HAS_BUILDER = 0x10; +export function hasBuilder(order: Order): boolean { + return (order.bitFlags & FLAG_HAS_BUILDER) !== 0; +} + export function calculateOrderBaseAssetAmount( order: Order, existingBaseAssetAmount: BN diff --git a/sdk/src/math/state.ts b/sdk/src/math/state.ts index f4414214a9..0565f959ba 100644 --- a/sdk/src/math/state.ts +++ b/sdk/src/math/state.ts @@ -38,3 +38,11 @@ export function useMedianTriggerPrice(stateAccount: StateAccount): boolean { (stateAccount.featureBitFlags & FeatureBitFlags.MEDIAN_TRIGGER_PRICE) > 0 ); } + +export function builderCodesEnabled(stateAccount: StateAccount): boolean { + return (stateAccount.featureBitFlags & FeatureBitFlags.BUILDER_CODES) > 0; +} + +export function builderReferralEnabled(stateAccount: StateAccount): boolean { + return (stateAccount.featureBitFlags & FeatureBitFlags.BUILDER_REFERRAL) > 0; +} diff --git a/sdk/src/memcmp.ts b/sdk/src/memcmp.ts index 896971ebbf..3ecdeb9a9c 100644 --- a/sdk/src/memcmp.ts +++ b/sdk/src/memcmp.ts @@ -129,3 +129,14 @@ export function getSpotMarketAccountsFilter(): MemcmpFilter { }, }; } + +export function getRevenueShareEscrowFilter(): MemcmpFilter { + return { + memcmp: { + offset: 0, + bytes: bs58.encode( + BorshAccountsCoder.accountDiscriminator('RevenueShareEscrow') + ), + }, + }; +} diff --git a/sdk/src/swift/swiftOrderSubscriber.ts b/sdk/src/swift/swiftOrderSubscriber.ts index 540691521d..eac7e3893c 100644 --- a/sdk/src/swift/swiftOrderSubscriber.ts +++ b/sdk/src/swift/swiftOrderSubscriber.ts @@ -201,9 +201,7 @@ export class SwiftOrderSubscriber { ).slice(0, 8) ) ); - const signedMessage: - | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage = + const signedMessage = this.driftClient.decodeSignedMsgOrderParamsMessage( signedMsgOrderParamsBuf, isDelegateSigner @@ -281,13 +279,10 @@ export class SwiftOrderSubscriber { ).slice(0, 8) ) ); - const signedMessage: - | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage = - this.driftClient.decodeSignedMsgOrderParamsMessage( - signedMsgOrderParamsBuf, - isDelegateSigner - ); + const signedMessage = this.driftClient.decodeSignedMsgOrderParamsMessage( + signedMsgOrderParamsBuf, + isDelegateSigner + ); const takerAuthority = new PublicKey(orderMessageRaw.taker_authority); const signingAuthority = new PublicKey(orderMessageRaw.signing_authority); diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 2d49e61b58..d48b51b9bf 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -31,6 +31,8 @@ export enum ExchangeStatus { export enum FeatureBitFlags { MM_ORACLE_UPDATE = 1, MEDIAN_TRIGGER_PRICE = 2, + BUILDER_CODES = 4, + BUILDER_REFERRAL = 8, } export class MarketStatus { @@ -1310,6 +1312,8 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + builderIdx?: number | null; + builderFeeTenthBps?: number | null; }; export type SignedMsgOrderParamsDelegateMessage = { @@ -1320,6 +1324,8 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + builderIdx?: number | null; + builderFeeTenthBps?: number | null; }; export type SignedMsgTriggerOrderParams = { @@ -1638,3 +1644,51 @@ export type SignedMsgUserOrdersAccount = { authorityPubkey: PublicKey; signedMsgOrderData: SignedMsgOrderId[]; }; + +export type RevenueShareAccount = { + authority: PublicKey; + totalReferrerRewards: BN; + totalBuilderRewards: BN; + padding: number[]; +}; + +export type RevenueShareEscrowAccount = { + authority: PublicKey; + referrer: PublicKey; + referrerBoostExpireTs: number; + referrerRewardOffset: number; + refereeFeeNumeratorOffset: number; + referrerBoostNumerator: number; + reservedFixed: number[]; + orders: RevenueShareOrder[]; + approvedBuilders: BuilderInfo[]; +}; + +export type RevenueShareOrder = { + builderIdx: number; + feesAccrued: BN; + orderId: number; + feeTenthBps: number; + marketIndex: number; + bitFlags: number; + marketType: MarketType; // 0: spot, 1: perp + padding: number[]; +}; + +export type BuilderInfo = { + authority: PublicKey; + maxFeeTenthBps: number; + padding: number[]; +}; + +export type RevenueShareSettleRecord = { + ts: number; + builder: PublicKey | null; + referrer: PublicKey | null; + feeSettled: BN; + marketIndex: number; + marketType: MarketType; + builderTotalReferrerRewards: BN; + builderTotalBuilderRewards: BN; + builderSubAccountId: number; +}; diff --git a/sdk/src/userMap/revenueShareEscrowMap.ts b/sdk/src/userMap/revenueShareEscrowMap.ts new file mode 100644 index 0000000000..fb56628a23 --- /dev/null +++ b/sdk/src/userMap/revenueShareEscrowMap.ts @@ -0,0 +1,306 @@ +import { PublicKey, RpcResponseAndContext } from '@solana/web3.js'; +import { DriftClient } from '../driftClient'; +import { RevenueShareEscrowAccount } from '../types'; +import { getRevenueShareEscrowAccountPublicKey } from '../addresses/pda'; +import { getRevenueShareEscrowFilter } from '../memcmp'; + +export class RevenueShareEscrowMap { + /** + * map from authority pubkey to RevenueShareEscrow account data. + */ + private authorityEscrowMap = new Map(); + private driftClient: DriftClient; + private parallelSync: boolean; + + private fetchPromise?: Promise; + private fetchPromiseResolver: () => void; + + /** + * Creates a new RevenueShareEscrowMap instance. + * + * @param {DriftClient} driftClient - The DriftClient instance. + * @param {boolean} parallelSync - Whether to sync accounts in parallel. + */ + constructor(driftClient: DriftClient, parallelSync?: boolean) { + this.driftClient = driftClient; + this.parallelSync = parallelSync !== undefined ? parallelSync : true; + } + + /** + * Subscribe to all RevenueShareEscrow accounts. + */ + public async subscribe() { + if (this.size() > 0) { + return; + } + + await this.driftClient.subscribe(); + await this.sync(); + } + + public has(authorityPublicKey: string): boolean { + return this.authorityEscrowMap.has(authorityPublicKey); + } + + public get( + authorityPublicKey: string + ): RevenueShareEscrowAccount | undefined { + return this.authorityEscrowMap.get(authorityPublicKey); + } + + /** + * Enforce that a RevenueShareEscrow will exist for the given authorityPublicKey, + * reading one from the blockchain if necessary. + * @param authorityPublicKey + * @returns + */ + public async mustGet( + authorityPublicKey: string + ): Promise { + if (!this.has(authorityPublicKey)) { + await this.addRevenueShareEscrow(authorityPublicKey); + } + return this.get(authorityPublicKey); + } + + public async addRevenueShareEscrow(authority: string) { + const escrowAccountPublicKey = getRevenueShareEscrowAccountPublicKey( + this.driftClient.program.programId, + new PublicKey(authority) + ); + + try { + const accountInfo = await this.driftClient.connection.getAccountInfo( + escrowAccountPublicKey, + 'processed' + ); + + if (accountInfo && accountInfo.data) { + const escrow = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + accountInfo.data + ) as RevenueShareEscrowAccount; + + this.authorityEscrowMap.set(authority, escrow); + } + } catch (error) { + // RevenueShareEscrow account doesn't exist for this authority, which is normal + console.debug( + `No RevenueShareEscrow account found for authority: ${authority}` + ); + } + } + + public size(): number { + return this.authorityEscrowMap.size; + } + + public async sync(): Promise { + if (this.fetchPromise) { + return this.fetchPromise; + } + + this.fetchPromise = new Promise((resolver) => { + this.fetchPromiseResolver = resolver; + }); + + try { + await this.syncAll(); + } finally { + this.fetchPromiseResolver(); + this.fetchPromise = undefined; + } + } + + /** + * A slow, bankrun test friendly version of sync(), uses getAccountInfo on every cached account to refresh data + * @returns + */ + public async slowSync(): Promise { + if (this.fetchPromise) { + return this.fetchPromise; + } + for (const authority of this.authorityEscrowMap.keys()) { + const accountInfo = await this.driftClient.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + this.driftClient.program.programId, + new PublicKey(authority) + ), + 'confirmed' + ); + const escrowNew = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + accountInfo.data + ) as RevenueShareEscrowAccount; + this.authorityEscrowMap.set(authority, escrowNew); + } + } + + public async syncAll(): Promise { + const rpcRequestArgs = [ + this.driftClient.program.programId.toBase58(), + { + commitment: this.driftClient.opts.commitment, + filters: [getRevenueShareEscrowFilter()], + encoding: 'base64', + withContext: true, + }, + ]; + + const rpcJSONResponse: any = + // @ts-ignore + await this.driftClient.connection._rpcRequest( + 'getProgramAccounts', + rpcRequestArgs + ); + + const rpcResponseAndContext: RpcResponseAndContext< + Array<{ + pubkey: string; + account: { + data: [string, string]; + }; + }> + > = rpcJSONResponse.result; + + const batchSize = 100; + for (let i = 0; i < rpcResponseAndContext.value.length; i += batchSize) { + const batch = rpcResponseAndContext.value.slice(i, i + batchSize); + + if (this.parallelSync) { + await Promise.all( + batch.map(async (programAccount) => { + try { + // @ts-ignore + const buffer = Buffer.from( + programAccount.account.data[0], + programAccount.account.data[1] + ); + + const escrow = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + buffer + ) as RevenueShareEscrowAccount; + + // Extract authority from the account data + const authorityKey = escrow.authority.toBase58(); + this.authorityEscrowMap.set(authorityKey, escrow); + } catch (error) { + console.warn( + `Failed to decode RevenueShareEscrow account ${programAccount.pubkey}:`, + error + ); + } + }) + ); + } else { + for (const programAccount of batch) { + try { + // @ts-ignore + const buffer = Buffer.from( + programAccount.account.data[0], + programAccount.account.data[1] + ); + + const escrow = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + buffer + ) as RevenueShareEscrowAccount; + + // Extract authority from the account data + const authorityKey = escrow.authority.toBase58(); + this.authorityEscrowMap.set(authorityKey, escrow); + } catch (error) { + console.warn( + `Failed to decode RevenueShareEscrow account ${programAccount.pubkey}:`, + error + ); + } + } + } + + // Add a small delay between batches to avoid overwhelming the RPC + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + + /** + * Get all RevenueShareEscrow accounts + */ + public getAll(): Map { + return new Map(this.authorityEscrowMap); + } + + /** + * Get all authorities that have RevenueShareEscrow accounts + */ + public getAuthorities(): string[] { + return Array.from(this.authorityEscrowMap.keys()); + } + + /** + * Get RevenueShareEscrow accounts that have approved referrers + */ + public getEscrowsWithApprovedReferrers(): Map< + string, + RevenueShareEscrowAccount + > { + const result = new Map(); + for (const [authority, escrow] of this.authorityEscrowMap) { + if (escrow.approvedBuilders && escrow.approvedBuilders.length > 0) { + result.set(authority, escrow); + } + } + return result; + } + + /** + * Get RevenueShareEscrow accounts that have active orders + */ + public getEscrowsWithOrders(): Map { + const result = new Map(); + for (const [authority, escrow] of this.authorityEscrowMap) { + if (escrow.orders && escrow.orders.length > 0) { + result.set(authority, escrow); + } + } + return result; + } + + /** + * Get RevenueShareEscrow account by referrer + */ + public getByReferrer( + referrerPublicKey: string + ): RevenueShareEscrowAccount | undefined { + for (const escrow of this.authorityEscrowMap.values()) { + if (escrow.referrer.toBase58() === referrerPublicKey) { + return escrow; + } + } + return undefined; + } + + /** + * Get all RevenueShareEscrow accounts for a specific referrer + */ + public getAllByReferrer( + referrerPublicKey: string + ): RevenueShareEscrowAccount[] { + const result: RevenueShareEscrowAccount[] = []; + for (const escrow of this.authorityEscrowMap.values()) { + if (escrow.referrer.toBase58() === referrerPublicKey) { + result.push(escrow); + } + } + return result; + } + + public async unsubscribe() { + this.authorityEscrowMap.clear(); + } +} diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index e8ef72b4ea..78b954d28f 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -22,6 +22,7 @@ test_files=( # updateK.ts # postOnlyAmmFulfillment.ts # TODO BROKEN ^^ + builderCodes.ts decodeUser.ts fuel.ts fuelSweep.ts diff --git a/test-scripts/run-til-failure.sh b/test-scripts/run-til-failure.sh new file mode 100644 index 0000000000..d832743171 --- /dev/null +++ b/test-scripts/run-til-failure.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +count=0 +trap 'echo -e "\nStopped after $count runs"; exit 0' INT + +while true; do + if ! bash test-scripts/single-anchor-test.sh --skip-build; then + echo "Test failed after $count successful runs!" + exit 1 + fi + count=$((count + 1)) + echo "Test passed ($count), running again..." +done diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index a9f48da728..8b0fc7f5bd 100755 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -7,7 +7,8 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json test_files=( - spotDepositWithdraw22ScaledUI.ts + builderCodes.ts + # placeAndMakeSignedMsgBankrun.ts ) for test_file in ${test_files[@]}; do diff --git a/tests/builderCodes.ts b/tests/builderCodes.ts new file mode 100644 index 0000000000..4f26cd0476 --- /dev/null +++ b/tests/builderCodes.ts @@ -0,0 +1,1612 @@ +import * as anchor from '@coral-xyz/anchor'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; + +import { + TestClient, + OracleSource, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + assert, + getRevenueShareAccountPublicKey, + getRevenueShareEscrowAccountPublicKey, + RevenueShareAccount, + RevenueShareEscrowAccount, + BASE_PRECISION, + BN, + PRICE_PRECISION, + getMarketOrderParams, + PositionDirection, + PostOnlyParams, + MarketType, + OrderParams, + PEG_PRECISION, + ZERO, + isVariant, + hasBuilder, + parseLogs, + RevenueShareEscrowMap, + getTokenAmount, + RevenueShareSettleRecord, + getLimitOrderParams, + SignedMsgOrderParamsMessage, + QUOTE_PRECISION, +} from '../sdk/src'; + +import { + createUserWithUSDCAccount, + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + printTxLogs, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_STORAGE_DATA } from './pythLazerData'; +import { nanoid } from 'nanoid'; +import { + isBuilderOrderCompleted, + isBuilderOrderReferral, +} from '../sdk/src/math/builder'; +import { createTransferInstruction } from '@solana/spl-token'; + +dotenv.config(); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +function buildMsg( + marketIndex: number, + baseAssetAmount: BN, + userOrderId: number, + feeBps: number, + slot: BN +) { + const params = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + return { + signedMsgOrderParams: params, + subAccountId: 0, + slot, + uuid: Uint8Array.from(Buffer.from(nanoid(8))), + builderIdx: 0, + builderFeeTenthBps: feeBps, + takeProfitOrderParams: null, + stopLossOrderParams: null, + } as SignedMsgOrderParamsMessage; +} + +describe('builder codes', () => { + const chProgram = anchor.workspace.Drift as Program; + + let usdcMint: Keypair; + + let builderClient: TestClient; + let builderUSDCAccount: Keypair = null; + + let makerClient: TestClient; + let makerUSDCAccount: PublicKey = null; + + let userUSDCAccount: PublicKey = null; + let userClient: TestClient; + + // user without RevenueShareEscrow + let user2USDCAccount: PublicKey = null; + let user2Client: TestClient; + + let escrowMap: RevenueShareEscrowMap; + let bulkAccountLoader: TestBulkAccountLoader; + let bankrunContextWrapper: BankrunContextWrapper; + + let solUsd: PublicKey; + let marketIndexes; + let spotMarketIndexes; + let oracleInfos; + + const usdcAmount = new BN(10000 * 10 ** 6); + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 224.3); + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + marketIndexes = [0, 1]; + spotMarketIndexes = [0, 1]; + oracleInfos = [{ publicKey: solUsd, source: OracleSource.PYTH }]; + + builderClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: marketIndexes, + spotMarketIndexes: spotMarketIndexes, + subAccountIds: [], + oracleInfos, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await builderClient.initialize(usdcMint.publicKey, true); + await builderClient.subscribe(); + + await builderClient.updateFeatureBitFlagsBuilderCodes(true); + // await builderClient.updateFeatureBitFlagsBuilderReferral(true); + + await initializeQuoteSpotMarket(builderClient, usdcMint.publicKey); + + const periodicity = new BN(0); + await builderClient.initializePerpMarket( + 0, + solUsd, + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await builderClient.initializePerpMarket( + 1, + solUsd, + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + builderUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount.add(new BN(1e9).mul(QUOTE_PRECISION)), + bankrunContextWrapper, + builderClient.wallet.publicKey + ); + await builderClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + builderUSDCAccount.publicKey + ); + + // top up pnl pool for mkt 0 and mkt 1 + const spotMarket = builderClient.getSpotMarketAccount(0); + const pnlPoolTopupAmount = new BN(500).mul(QUOTE_PRECISION); + + const transferIx0 = createTransferInstruction( + builderUSDCAccount.publicKey, + spotMarket.vault, + builderClient.wallet.publicKey, + pnlPoolTopupAmount.toNumber() + ); + const tx0 = new Transaction().add(transferIx0); + tx0.recentBlockhash = ( + await bankrunContextWrapper.connection.getLatestBlockhash() + ).blockhash; + tx0.sign(builderClient.wallet.payer); + await bankrunContextWrapper.connection.sendTransaction(tx0); + + // top up pnl pool for mkt 1 + const transferIx1 = createTransferInstruction( + builderUSDCAccount.publicKey, + spotMarket.vault, + builderClient.wallet.publicKey, + pnlPoolTopupAmount.toNumber() + ); + const tx1 = new Transaction().add(transferIx1); + tx1.recentBlockhash = ( + await bankrunContextWrapper.connection.getLatestBlockhash() + ).blockhash; + tx1.sign(builderClient.wallet.payer); + await bankrunContextWrapper.connection.sendTransaction(tx1); + + await builderClient.updatePerpMarketPnlPool(0, pnlPoolTopupAmount); + await builderClient.updatePerpMarketPnlPool(1, pnlPoolTopupAmount); + + // await builderClient.depositIntoPerpMarketFeePool( + // 0, + // new BN(1e6).mul(QUOTE_PRECISION), + // builderUSDCAccount.publicKey + // ); + + [userClient, userUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + } + ); + await userClient.deposit( + usdcAmount, + 0, + userUSDCAccount, + undefined, + false, + undefined, + true + ); + + [user2Client, user2USDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + } + ); + await user2Client.deposit( + usdcAmount, + 0, + user2USDCAccount, + undefined, + false, + undefined, + true + ); + + [makerClient, makerUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + await makerClient.deposit( + usdcAmount, + 0, + makerUSDCAccount, + undefined, + false, + undefined, + true + ); + + escrowMap = new RevenueShareEscrowMap(userClient, false); + }); + + after(async () => { + await builderClient.unsubscribe(); + await userClient.unsubscribe(); + await user2Client.unsubscribe(); + await makerClient.unsubscribe(); + }); + + it('builder can create builder', async () => { + await builderClient.initializeRevenueShare(builderClient.wallet.publicKey); + + const builderAccountInfo = + await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + + const builderAcc: RevenueShareAccount = + builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + 'RevenueShare', + builderAccountInfo.data + ); + assert( + builderAcc.authority.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + assert(builderAcc.totalBuilderRewards.toNumber() === 0); + assert(builderAcc.totalReferrerRewards.toNumber() === 0); + }); + + it('user can initialize a RevenueShareEscrow', async () => { + const numOrders = 2; + + // Test the instruction creation + const ix = await userClient.getInitializeRevenueShareEscrowIx( + userClient.wallet.publicKey, + numOrders + ); + + assert(ix !== null, 'Instruction should be created'); + assert(ix.programId.toBase58() === userClient.program.programId.toBase58()); + + // Test the full transaction + await userClient.initializeRevenueShareEscrow( + userClient.wallet.publicKey, + numOrders + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + assert(accountInfo !== null, 'RevenueShareEscrow account should exist'); + assert( + accountInfo.owner.toBase58() === userClient.program.programId.toBase58() + ); + + const revShareEscrow: RevenueShareEscrowAccount = + builderClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + assert( + revShareEscrow.authority.toBase58() === + userClient.wallet.publicKey.toBase58() + ); + // assert( + // revShareEscrow.referrer.toBase58() === + // builderClient.wallet.publicKey.toBase58() + // ); + assert(revShareEscrow.orders.length === numOrders); + assert(revShareEscrow.approvedBuilders.length === 0); + }); + + it('user can resize RevenueShareEscrow account', async () => { + const newNumOrders = 10; + + // Test the instruction creation + const ix = await userClient.getResizeRevenueShareEscrowOrdersIx( + userClient.wallet.publicKey, + newNumOrders + ); + + assert(ix !== null, 'Instruction should be created'); + assert(ix.programId.toBase58() === userClient.program.programId.toBase58()); + + // Test the full transaction + await userClient.resizeRevenueShareEscrowOrders( + userClient.wallet.publicKey, + newNumOrders + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + assert( + accountInfo !== null, + 'RevenueShareEscrow account should exist after resize' + ); + assert( + accountInfo.owner.toBase58() === userClient.program.programId.toBase58() + ); + + const revShareEscrow: RevenueShareEscrowAccount = + builderClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + assert( + revShareEscrow.authority.toBase58() === + userClient.wallet.publicKey.toBase58() + ); + // assert( + // revShareEscrow.referrer.toBase58() === + // builderClient.wallet.publicKey.toBase58() + // ); + assert(revShareEscrow.orders.length === newNumOrders); + }); + + it('user can add/update/remove approved builder from RevenueShareEscrow', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; // 1.5% + + // First add a builder + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps, + true // add + ); + + // Verify the builder was added + let accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + let revShareEscrow: RevenueShareEscrowAccount = + userClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + const addedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + addedBuilder !== undefined, + 'Builder should be in approved builders list before removal' + ); + assert( + revShareEscrow.approvedBuilders.length === 1, + 'Approved builders list should contain 1 builder' + ); + assert( + addedBuilder.maxFeeTenthBps === maxFeeBps, + 'Builder should have correct max fee bps before removal' + ); + + // update the user fee + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps * 2, + true // update existing builder + ); + + // Verify the builder was updated + accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + revShareEscrow = userClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + const updatedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + updatedBuilder !== undefined, + 'Builder should be in approved builders list after update' + ); + assert( + updatedBuilder.maxFeeTenthBps === maxFeeBps * 2, + 'Builder should have correct max fee bps after update' + ); + + // Now remove the builder + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps, + false // remove + ); + + // Verify the builder was removed + accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + revShareEscrow = userClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + const removedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + removedBuilder.maxFeeTenthBps === 0, + 'Builder should have 0 max fee bps after removal' + ); + }); + + it('user with no RevenueShareEscrow can place and fill order with no builder', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount: baseAssetAmount.muln(2), + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + + let userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const takerOrderParamsMessage: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: takerOrderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: { + triggerPrice: new BN(235).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + stopLossOrderParams: { + triggerPrice: new BN(220).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + builderIdx: null, + builderFeeTenthBps: null, + }; + + const signedOrderParams = user2Client.signSignedMsgOrderParamsMessage( + takerOrderParamsMessage, + false + ); + + await builderClient.placeSignedMsgTakerOrder( + signedOrderParams, + marketIndex, + { + taker: await user2Client.getUserAccountPublicKey(), + takerUserAccount: user2Client.getUserAccount(), + takerStats: user2Client.getUserStatsAccountPublicKey(), + signingAuthority: user2Client.wallet.publicKey, + }, + undefined, + 2 + ); + + await user2Client.fetchAccounts(); + + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 3); + assert(userOrders[0].orderId === 1); + assert(userOrders[0].reduceOnly === true); + assert(hasBuilder(userOrders[0]) === false); + assert(userOrders[1].orderId === 2); + assert(userOrders[1].reduceOnly === true); + assert(hasBuilder(userOrders[1]) === false); + assert(userOrders[2].orderId === 3); + assert(userOrders[2].reduceOnly === false); + assert(hasBuilder(userOrders[2]) === false); + + await user2Client.fetchAccounts(); + + // fill order with vamm + await builderClient.fetchAccounts(); + const fillTx = await makerClient.fillPerpOrder( + await user2Client.getUserAccountPublicKey(), + user2Client.getUserAccount(), + { + marketIndex, + orderId: 3, + }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTx + ); + const events = parseLogs(builderClient.program, logs); + assert(events[0].name === 'OrderActionRecord'); + const fillQuoteAssetAmount = events[0].data['quoteAssetAmountFilled'] as BN; + const builderFee = events[0].data['builderFee'] as BN | null; + const takerFee = events[0].data['takerFee'] as BN; + const totalFeePaid = takerFee; + const referrerReward = new BN(events[0].data['referrerReward'] as number); + assert(builderFee === null); + assert(referrerReward.gt(ZERO)); + + await user2Client.fetchAccounts(); + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + await bankrunContextWrapper.moveTimeForward(100); + + // cancel remaining orders + await user2Client.cancelOrders(); + await user2Client.fetchAccounts(); + + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const perpPos = user2Client.getUser().getPerpPosition(0); + assert( + perpPos.quoteAssetAmount.eq(fillQuoteAssetAmount.add(totalFeePaid).neg()) + ); + + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBeforeSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfterSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + assert(builderUsdcAfterSettle.eq(builderUsdcBeforeSettle)); + }); + + it('user can place and fill order with builder', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + // approve builder again + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; // 1.5% + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps, + true // update existing builder + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount: baseAssetAmount.muln(2), + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + + // Should fail if we try first without encoding properly + + let userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const builderFeeBps = 7 * 10; + const takerOrderParamsMessage: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: takerOrderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: { + triggerPrice: new BN(235).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + stopLossOrderParams: { + triggerPrice: new BN(220).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + builderIdx: 0, + builderFeeTenthBps: builderFeeBps, + }; + + const signedOrderParams = userClient.signSignedMsgOrderParamsMessage( + takerOrderParamsMessage, + false + ); + + await builderClient.placeSignedMsgTakerOrder( + signedOrderParams, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + + await userClient.fetchAccounts(); + + // try to revoke builder with open orders + try { + await userClient.changeApprovedBuilder( + builder.publicKey, + 0, + false // remove + ); + assert( + false, + 'should throw error when revoking builder with open orders' + ); + } catch (e) { + assert(e.message.includes('0x18b3')); // CannotRevokeBuilderWithOpenOrders + } + + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 3); + assert(userOrders[0].orderId === 1); + assert(userOrders[0].reduceOnly === true); + assert(hasBuilder(userOrders[0]) === true); + assert(userOrders[1].orderId === 2); + assert(userOrders[1].reduceOnly === true); + assert(hasBuilder(userOrders[1]) === true); + assert(userOrders[2].orderId === 3); + assert(userOrders[2].reduceOnly === false); + assert(hasBuilder(userOrders[2]) === true); + + await escrowMap.slowSync(); + let escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + + // check the corresponding revShareEscrow orders are added + for (let i = 0; i < userOrders.length; i++) { + assert(escrow.orders[i]!.builderIdx === 0); + assert(escrow.orders[i]!.feesAccrued.eq(ZERO)); + assert( + escrow.orders[i]!.feeTenthBps === builderFeeBps, + `builderFeeBps ${escrow.orders[i]!.feeTenthBps} !== ${builderFeeBps}` + ); + assert( + escrow.orders[i]!.orderId === i + 1, + `orderId ${i} is ${escrow.orders[i]!.orderId}` + ); + assert(isVariant(escrow.orders[i]!.marketType, 'perp')); + assert(escrow.orders[i]!.marketIndex === marketIndex); + } + + assert(escrow.approvedBuilders[0]!.authority.equals(builder.publicKey)); + assert(escrow.approvedBuilders[0]!.maxFeeTenthBps === maxFeeBps); + + await userClient.fetchAccounts(); + + // fill order with vamm + await builderClient.fetchAccounts(); + const fillTx = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { + marketIndex, + orderId: 3, + }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTx + ); + const events = parseLogs(builderClient.program, logs); + assert(events[0].name === 'OrderActionRecord'); + const fillQuoteAssetAmount = events[0].data['quoteAssetAmountFilled'] as BN; + const builderFee = events[0].data['builderFee'] as BN; + const takerFee = events[0].data['takerFee'] as BN; + // const referrerReward = events[0].data['referrerReward'] as number; + assert( + builderFee.eq(fillQuoteAssetAmount.muln(builderFeeBps).divn(100000)) + ); + + await userClient.fetchAccounts(); + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + const pos = userClient.getUser().getPerpPosition(0); + const takerOrderCumulativeQuoteAssetAmountFilled = events[0].data[ + 'takerOrderCumulativeQuoteAssetAmountFilled' + ] as BN; + assert( + pos.quoteEntryAmount.abs().eq(takerOrderCumulativeQuoteAssetAmountFilled), + `pos.quoteEntryAmount ${pos.quoteEntryAmount.toNumber()} !== takerOrderCumulativeQuoteAssetAmountFilled ${takerOrderCumulativeQuoteAssetAmountFilled.toNumber()}` + ); + + const builderFeePaidBps = + (builderFee.toNumber() / Math.abs(pos.quoteEntryAmount.toNumber())) * + 10_000; + assert( + Math.round(builderFeePaidBps) === builderFeeBps / 10, + `builderFeePaidBps ${builderFeePaidBps} !== builderFeeBps ${ + builderFeeBps / 10 + }` + ); + + // expect 9.5 bps (taker fee - discount) + 7 bps (builder fee) + const takerFeePaidBps = + (takerFee.toNumber() / Math.abs(pos.quoteEntryAmount.toNumber())) * + 10_000; + assert( + Math.round(takerFeePaidBps * 10) === 165, + `takerFeePaidBps ${takerFeePaidBps} !== 16.5 bps` + ); + + await bankrunContextWrapper.moveTimeForward(100); + + await escrowMap.slowSync(); + escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + assert(escrow.orders[2].orderId === 3); + assert(escrow.orders[2].feesAccrued.gt(ZERO)); + assert(isBuilderOrderCompleted(escrow.orders[2])); + + // cancel remaining orders + await userClient.cancelOrders(); + await userClient.fetchAccounts(); + + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const perpPos = userClient.getUser().getPerpPosition(0); + assert( + perpPos.quoteAssetAmount.eq(fillQuoteAssetAmount.add(takerFee).neg()) + ); + + await escrowMap.slowSync(); + escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + assert(escrow.orders[2].bitFlags === 3); + assert(escrow.orders[2].feesAccrued.eq(builderFee)); + + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBeforeSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await userClient.fetchAccounts(); + const settleTx = await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + escrowMap + ); + + const settleLogs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + settleTx + ); + const settleEvents = parseLogs(builderClient.program, settleLogs); + const builderSettleEvents = settleEvents + .filter((e) => e.name === 'RevenueShareSettleRecord') + .map((e) => e.data) as RevenueShareSettleRecord[]; + + assert(builderSettleEvents.length === 1); + assert(builderSettleEvents[0].builder.equals(builder.publicKey)); + assert(builderSettleEvents[0].referrer == null); + assert(builderSettleEvents[0].feeSettled.eq(builderFee)); + assert(builderSettleEvents[0].marketIndex === marketIndex); + assert(isVariant(builderSettleEvents[0].marketType, 'perp')); + assert(builderSettleEvents[0].builderTotalReferrerRewards.eq(ZERO)); + assert(builderSettleEvents[0].builderTotalBuilderRewards.eq(builderFee)); + + // assert(builderSettleEvents[1].builder === null); + // assert(builderSettleEvents[1].referrer.equals(builder.publicKey)); + // assert(builderSettleEvents[1].feeSettled.eq(new BN(referrerReward))); + // assert(builderSettleEvents[1].marketIndex === marketIndex); + // assert(isVariant(builderSettleEvents[1].marketType, 'spot')); + // assert( + // builderSettleEvents[1].builderTotalReferrerRewards.eq( + // new BN(referrerReward) + // ) + // ); + // assert(builderSettleEvents[1].builderTotalBuilderRewards.eq(builderFee)); + + await escrowMap.slowSync(); + escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + for (const order of escrow.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfterSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + const finalBuilderFee = builderUsdcAfterSettle.sub(builderUsdcBeforeSettle); + // .sub(new BN(referrerReward)) + assert( + finalBuilderFee.eq(builderFee), + `finalBuilderFee ${finalBuilderFee.toString()} !== builderFee ${builderFee.toString()}` + ); + }); + + it('user can place and cancel with no fill (no fees accrued, escrow unchanged)', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; + await userClient.changeApprovedBuilder(builder.publicKey, maxFeeBps, true); + + await escrowMap.slowSync(); + const beforeEscrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const beforeTotalFees = beforeEscrow.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const orderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 7, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + const builderFeeBps = 5; + const msg: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: orderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: null, + stopLossOrderParams: null, + builderIdx: 0, + builderFeeTenthBps: builderFeeBps, + }; + + const signed = userClient.signSignedMsgOrderParamsMessage(msg, false); + await builderClient.placeSignedMsgTakerOrder( + signed, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + + await userClient.cancelOrders(); + await userClient.fetchAccounts(); + assert(userClient.getUser().getOpenOrders().length === 0); + + await escrowMap.slowSync(); + const afterEscrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const afterTotalFees = afterEscrow.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + assert(afterTotalFees.eq(beforeTotalFees)); + }); + + it('user can place and fill multiple orders (fees accumulate and settle)', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; + await userClient.changeApprovedBuilder(builder.publicKey, maxFeeBps, true); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + + await escrowMap.slowSync(); + const escrowStart = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const totalFeesInEscrowStart = escrowStart.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const feeBpsA = 6; + const feeBpsB = 9; + + const signedA = userClient.signSignedMsgOrderParamsMessage( + buildMsg(marketIndex, baseAssetAmount, 10, feeBpsA, slot), + false + ); + await builderClient.placeSignedMsgTakerOrder( + signedA, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const signedB = userClient.signSignedMsgOrderParamsMessage( + buildMsg(marketIndex, baseAssetAmount, 11, feeBpsB, slot), + false + ); + await builderClient.placeSignedMsgTakerOrder( + signedB, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + // Fill both orders + const fillTxA = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[0].orderId }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsA = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxA + ); + const eventsA = parseLogs(builderClient.program, logsA); + const fillEventA = eventsA.find((e) => e.name === 'OrderActionRecord'); + assert(fillEventA !== undefined); + const builderFeeA = fillEventA.data['builderFee'] as BN; + // const referrerRewardA = new BN(fillEventA.data['referrerReward'] as number); + + const fillTxB = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[1].orderId }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsB = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxB + ); + const eventsB = parseLogs(builderClient.program, logsB); + const fillEventB = eventsB.find((e) => e.name === 'OrderActionRecord'); + assert(fillEventB !== undefined); + const builderFeeB = fillEventB.data['builderFee'] as BN; + // const referrerRewardB = new BN(fillEventB.data['referrerReward'] as number); + + await bankrunContextWrapper.moveTimeForward(100); + + await escrowMap.slowSync(); + const escrowAfterFills = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const totalFeesAccrued = escrowAfterFills.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + const expectedTotal = builderFeeA.add(builderFeeB); + // .add(referrerRewardA) + // .add(referrerRewardB); + assert( + totalFeesAccrued.sub(totalFeesInEscrowStart).eq(expectedTotal), + `totalFeesAccrued: ${totalFeesAccrued.toString()}, expectedTotal: ${expectedTotal.toString()}` + ); + + // Settle and verify fees swept to builder + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBefore = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await userClient.fetchAccounts(); + await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + escrowMap + ); + + await escrowMap.slowSync(); + const escrowAfterSettle = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + for (const order of escrowAfterSettle.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfter = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + const usdcDiff = builderUsdcAfter.sub(builderUsdcBefore); + assert( + usdcDiff.eq(expectedTotal), + `usdcDiff: ${usdcDiff.toString()}, expectedTotal: ${expectedTotal.toString()}` + ); + }); + + it('user can place and fill with multiple maker orders', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; + await userClient.changeApprovedBuilder(builder.publicKey, maxFeeBps, true); + + const builderAccountInfoBefore = + await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + const builderAccBefore: RevenueShareAccount = + builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + 'RevenueShare', + builderAccountInfoBefore.data + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + + // place maker orders + await makerClient.placeOrders([ + getLimitOrderParams({ + marketIndex: 0, + baseAssetAmount: baseAssetAmount.divn(3), + direction: PositionDirection.SHORT, + price: new BN(223000000), + marketType: MarketType.PERP, + postOnly: PostOnlyParams.SLIDE, + }) as OrderParams, + getLimitOrderParams({ + marketIndex: 0, + baseAssetAmount: baseAssetAmount.divn(3), + direction: PositionDirection.SHORT, + price: new BN(223500000), + marketType: MarketType.PERP, + postOnly: PostOnlyParams.SLIDE, + }) as OrderParams, + ]); + await makerClient.fetchAccounts(); + const makerOrders = makerClient.getUser().getOpenOrders(); + assert(makerOrders.length === 2); + + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const feeBpsA = 6; + + const signedA = userClient.signSignedMsgOrderParamsMessage( + buildMsg(marketIndex, baseAssetAmount, 10, feeBpsA, slot), + false + ); + await builderClient.placeSignedMsgTakerOrder( + signedA, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 1); + + // Fill taker against maker orders + const fillTxA = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[0].orderId }, + { + maker: await makerClient.getUserAccountPublicKey(), + makerStats: makerClient.getUserStatsAccountPublicKey(), + makerUserAccount: makerClient.getUserAccount(), + // order?: Order; + }, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsA = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxA + ); + const eventsA = parseLogs(builderClient.program, logsA); + const fillEventA = eventsA.filter((e) => e.name === 'OrderActionRecord'); + assert(fillEventA !== undefined); + const builderFeeA = fillEventA.reduce( + (sum, e) => sum.add(e.data['builderFee'] as BN), + ZERO + ); + // const referrerRewardA = fillEventA.reduce( + // (sum, e) => sum.add(new BN(e.data['referrerReward'] as number)), + // ZERO + // ); + + await bankrunContextWrapper.moveTimeForward(100); + + await escrowMap.slowSync(); + const escrowAfterFills = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const totalFeesAccrued = escrowAfterFills.orders + .filter((o) => !isBuilderOrderReferral(o)) + .reduce((sum, o) => sum.add(o.feesAccrued ?? ZERO), ZERO); + assert( + totalFeesAccrued.eq(builderFeeA), + `totalFeesAccrued: ${totalFeesAccrued.toString()}, builderFeeA: ${builderFeeA.toString()}` + ); + + // Settle and verify fees swept to builder + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBefore = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await userClient.fetchAccounts(); + const settleTx = await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + escrowMap + ); + await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + settleTx + ); + + await escrowMap.slowSync(); + const escrowAfterSettle = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + for (const order of escrowAfterSettle.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfter = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + assert( + builderUsdcAfter.sub(builderUsdcBefore).eq(builderFeeA), + // .add(referrerRewardA) + `builderUsdcAfter: ${builderUsdcAfter.toString()} !== builderUsdcBefore ${builderUsdcBefore.toString()} + builderFeeA ${builderFeeA.toString()}` + ); + + const builderAccountInfoAfter = + await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + const builderAccAfter: RevenueShareAccount = + builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + 'RevenueShare', + builderAccountInfoAfter.data + ); + assert( + builderAccAfter.authority.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + + const builderFeeChange = builderAccAfter.totalBuilderRewards.sub( + builderAccBefore.totalBuilderRewards + ); + assert( + builderFeeChange.eq(builderFeeA), + `builderFeeChange: ${builderFeeChange.toString()}, builderFeeA: ${builderFeeA.toString()}` + ); + + // const referrerRewardChange = builderAccAfter.totalReferrerRewards.sub( + // builderAccBefore.totalReferrerRewards + // ); + // assert(referrerRewardChange.eq(referrerRewardA)); + }); + + // it('can track referral rewards for 2 markets', async () => { + // const builderAccountInfoBefore = + // await bankrunContextWrapper.connection.getAccountInfo( + // getRevenueShareAccountPublicKey( + // builderClient.program.programId, + // builderClient.wallet.publicKey + // ) + // ); + // const builderAccBefore: RevenueShareAccount = + // builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + // 'RevenueShare', + // builderAccountInfoBefore.data + // ); + // // await escrowMap.slowSync(); + // // const escrowBeforeFills = (await escrowMap.mustGet( + // // userClient.wallet.publicKey.toBase58() + // // )) as RevenueShareEscrowAccount; + + // const slot = new BN( + // await bankrunContextWrapper.connection.toConnection().getSlot() + // ); + + // // place 2 orders in different markets + + // const signedA = userClient.signSignedMsgOrderParamsMessage( + // buildMsg(0, BASE_PRECISION, 1, 5, slot), + // false + // ); + // await builderClient.placeSignedMsgTakerOrder( + // signedA, + // 0, + // { + // taker: await userClient.getUserAccountPublicKey(), + // takerUserAccount: userClient.getUserAccount(), + // takerStats: userClient.getUserStatsAccountPublicKey(), + // signingAuthority: userClient.wallet.publicKey, + // }, + // undefined, + // 2 + // ); + + // const signedB = userClient.signSignedMsgOrderParamsMessage( + // buildMsg(1, BASE_PRECISION, 2, 5, slot), + // false + // ); + // await builderClient.placeSignedMsgTakerOrder( + // signedB, + // 1, + // { + // taker: await userClient.getUserAccountPublicKey(), + // takerUserAccount: userClient.getUserAccount(), + // takerStats: userClient.getUserStatsAccountPublicKey(), + // signingAuthority: userClient.wallet.publicKey, + // }, + // undefined, + // 2 + // ); + + // await userClient.fetchAccounts(); + // const openOrders = userClient.getUser().getOpenOrders(); + + // const fillTxA = await makerClient.fillPerpOrder( + // await userClient.getUserAccountPublicKey(), + // userClient.getUserAccount(), + // { + // marketIndex: 0, + // orderId: openOrders.find( + // (o) => isVariant(o.status, 'open') && o.marketIndex === 0 + // )!.orderId, + // }, + // undefined, + // { + // referrer: await builderClient.getUserAccountPublicKey(), + // referrerStats: builderClient.getUserStatsAccountPublicKey(), + // }, + // undefined, + // undefined, + // undefined, + // true + // ); + // const logsA = await printTxLogs( + // bankrunContextWrapper.connection.toConnection(), + // fillTxA + // ); + // const eventsA = parseLogs(builderClient.program, logsA); + // const fillsA = eventsA.filter((e) => e.name === 'OrderActionRecord'); + // const fillAReferrerReward = fillsA[0]['data']['referrerReward'] as number; + // assert(fillsA.length > 0); + // // debug: fillsA[0]['data'] + + // const fillTxB = await makerClient.fillPerpOrder( + // await userClient.getUserAccountPublicKey(), + // userClient.getUserAccount(), + // { + // marketIndex: 1, + // orderId: openOrders.find( + // (o) => isVariant(o.status, 'open') && o.marketIndex === 1 + // )!.orderId, + // }, + // undefined, + // { + // referrer: await builderClient.getUserAccountPublicKey(), + // referrerStats: builderClient.getUserStatsAccountPublicKey(), + // }, + // undefined, + // undefined, + // undefined, + // true + // ); + // const logsB = await printTxLogs( + // bankrunContextWrapper.connection.toConnection(), + // fillTxB + // ); + // const eventsB = parseLogs(builderClient.program, logsB); + // const fillsB = eventsB.filter((e) => e.name === 'OrderActionRecord'); + // assert(fillsB.length > 0); + // const fillBReferrerReward = fillsB[0]['data']['referrerReward'] as number; + // // debug: fillsB[0]['data'] + + // await escrowMap.slowSync(); + // const escrowAfterFills = (await escrowMap.mustGet( + // userClient.wallet.publicKey.toBase58() + // )) as RevenueShareEscrowAccount; + + // const referrerOrdersMarket0 = escrowAfterFills.orders.filter( + // (o) => o.marketIndex === 0 && isBuilderOrderReferral(o) + // ); + // const referrerOrdersMarket1 = escrowAfterFills.orders.filter( + // (o) => o.marketIndex === 1 && isBuilderOrderReferral(o) + // ); + // assert(referrerOrdersMarket0[0].marketIndex === 0); + // assert( + // referrerOrdersMarket0[0].feesAccrued.eq(new BN(fillAReferrerReward)) + // ); + // assert(referrerOrdersMarket1[0].marketIndex === 1); + // assert( + // referrerOrdersMarket1[0].feesAccrued.eq(new BN(fillBReferrerReward)) + // ); + + // // settle pnl + // const settleTxA = await builderClient.settleMultiplePNLs( + // await userClient.getUserAccountPublicKey(), + // userClient.getUserAccount(), + // [0, 1], + // SettlePnlMode.MUST_SETTLE, + // escrowMap + // ); + // await printTxLogs( + // bankrunContextWrapper.connection.toConnection(), + // settleTxA + // ); + + // await escrowMap.slowSync(); + // const escrowAfterSettle = (await escrowMap.mustGet( + // userClient.wallet.publicKey.toBase58() + // )) as RevenueShareEscrowAccount; + // const referrerOrdersMarket0AfterSettle = escrowAfterSettle.orders.filter( + // (o) => o.marketIndex === 0 && isBuilderOrderReferral(o) + // ); + // const referrerOrdersMarket1AfterSettle = escrowAfterSettle.orders.filter( + // (o) => o.marketIndex === 1 && isBuilderOrderReferral(o) + // ); + // assert(referrerOrdersMarket0AfterSettle.length === 1); + // assert(referrerOrdersMarket1AfterSettle.length === 1); + // assert(referrerOrdersMarket0AfterSettle[0].feesAccrued.eq(ZERO)); + // assert(referrerOrdersMarket1AfterSettle[0].feesAccrued.eq(ZERO)); + + // const builderAccountInfoAfter = + // await bankrunContextWrapper.connection.getAccountInfo( + // getRevenueShareAccountPublicKey( + // builderClient.program.programId, + // builderClient.wallet.publicKey + // ) + // ); + // const builderAccAfter: RevenueShareAccount = + // builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + // 'RevenueShare', + // builderAccountInfoAfter.data + // ); + // const referrerRewards = builderAccAfter.totalReferrerRewards.sub( + // builderAccBefore.totalReferrerRewards + // ); + // assert( + // referrerRewards.eq(new BN(fillAReferrerReward + fillBReferrerReward)) + // ); + // }); +}); diff --git a/tests/placeAndMakeSignedMsgBankrun.ts b/tests/placeAndMakeSignedMsgBankrun.ts index 3b23722445..5971501251 100644 --- a/tests/placeAndMakeSignedMsgBankrun.ts +++ b/tests/placeAndMakeSignedMsgBankrun.ts @@ -1564,7 +1564,7 @@ describe('place and make signedMsg order', () => { ); assert.fail('should fail'); } catch (e) { - assert(e.toString().includes('0x1776')); + assert(e.toString().includes('Error: Invalid option')); const takerOrders = takerDriftClient.getUser().getOpenOrders(); assert(takerOrders.length == 0); } diff --git a/tests/subaccounts.ts b/tests/subaccounts.ts index 2f6f5c5cd2..fe0c63acc4 100644 --- a/tests/subaccounts.ts +++ b/tests/subaccounts.ts @@ -158,6 +158,7 @@ describe('subaccounts', () => { undefined, donationAmount ); + await driftClient.fetchAccounts(); await driftClient.addUser(1); await driftClient.switchActiveUser(1); diff --git a/tests/switchboardTxCus.ts b/tests/switchboardTxCus.ts index 40e6a33277..b3a933eb18 100644 --- a/tests/switchboardTxCus.ts +++ b/tests/switchboardTxCus.ts @@ -219,6 +219,6 @@ describe('switchboard place orders cus', () => { const cus = bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); console.log(cus); - assert(cus < 410000); + assert(cus < 413000); }); }); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index df4e742abe..8df74e2a33 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -43,6 +43,7 @@ import { PositionDirection, DriftClient, OrderType, + ReferrerInfo, } from '../sdk'; import { TestClient, @@ -401,7 +402,8 @@ export async function initializeAndSubscribeDriftClient( marketIndexes: number[], bankIndexes: number[], oracleInfos: OracleInfo[] = [], - accountLoader?: TestBulkAccountLoader + accountLoader?: TestBulkAccountLoader, + referrerInfo?: ReferrerInfo ): Promise { const driftClient = new TestClient({ connection, @@ -426,7 +428,7 @@ export async function initializeAndSubscribeDriftClient( }, }); await driftClient.subscribe(); - await driftClient.initializeUserAccount(); + await driftClient.initializeUserAccount(0, undefined, referrerInfo); return driftClient; } @@ -438,7 +440,8 @@ export async function createUserWithUSDCAccount( marketIndexes: number[], bankIndexes: number[], oracleInfos: OracleInfo[] = [], - accountLoader?: TestBulkAccountLoader + accountLoader?: TestBulkAccountLoader, + referrerInfo?: ReferrerInfo ): Promise<[TestClient, PublicKey, Keypair]> { const userKeyPair = await createFundedKeyPair(context); const usdcAccount = await createUSDCAccountForUser( @@ -454,7 +457,8 @@ export async function createUserWithUSDCAccount( marketIndexes, bankIndexes, oracleInfos, - accountLoader + accountLoader, + referrerInfo ); return [driftClient, usdcAccount, userKeyPair]; @@ -557,7 +561,6 @@ export async function printTxLogs( const tx = await connection.getTransaction(txSig, { commitment: 'confirmed', }); - console.log('tx logs', tx.meta.logMessages); return tx.meta.logMessages; } From bc2493d1b23200f354b732e354282a00ca433129 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:11:43 +0000 Subject: [PATCH 050/247] sdk: release v2.140.0-beta.2 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index ea9c93bfd5..430d4c506d 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.140.0-beta.1 \ No newline at end of file +2.140.0-beta.2 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 9ce87d3f79..0cfd36d013 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.140.0-beta.1", + "version": "2.140.0-beta.2", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From d315d880dd85cc59f10e5f788c8c64d65d52cd76 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 29 Sep 2025 12:17:46 -0700 Subject: [PATCH 051/247] v2.140.0 --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6391cb4f4..ec43602a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +### Fixes + +### Breaking + +## [2.140.0] - 2025-09-29 + +### Features + - program: builder codes ([#1805](https://github.com/drift-labs/protocol-v2/pull/1805)) ### Fixes diff --git a/Cargo.lock b/Cargo.lock index cc1c58e9fa..1f28352549 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.139.0" +version = "2.140.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 6c21eefc2a..9da71196a1 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.139.0" +version = "2.140.0" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index 0cfd36d013..fb18d32aa7 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.140.0-beta.2", + "version": "2.140.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 8400a22666..fa7e468f8a 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.139.0", + "version": "2.140.0", "name": "drift", "instructions": [ { @@ -16700,4 +16700,4 @@ "metadata": { "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" } -} \ No newline at end of file +} From aedb8d6438470da28ced7a4dd32b7ad376aac8f1 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:25:49 +0000 Subject: [PATCH 052/247] sdk: release v2.141.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 430d4c506d..049297baa0 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.140.0-beta.2 \ No newline at end of file +2.141.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index fb18d32aa7..9171f5ec0a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.140.0", + "version": "2.141.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 1fd7490471e3ea817e19bfce89681fad183376ad Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Mon, 29 Sep 2025 12:38:07 -0700 Subject: [PATCH 053/247] feat: add margin ratio ix to open orders + swift prop (#1864) * feat: add margin ratio ix to open orders + swift prop * fix: bug with max lev available calculation * fix: bug with swift msg encoding + margin ratio * feat: re-add types for swift non-optional * rm: unneeded undefined check on swift maxMarginRation * allow enter HLM on position margin ratio update * fix margin ratio calc * updates * rm logs --------- Co-authored-by: Nick Caradonna --- sdk/src/driftClient.ts | 114 ++++++++++++++++++++++++++++++++++---- sdk/src/user.ts | 51 ++++++++++++----- sdk/tests/dlob/helpers.ts | 1 + 3 files changed, 139 insertions(+), 27 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 596598f9bd..63e1d8a607 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -126,6 +126,7 @@ import { TxSender, TxSigAndSlot } from './tx/types'; import { BASE_PRECISION, GOV_SPOT_MARKET_INDEX, + MARGIN_PRECISION, ONE, PERCENTAGE_PRECISION, PRICE_PRECISION, @@ -1723,11 +1724,11 @@ export class DriftClient { ): Promise { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, - this.wallet.publicKey, + this.authority, subAccountId ); - await this.addUser(subAccountId, this.wallet.publicKey); + await this.addUser(subAccountId, this.authority); const ix = this.program.instruction.updateUserPerpPositionCustomMarginRatio( subAccountId, @@ -1748,14 +1749,21 @@ export class DriftClient { perpMarketIndex: number, marginRatio: number, subAccountId = 0, - txParams?: TxParams + txParams?: TxParams, + enterHighLeverageMode?: boolean ): Promise { - const ix = await this.getUpdateUserPerpPositionCustomMarginRatioIx( + const ixs = []; + if (enterHighLeverageMode) { + const enableIx = await this.getEnableHighLeverageModeIx(subAccountId); + ixs.push(enableIx); + } + const updateIx = await this.getUpdateUserPerpPositionCustomMarginRatioIx( perpMarketIndex, marginRatio, subAccountId ); - const tx = await this.buildTransaction(ix, txParams ?? this.txParams); + ixs.push(updateIx); + const tx = await this.buildTransaction(ixs, txParams ?? this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } @@ -4292,7 +4300,8 @@ export class DriftClient { bracketOrdersParams = new Array(), referrerInfo?: ReferrerInfo, cancelExistingOrders?: boolean, - settlePnl?: boolean + settlePnl?: boolean, + positionMaxLev?: number ): Promise<{ cancelExistingOrdersTx?: Transaction | VersionedTransaction; settlePnlTx?: Transaction | VersionedTransaction; @@ -4308,7 +4317,10 @@ export class DriftClient { const marketIndex = orderParams.marketIndex; const orderId = userAccount.nextOrderId; - const ixPromisesForTxs: Record> = { + const ixPromisesForTxs: Record< + TxKeys, + Promise + > = { cancelExistingOrdersTx: undefined, settlePnlTx: undefined, fillTx: undefined, @@ -4317,10 +4329,18 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - ixPromisesForTxs.marketOrderTx = this.getPlaceOrdersIx( - [orderParams, ...bracketOrdersParams], - userAccount.subAccountId - ); + const marketOrderTxIxs = positionMaxLev + ? this.getPlaceOrdersAndSetPositionMaxLevIx( + [orderParams, ...bracketOrdersParams], + positionMaxLev, + userAccount.subAccountId + ) + : this.getPlaceOrdersIx( + [orderParams, ...bracketOrdersParams], + userAccount.subAccountId + ); + + ixPromisesForTxs.marketOrderTx = marketOrderTxIxs; /* Cancel open orders in market if requested */ if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { @@ -4361,7 +4381,10 @@ export class DriftClient { const ixsMap = ixs.reduce((acc, ix, i) => { acc[txKeys[i]] = ix; return acc; - }, {}) as MappedRecord; + }, {}) as MappedRecord< + typeof ixPromisesForTxs, + TransactionInstruction | TransactionInstruction[] + >; const txsMap = (await this.buildTransactionsMap( ixsMap, @@ -4945,6 +4968,73 @@ export class DriftClient { }); } + public async getPlaceOrdersAndSetPositionMaxLevIx( + params: OptionalOrderParams[], + positionMaxLev: number, + subAccountId?: number + ): Promise { + const user = await this.getUserAccountPublicKey(subAccountId); + + const readablePerpMarketIndex: number[] = []; + const readableSpotMarketIndexes: number[] = []; + for (const param of params) { + if (!param.marketType) { + throw new Error('must set param.marketType'); + } + if (isVariant(param.marketType, 'perp')) { + readablePerpMarketIndex.push(param.marketIndex); + } else { + readableSpotMarketIndexes.push(param.marketIndex); + } + } + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(subAccountId)], + readablePerpMarketIndex, + readableSpotMarketIndexes, + useMarketLastSlotCache: true, + }); + + for (const param of params) { + if (isUpdateHighLeverageMode(param.bitFlags)) { + remainingAccounts.push({ + pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), + isWritable: true, + isSigner: false, + }); + } + } + + const formattedParams = params.map((item) => getOrderParams(item)); + + const placeOrdersIxs = await this.program.instruction.placeOrders( + formattedParams, + { + accounts: { + state: await this.getStatePublicKey(), + user, + userStats: this.getUserStatsAccountPublicKey(), + authority: this.wallet.publicKey, + }, + remainingAccounts, + } + ); + + const marginRatio = Math.floor( + (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() + ); + + // TODO: Handle multiple markets? + const setPositionMaxLevIxs = + await this.getUpdateUserPerpPositionCustomMarginRatioIx( + readablePerpMarketIndex[0], + marginRatio, + subAccountId + ); + + return [placeOrdersIxs, setPositionMaxLevIxs]; + } + public async fillPerpOrder( userAccountPublicKey: PublicKey, user: UserAccount, diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 08494f71f8..a36820306d 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -449,7 +449,8 @@ export class User { public getPerpBuyingPower( marketIndex: number, collateralBuffer = ZERO, - enterHighLeverageMode = undefined + enterHighLeverageMode = undefined, + maxMarginRatio = undefined ): BN { const perpPosition = this.getPerpPositionOrEmpty(marketIndex); @@ -473,7 +474,7 @@ export class User { freeCollateral, worstCaseBaseAssetAmount, enterHighLeverageMode, - perpPosition + maxMarginRatio || perpPosition.maxMarginRatio ); } @@ -482,17 +483,17 @@ export class User { freeCollateral: BN, baseAssetAmount: BN, enterHighLeverageMode = undefined, - perpPosition?: PerpPosition + perpMarketMaxMarginRatio = undefined ): BN { - const userCustomMargin = Math.max( - perpPosition?.maxMarginRatio ?? 0, + const maxMarginRatio = Math.max( + perpMarketMaxMarginRatio, this.getUserAccount().maxMarginRatio ); const marginRatio = calculateMarketMarginRatio( this.driftClient.getPerpMarketAccount(marketIndex), baseAssetAmount, 'Initial', - userCustomMargin, + maxMarginRatio, enterHighLeverageMode || this.isHighLeverageMode('Initial') ); @@ -1247,7 +1248,10 @@ export class User { } if (marginCategory) { - const userCustomMargin = this.getUserAccount().maxMarginRatio; + const userCustomMargin = Math.max( + perpPosition.maxMarginRatio, + this.getUserAccount().maxMarginRatio + ); let marginRatio = new BN( calculateMarketMarginRatio( market, @@ -2345,13 +2349,18 @@ export class User { public getMarginUSDCRequiredForTrade( targetMarketIndex: number, baseSize: BN, - estEntryPrice?: BN + estEntryPrice?: BN, + perpMarketMaxMarginRatio?: number ): BN { + const maxMarginRatio = Math.max( + perpMarketMaxMarginRatio, + this.getUserAccount().maxMarginRatio + ); return calculateMarginUSDCRequiredForTrade( this.driftClient, targetMarketIndex, baseSize, - this.getUserAccount().maxMarginRatio, + maxMarginRatio, undefined, estEntryPrice ); @@ -2360,14 +2369,19 @@ export class User { public getCollateralDepositRequiredForTrade( targetMarketIndex: number, baseSize: BN, - collateralIndex: number + collateralIndex: number, + perpMarketMaxMarginRatio?: number ): BN { + const maxMarginRatio = Math.max( + perpMarketMaxMarginRatio, + this.getUserAccount().maxMarginRatio + ); return calculateCollateralDepositRequiredForTrade( this.driftClient, targetMarketIndex, baseSize, collateralIndex, - this.getUserAccount().maxMarginRatio, + maxMarginRatio, false // assume user cant be high leverage if they havent created user account ? ); } @@ -2385,7 +2399,8 @@ export class User { targetMarketIndex: number, tradeSide: PositionDirection, isLp = false, - enterHighLeverageMode = undefined + enterHighLeverageMode = undefined, + maxMarginRatio = undefined ): { tradeSize: BN; oppositeSideTradeSize: BN } { let tradeSize = ZERO; let oppositeSideTradeSize = ZERO; @@ -2424,7 +2439,8 @@ export class User { const maxPositionSize = this.getPerpBuyingPower( targetMarketIndex, lpBuffer, - enterHighLeverageMode + enterHighLeverageMode, + maxMarginRatio ); if (maxPositionSize.gte(ZERO)) { @@ -2451,8 +2467,12 @@ export class User { const marginRequirement = this.getInitialMarginRequirement( enterHighLeverageMode ); + const marginRatio = Math.max( + currentPosition.maxMarginRatio, + this.getUserAccount().maxMarginRatio + ); const marginFreedByClosing = perpLiabilityValue - .mul(new BN(market.marginRatioInitial)) + .mul(new BN(marginRatio)) .div(MARGIN_PRECISION); const marginRequirementAfterClosing = marginRequirement.sub(marginFreedByClosing); @@ -2468,7 +2488,8 @@ export class User { this.getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount( targetMarketIndex, freeCollateralAfterClose, - ZERO + ZERO, + currentPosition.maxMarginRatio ); oppositeSideTradeSize = perpLiabilityValue; tradeSize = buyingPowerAfterClose; diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index d1b68abe8c..d682f5e757 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -44,6 +44,7 @@ export const mockPerpPosition: PerpPosition = { lastBaseAssetAmountPerLp: new BN(0), lastQuoteAssetAmountPerLp: new BN(0), perLpBase: 0, + maxMarginRatio: 1, }; export const mockAMM: AMM = { From f1a1c712fcc12c2a334099f896d416a96b0d5187 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:43:40 +0000 Subject: [PATCH 054/247] sdk: release v2.141.0-beta.1 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 049297baa0..c14ebc75d8 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.141.0-beta.0 \ No newline at end of file +2.141.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 9171f5ec0a..7d0b1f3b3e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0-beta.0", + "version": "2.141.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From a1719bbd8765e8ca7448b200edcf4ca0ef960812 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 29 Sep 2025 14:05:05 -0700 Subject: [PATCH 055/247] run CICD on PRs to devnet --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cc4d922902..a327ad3bee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: push: branches: [master] pull_request: - branches: [master] + branches: [master, devnet] defaults: run: From d3dbc64bc5e4d7d6550de81407b7299aa46f0eaa Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 1 Oct 2025 20:02:11 -0700 Subject: [PATCH 056/247] remove redundant account in getPlaceAndMakePerpOrderIx (#1923) * remove redundant account in getPlaceAndMakePerpOrderIx * update idl --- sdk/src/driftClient.ts | 9 --------- sdk/src/idl/drift.json | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 63e1d8a607..3104fdbbe6 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -6807,15 +6807,6 @@ export class DriftClient { }); } - remainingAccounts.push({ - pubkey: getRevenueShareEscrowAccountPublicKey( - this.program.programId, - takerInfo.takerUserAccount.authority - ), - isWritable: true, - isSigner: false, - }); - const takerOrderId = takerInfo.order.orderId; if (hasBuilder(takerInfo.order)) { remainingAccounts.push({ diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index fa7e468f8a..9db0b9deb3 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -14031,6 +14031,20 @@ "option": "u64" }, "index": false + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + }, + "index": false + }, + { + "name": "builderFee", + "type": { + "option": "u64" + }, + "index": false } ] }, @@ -16700,4 +16714,4 @@ "metadata": { "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" } -} +} \ No newline at end of file From 7db528a7d86d6f69250d21edbd4cdf4041a77875 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 03:06:42 +0000 Subject: [PATCH 057/247] sdk: release v2.141.0-beta.2 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index c14ebc75d8..7a08f75b2b 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.141.0-beta.1 \ No newline at end of file +2.141.0-beta.2 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 7d0b1f3b3e..7d0d1880c3 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0-beta.1", + "version": "2.141.0-beta.2", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From cc97506c3cf850df892df13476c3eb039a982551 Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:08:51 -0400 Subject: [PATCH 058/247] sdk: add spot-market-index-59 (#1925) * sdk: add spot-market-index-59 * launch ts --- sdk/src/constants/perpMarkets.ts | 13 +++++++++++++ sdk/src/constants/spotMarkets.ts | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index bbb7a12423..1e9aa6e440 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1338,6 +1338,19 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ '0x9873512f5cb33c77ad7a5af098d74812c62111166be395fd0941c8cedb9b00d4', pythLazerId: 2312, }, + { + fullName: 'Double Zero', + category: ['Infra'], + symbol: '2Z-PERP', + baseAssetSymbol: '2Z', + marketIndex: 78, + oracle: new PublicKey('4HTDpcHAwBTHCJLNMwT35w4FGc4nfA4YhT1BkcZQwQ2m'), + launchTs: 1759412919000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0xf2b3ab1c49e35e881003c3c0482d18b181a1560b697b844c24c8f85aba1cab95', + pythLazerId: 2316, + }, ]; export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = { diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 3840a37d8a..30dfb8ec5b 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -946,6 +946,20 @@ export const MainnetSpotMarkets: SpotMarketConfig[] = [ '0x8f257aab6e7698bb92b15511915e593d6f8eae914452f781874754b03d0c612b', launchTs: 1756392947000, }, + { + symbol: '2Z', + marketIndex: 59, + poolId: 0, + oracle: new PublicKey('4HTDpcHAwBTHCJLNMwT35w4FGc4nfA4YhT1BkcZQwQ2m'), + oracleSource: OracleSource.PYTH_LAZER, + mint: new PublicKey('J6pQQ3FAcJQeWPPGppWRb4nM8jU3wLyYbRrLh7feMfvd'), + precision: new BN(10).pow(EIGHT), + precisionExp: EIGHT, + pythFeedId: + '0xf2b3ab1c49e35e881003c3c0482d18b181a1560b697b844c24c8f85aba1cab95', + pythLazerId: 2316, + launchTs: 1759412919000, + }, ]; export const SpotMarkets: { [key in DriftEnv]: SpotMarketConfig[] } = { From ce5a2f2d42a1ed5077e2463dbcaef43e284750ed Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:14:43 +0000 Subject: [PATCH 059/247] sdk: release v2.141.0-beta.3 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 7a08f75b2b..69437da9ca 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.141.0-beta.2 \ No newline at end of file +2.141.0-beta.3 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 7d0d1880c3..600e2893e4 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0-beta.2", + "version": "2.141.0-beta.3", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 4d1cfe964eb3b28799623cf670dab22d159709b6 Mon Sep 17 00:00:00 2001 From: lil perp Date: Fri, 3 Oct 2025 01:14:30 +0800 Subject: [PATCH 060/247] program: calculate_max_perp_order_size account for max margin ratio (#1921) --- programs/drift/src/math/orders.rs | 7 +- programs/drift/src/math/orders/tests.rs | 246 ++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 013563f0bd..b4495afd5c 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -809,6 +809,7 @@ pub fn calculate_max_perp_order_size( )?; let user_custom_margin_ratio = user.max_margin_ratio; + let perp_position_margin_ratio = user.perp_positions[position_index].max_margin_ratio as u32; let user_high_leverage_mode = user.is_high_leverage_mode(MarginRequirementType::Initial); let free_collateral_before = total_collateral.safe_sub(margin_requirement.cast()?)?; @@ -838,7 +839,8 @@ pub fn calculate_max_perp_order_size( MarginRequirementType::Initial, user_high_leverage_mode, )? - .max(user_custom_margin_ratio); + .max(user_custom_margin_ratio) + .max(perp_position_margin_ratio); let mut order_size_to_reduce_position = 0_u64; let mut free_collateral_released = 0_i128; @@ -915,7 +917,8 @@ pub fn calculate_max_perp_order_size( MarginRequirementType::Initial, user_high_leverage_mode, )? - .max(user_custom_margin_ratio); + .max(user_custom_margin_ratio) + .max(perp_position_margin_ratio); Ok((new_order_size, new_margin_ratio)) }; diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 112144451d..52b0e012a8 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -2791,6 +2791,252 @@ mod calculate_max_perp_order_size { assert!(total_collateral.unsigned_abs() - margin_requirement < 100 * QUOTE_PRECISION); } + #[test] + pub fn sol_perp_5x_bid_perp_position_margin_ratio() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 2000, + margin_ratio_maintenance: 1000, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Long, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 49999950000); + + user.perp_positions[0].open_orders = 1; + user.perp_positions[0].open_bids = max_order_size as i64; + + let MarginCalculation { + margin_requirement, + total_collateral, + .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial).strict(true), + ) + .unwrap(); + + assert_eq!(total_collateral.unsigned_abs(), margin_requirement); + } + + #[test] + pub fn sol_perp_5x_ask_perp_position_margin_ratio() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 2000, + margin_ratio_maintenance: 1000, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 49999950000); + + user.perp_positions[0].open_orders = 1; + user.perp_positions[0].open_asks = -(max_order_size as i64); + + let MarginCalculation { + margin_requirement, + total_collateral, + .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial).strict(true), + ) + .unwrap(); + + assert_eq!(total_collateral.unsigned_abs(), margin_requirement); + } + #[test] pub fn sol_perp_10x_ask_with_imf() { let slot = 0_u64; From 10b6101ad769ecf01388f07344994b4dfd3106da Mon Sep 17 00:00:00 2001 From: 0xDeep <65382963+0xDeeep@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:44:55 +0530 Subject: [PATCH 061/247] feat: add dflow in whitelisted programs (#1924) --- programs/drift/src/ids.rs | 5 +++++ programs/drift/src/instructions/keeper.rs | 2 ++ programs/drift/src/instructions/user.rs | 2 ++ 3 files changed, 9 insertions(+) diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index 0c2a80addb..2e65c5de29 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -107,3 +107,8 @@ pub mod amm_spread_adjust_wallet { #[cfg(feature = "anchor-test")] declare_id!("1ucYHAGrBbi1PaecC4Ptq5ocZLWGLBmbGWysoDGNB1N"); } + +pub mod dflow_mainnet_aggregator_4 { + use solana_program::declare_id; + declare_id!("DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"); +} diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 6bcac5b669..3fb3db113a 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -21,6 +21,7 @@ use crate::controller::spot_balance::update_spot_balances; use crate::controller::token::{receive, send_from_program_vault}; use crate::error::ErrorCode; use crate::ids::admin_hot_wallet; +use crate::ids::dflow_mainnet_aggregator_4; use crate::ids::{jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, serum_program}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; @@ -1721,6 +1722,7 @@ pub fn handle_liquidate_spot_with_swap_begin<'c: 'info, 'info>( jupiter_mainnet_3::ID, jupiter_mainnet_4::ID, jupiter_mainnet_6::ID, + dflow_mainnet_aggregator_4::ID, ]; validate!( whitelisted_programs.contains(&ix.program_id), diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 6d42cd4c4b..d49bc5ea62 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -22,6 +22,7 @@ use crate::controller::spot_position::{ }; use crate::error::ErrorCode; use crate::ids::admin_hot_wallet; +use crate::ids::dflow_mainnet_aggregator_4; use crate::ids::{ jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, lighthouse, marinade_mainnet, serum_program, @@ -3678,6 +3679,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( jupiter_mainnet_3::ID, jupiter_mainnet_4::ID, jupiter_mainnet_6::ID, + dflow_mainnet_aggregator_4::ID, ]; if !delegate_is_signer { whitelisted_programs.push(Token::id()); From 806d611089fba79fd8d2d0b2a65b39869a447948 Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 2 Oct 2025 11:51:57 -0700 Subject: [PATCH 062/247] update settle pnl take rev share map (#1926) --- sdk/src/driftClient.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 3104fdbbe6..eec1e1d58d 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -7827,7 +7827,8 @@ export class DriftClient { settleeUserAccountPublicKey: PublicKey; settleeUserAccount: UserAccount; }[], - marketIndexes: number[] + marketIndexes: number[], + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise> { const ixs = []; for (const { settleeUserAccountPublicKey, settleeUserAccount } of users) { @@ -7836,7 +7837,8 @@ export class DriftClient { await this.settlePNLIx( settleeUserAccountPublicKey, settleeUserAccount, - marketIndex + marketIndex, + revenueShareEscrowMap ) ); } @@ -7851,7 +7853,7 @@ export class DriftClient { marketIndex: number, txParams?: TxParams, optionalIxs?: TransactionInstruction[], - escrowMap?: RevenueShareEscrowMap + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { const lookupTableAccounts = await this.fetchAllLookupTableAccounts(); @@ -7861,7 +7863,7 @@ export class DriftClient { settleeUserAccountPublicKey, settleeUserAccount, marketIndex, - escrowMap + revenueShareEscrowMap ), txParams, undefined, From d12614d395f36bf5eb07067807ea726a929e7cbc Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:56:58 +0000 Subject: [PATCH 063/247] sdk: release v2.141.0-beta.4 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 69437da9ca..aa821f2cd5 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.141.0-beta.3 \ No newline at end of file +2.141.0-beta.4 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 600e2893e4..d6dc104dd4 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0-beta.3", + "version": "2.141.0-beta.4", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From c507bcaebc5d921b217378274e927cc2b5566e18 Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 2 Oct 2025 17:58:38 -0700 Subject: [PATCH 064/247] fix ci sdk check --- sdk/src/constants/spotMarkets.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 30dfb8ec5b..349a60893c 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -141,6 +141,16 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ pythFeedId: '0x67e031d1723e5c89e4a826d80b2f3b41a91b05ef6122d523b8829a02e0f563aa', }, + { + symbol: 'bSOL', + marketIndex: 8, + poolId: 2, + oracle: new PublicKey('4wFrjUQHzRBc6qjVtMDbt28aEVgn6GaNiWR6vEff4KxR'), + oracleSource: OracleSource.Prelaunch, + mint: new PublicKey('2vVfXmcWXEaFzp7iaTVnQ4y1gR41S6tJQQMo1S5asJyC'), + precision: new BN(10).pow(NINE), + precisionExp: NINE, + }, ]; export const MainnetSpotMarkets: SpotMarketConfig[] = [ From 5c42b0a535e388524600ee4dc44c91dec07c5bd1 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:03:40 +0000 Subject: [PATCH 065/247] sdk: release v2.141.0-beta.5 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index aa821f2cd5..7dc981a2fc 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.141.0-beta.4 \ No newline at end of file +2.141.0-beta.5 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index d6dc104dd4..02fbae1e48 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0-beta.4", + "version": "2.141.0-beta.5", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 30f687163a85605f62838e5615251e221109cddc Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 2 Oct 2025 20:24:17 -0700 Subject: [PATCH 066/247] fix maker ix missing escrow detection (#1927) --- programs/drift/src/controller/orders.rs | 14 +++++++++++--- programs/drift/src/state/user.rs | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 57431d991c..6d4533dd33 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -2243,6 +2243,10 @@ pub fn fulfill_perp_order_with_amm( direction, order_id ); + let user_order_has_builder = user.orders[order_index].is_has_builder(); + if user_order_has_builder && rev_share_escrow.is_none() { + msg!("Order has builder but no escrow account included, in the future this will fail."); + } validation::perp_market::validate_amm_account_for_fill(&market.amm, order_direction)?; @@ -2331,7 +2335,7 @@ pub fn fulfill_perp_order_with_amm( if builder_fee != 0 { if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_mut()) { - let mut order = escrow.get_order_mut(idx)?; + let order = escrow.get_order_mut(idx)?; order.fees_accrued = order.fees_accrued.safe_add(builder_fee)?; } else { msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); @@ -2593,6 +2597,10 @@ pub fn fulfill_perp_order_with_match( let oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; let taker_direction: PositionDirection = taker.orders[taker_order_index].direction; + let taker_order_has_builder = taker.orders[taker_order_index].is_has_builder(); + if taker_order_has_builder && rev_share_escrow.is_none() { + msg!("Order has builder but no escrow account included, in the future this will fail."); + } let taker_price = if let Some(taker_limit_price) = taker_limit_price { taker_limit_price @@ -2840,7 +2848,7 @@ pub fn fulfill_perp_order_with_match( if builder_fee != 0 { if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_deref_mut()) { - let mut order = escrow.get_order_mut(idx)?; + let order = escrow.get_order_mut(idx)?; order.fees_accrued = order.fees_accrued.safe_add(builder_fee)?; } else { msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); @@ -2906,7 +2914,7 @@ pub fn fulfill_perp_order_with_match( if let (Some(idx), Some(escrow)) = (referrer_builder_order_idx, rev_share_escrow.as_deref_mut()) { - let mut order = escrow.get_order_mut(idx)?; + let order = escrow.get_order_mut(idx)?; order.fees_accrued = order.fees_accrued.safe_add(referrer_reward)?; } else if let (Some(referrer), Some(referrer_stats)) = (referrer.as_mut(), referrer_stats.as_mut()) diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 363efeab3f..b36607e3dc 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1503,6 +1503,10 @@ impl Order { self.is_bit_flag_set(OrderBitFlag::SignedMessage) } + pub fn is_has_builder(&self) -> bool { + self.is_bit_flag_set(OrderBitFlag::HasBuilder) + } + pub fn add_bit_flag(&mut self, flag: OrderBitFlag) { self.bit_flags |= flag as u8; } From 01656ee871b4c152765066845891e4e6f0b5a88b Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Fri, 3 Oct 2025 12:55:03 +0800 Subject: [PATCH 067/247] refactor(sdk): add OneShotUserStatsAccountSubscriber (#1929) --- .../basicUserStatsAccountSubscriber.ts | 65 +++++++++++++++++ .../oneShotUserStatsAccountSubscriber.ts | 69 +++++++++++++++++++ sdk/src/driftClient.ts | 2 +- sdk/src/index.ts | 1 + sdk/src/userStats.ts | 7 +- sdk/src/userStatsConfig.ts | 3 +- sdk/src/wallet.ts | 11 +++ 7 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 sdk/src/accounts/basicUserStatsAccountSubscriber.ts create mode 100644 sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts diff --git a/sdk/src/accounts/basicUserStatsAccountSubscriber.ts b/sdk/src/accounts/basicUserStatsAccountSubscriber.ts new file mode 100644 index 0000000000..dc5b48671d --- /dev/null +++ b/sdk/src/accounts/basicUserStatsAccountSubscriber.ts @@ -0,0 +1,65 @@ +import { + DataAndSlot, + UserStatsAccountEvents, + UserStatsAccountSubscriber, +} from './types'; +import { PublicKey } from '@solana/web3.js'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { UserStatsAccount } from '../types'; + +/** + * Basic implementation of UserStatsAccountSubscriber. It will only take in UserStatsAccount + * data during initialization and will not fetch or subscribe to updates. + */ +export class BasicUserStatsAccountSubscriber + implements UserStatsAccountSubscriber +{ + isSubscribed: boolean; + eventEmitter: StrictEventEmitter; + userStatsAccountPublicKey: PublicKey; + + callbackId?: string; + errorCallbackId?: string; + + userStats: DataAndSlot; + + public constructor( + userStatsAccountPublicKey: PublicKey, + data?: UserStatsAccount, + slot?: number + ) { + this.isSubscribed = true; + this.eventEmitter = new EventEmitter(); + this.userStatsAccountPublicKey = userStatsAccountPublicKey; + this.userStats = { data, slot }; + } + + async subscribe(_userStatsAccount?: UserStatsAccount): Promise { + return true; + } + + async addToAccountLoader(): Promise {} + + async fetch(): Promise {} + + doesAccountExist(): boolean { + return this.userStats !== undefined; + } + + async unsubscribe(): Promise {} + + assertIsSubscribed(): void {} + + public getUserStatsAccountAndSlot(): DataAndSlot { + return this.userStats; + } + + public updateData(userStatsAccount: UserStatsAccount, slot: number): void { + if (!this.userStats || slot >= (this.userStats.slot ?? 0)) { + this.userStats = { data: userStatsAccount, slot }; + this.eventEmitter.emit('userStatsAccountUpdate', userStatsAccount); + this.eventEmitter.emit('update'); + } + } +} diff --git a/sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts b/sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts new file mode 100644 index 0000000000..a464063f6c --- /dev/null +++ b/sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts @@ -0,0 +1,69 @@ +import { Commitment, PublicKey } from '@solana/web3.js'; +import { UserStatsAccount } from '../types'; +import { BasicUserStatsAccountSubscriber } from './basicUserStatsAccountSubscriber'; +import { Program } from '@coral-xyz/anchor'; +import { UserStatsAccountSubscriber } from './types'; + +/** + * Simple implementation of UserStatsAccountSubscriber. It will fetch the UserStatsAccount + * data on subscribe (or call to fetch) if no account data is provided on init. + * Expect to use only 1 RPC call unless you call fetch repeatedly. + */ +export class OneShotUserStatsAccountSubscriber + extends BasicUserStatsAccountSubscriber + implements UserStatsAccountSubscriber +{ + program: Program; + commitment: Commitment; + + public constructor( + program: Program, + userStatsAccountPublicKey: PublicKey, + data?: UserStatsAccount, + slot?: number, + commitment?: Commitment + ) { + super(userStatsAccountPublicKey, data, slot); + this.program = program; + this.commitment = commitment ?? 'confirmed'; + } + + async subscribe(userStatsAccount?: UserStatsAccount): Promise { + if (userStatsAccount) { + this.userStats = { data: userStatsAccount, slot: this.userStats.slot }; + return true; + } + + await this.fetchIfUnloaded(); + if (this.doesAccountExist()) { + this.eventEmitter.emit('update'); + } + return true; + } + + async fetchIfUnloaded(): Promise { + if (this.userStats.data === undefined) { + await this.fetch(); + } + } + + async fetch(): Promise { + try { + const dataAndContext = + await this.program.account.userStats.fetchAndContext( + this.userStatsAccountPublicKey, + this.commitment + ); + if (dataAndContext.context.slot > (this.userStats?.slot ?? 0)) { + this.userStats = { + data: dataAndContext.data as UserStatsAccount, + slot: dataAndContext.context.slot, + }; + } + } catch (e) { + console.error( + `OneShotUserStatsAccountSubscriber.fetch() UserStatsAccount does not exist: ${e.message}` + ); + } + } +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index eec1e1d58d..9b2463f0c4 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -933,7 +933,7 @@ export class DriftClient { this.userStats = new UserStats({ driftClient: this, userStatsAccountPublicKey: this.userStatsAccountPublicKey, - accountSubscription: this.userAccountSubscriptionConfig, + accountSubscription: this.userStatsAccountSubscriptionConfig, }); this.userStats.subscribe(); diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 0a261c0030..66c7ca9539 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -29,6 +29,7 @@ export * from './accounts/pollingInsuranceFundStakeAccountSubscriber'; export * from './accounts/pollingHighLeverageModeConfigAccountSubscriber'; export * from './accounts/basicUserAccountSubscriber'; export * from './accounts/oneShotUserAccountSubscriber'; +export * from './accounts/oneShotUserStatsAccountSubscriber'; export * from './accounts/types'; export * from './addresses/pda'; export * from './adminClient'; diff --git a/sdk/src/userStats.ts b/sdk/src/userStats.ts index 8fdb30d2bf..a045f533c0 100644 --- a/sdk/src/userStats.ts +++ b/sdk/src/userStats.ts @@ -54,9 +54,14 @@ export class UserStats { }, config.accountSubscription.commitment ); + } else if (config.accountSubscription?.type === 'custom') { + this.accountSubscriber = + config.accountSubscription.userStatsAccountSubscriber; } else { + const exhaustiveCheck: never = config.accountSubscription; + throw new Error( - `Unknown user stats account subscription type: ${config.accountSubscription?.type}` + `Unknown user stats account subscription type: ${exhaustiveCheck}` ); } } diff --git a/sdk/src/userStatsConfig.ts b/sdk/src/userStatsConfig.ts index 693de80e85..2a8f1ce813 100644 --- a/sdk/src/userStatsConfig.ts +++ b/sdk/src/userStatsConfig.ts @@ -1,7 +1,7 @@ import { DriftClient } from './driftClient'; import { Commitment, PublicKey } from '@solana/web3.js'; import { BulkAccountLoader } from './accounts/bulkAccountLoader'; -import { GrpcConfigs } from './accounts/types'; +import { GrpcConfigs, UserStatsAccountSubscriber } from './accounts/types'; export type UserStatsConfig = { accountSubscription?: UserStatsSubscriptionConfig; @@ -22,6 +22,7 @@ export type UserStatsSubscriptionConfig = } | { type: 'custom'; + userStatsAccountSubscriber: UserStatsAccountSubscriber; } | { type: 'grpc'; diff --git a/sdk/src/wallet.ts b/sdk/src/wallet.ts index c7ad92f4a2..05c5759a17 100644 --- a/sdk/src/wallet.ts +++ b/sdk/src/wallet.ts @@ -5,6 +5,7 @@ import { VersionedTransaction, } from '@solana/web3.js'; import { IWallet, IVersionedWallet } from './types'; +import nacl from 'tweetnacl'; export class Wallet implements IWallet, IVersionedWallet { constructor(readonly payer: Keypair) {} @@ -41,3 +42,13 @@ export class Wallet implements IWallet, IVersionedWallet { return this.payer.publicKey; } } + +export class WalletV2 extends Wallet { + constructor(readonly payer: Keypair) { + super(payer); + } + + async signMessage(message: Uint8Array): Promise { + return Buffer.from(nacl.sign.detached(message, this.payer.secretKey)); + } +} From 1f63c6e7159d326a57dfbaae0e45a69a5b6f0c19 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 04:59:25 +0000 Subject: [PATCH 068/247] sdk: release v2.141.0-beta.6 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 7dc981a2fc..41df0faa55 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.141.0-beta.5 \ No newline at end of file +2.141.0-beta.6 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 02fbae1e48..f90fce70bb 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0-beta.5", + "version": "2.141.0-beta.6", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 4a17883866fcf152004e8ac5f27d91468ae803b3 Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 2 Oct 2025 23:06:40 -0700 Subject: [PATCH 069/247] make working devcontainer and dockerfile (#1919) * make working devcontainer and dockerfile * fix node version * run as root * use final verison of devcontainer and dockerfile --------- Co-authored-by: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> --- .devcontainer/Dockerfile | 85 ++++++++++++++++----------------- .devcontainer/devcontainer.json | 55 ++++++++++++++++++++- README.md | 42 ++++++++++++++++ 3 files changed, 135 insertions(+), 47 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2d4688614c..39d1ca340f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,54 +1,49 @@ # -# Docker image to generate deterministic, verifiable builds of Anchor programs. -# This must be run *after* a given ANCHOR_CLI version is published and a git tag -# is released on GitHub. +# Drift Protocol Dev Container # -FROM rust:1.75 +FROM --platform=linux/amd64 rust:1.70.0 ARG DEBIAN_FRONTEND=noninteractive - -ARG SOLANA_CLI="1.14.7" -ARG ANCHOR_CLI="0.26.0" -ARG NODE_VERSION="v18.16.0" +ARG SOLANA_CLI="1.16.27" +ARG ANCHOR_CLI="0.29.0" +ARG NODE_VERSION="20.18.1" ENV HOME="/root" -ENV PATH="${HOME}/.cargo/bin:${PATH}" -ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}" -ENV PATH="${HOME}/.nvm/versions/node/${NODE_VERSION}/bin:${PATH}" - -# Install base utilities. -RUN mkdir -p /workdir && mkdir -p /tmp && \ - apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \ - build-essential git curl wget jq pkg-config python3-pip \ - libssl-dev libudev-dev - -RUN wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb -RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb - -# Install rust. -RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \ - sh rustup.sh -y && \ - rustup component add rustfmt clippy - -# Install node / npm / yarn. -RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash -ENV NVM_DIR="${HOME}/.nvm" -RUN . $NVM_DIR/nvm.sh && \ - nvm install ${NODE_VERSION} && \ - nvm use ${NODE_VERSION} && \ - nvm alias default node && \ - npm install -g yarn && \ - yarn add ts-mocha - -# Install Solana tools. -RUN sh -c "$(curl -sSfL https://release.solana.com/v${SOLANA_CLI}/install)" - -# Install anchor. -RUN cargo install --git https://github.com/coral-xyz/anchor avm --locked --force -RUN avm install ${ANCHOR_CLI} && avm use ${ANCHOR_CLI} - -RUN solana-keygen new --no-bip39-passphrase +ENV PATH="/usr/local/cargo/bin:${PATH}" +ENV PATH="/root/.local/share/solana/install/active_release/bin:${PATH}" + +RUN mkdir -p /workdir /tmp && \ + apt-get update -qq && apt-get upgrade -qq && apt-get install -y --no-install-recommends \ + build-essential git curl wget jq pkg-config python3-pip xz-utils ca-certificates \ + libssl-dev libudev-dev bash && \ + rm -rf /var/lib/apt/lists/* + +RUN rustup install 1.78.0 \ + && rustup component add rustfmt clippy --toolchain 1.78.0 + +RUN curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o /tmp/node.tar.xz \ + && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \ + && rm /tmp/node.tar.xz \ + && corepack enable \ + && npm install -g ts-mocha typescript mocha \ + && node -v && npm -v && yarn -v + +# Solana CLI (x86_64 build) +RUN curl -sSfL "https://github.com/solana-labs/solana/releases/download/v${SOLANA_CLI}/solana-release-x86_64-unknown-linux-gnu.tar.bz2" \ + | tar -xjC /tmp \ + && mv /tmp/solana-release/bin/* /usr/local/bin/ \ + && rm -rf /tmp/solana-release + +# Anchor CLI +RUN cargo install --git https://github.com/coral-xyz/anchor --tag "v${ANCHOR_CLI}" anchor-cli --locked + +# Set up Solana key + config for root +RUN solana-keygen new --no-bip39-passphrase --force \ + && solana config set --url localhost + +RUN apt-get update && apt-get install -y zsh curl git \ + && sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended \ + && chsh -s /usr/bin/zsh root WORKDIR /workdir -#be sure to add `/root/.avm/bin` to your PATH to be able to run the installed binaries diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d5aa4e718b..3228a43367 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,3 +1,54 @@ { - "build": { "dockerfile": "Dockerfile" }, - } \ No newline at end of file + "name": "Drift Protocol Development", + "build": { + "dockerfile": "Dockerfile", + "platform": "linux/amd64" + }, + "workspaceFolder": "/workdir", + "remoteUser": "root", + "mounts": [ + "source=${localWorkspaceFolder},target=/workdir,type=bind,consistency=cached", + "source=drift-target,target=/workdir/target,type=volume,consistency=delegated" + ], + "postCreateCommand": "yarn config set ignore-package-manager true && echo 'Dev container ready! You can now run: anchor build, anchor test, cargo build, etc.'", + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer", + "ms-vscode.vscode-json", + "tamasfe.even-better-toml" + ], + "settings": { + "rust-analyzer.cachePriming.numThreads": 1, + "rust-analyzer.cargo.buildScripts.enable": true, + "rust-analyzer.procMacro.enable": true, + "rust-analyzer.checkOnSave": true, + "rust-analyzer.check.command": "clippy", + "rust-analyzer.server.extraEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + "RUSTUP_TOOLCHAIN": "1.78.0-x86_64-unknown-linux-gnu" + }, + "editor.formatOnSave": true, + "git.ignoreLimitWarning": true + } + } + }, + "forwardPorts": [ + 8899, + 8900 + ], + "portsAttributes": { + "8899": { + "label": "Solana Test Validator", + "onAutoForward": "notify" + }, + "8900": { + "label": "Solana Test Validator RPC", + "onAutoForward": "notify" + } + }, + "containerEnv": { + "ANCHOR_WALLET": "/root/.config/solana/id.json", + "RUST_LOG": "solana_runtime::message_processor::stable_log=debug" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 28e340d853..0bb99c9b0a 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,48 @@ cargo test bash test-scripts/run-anchor-tests.sh ``` +# Development (with devcontainer) + +We've provided a devcontainer `Dockerfile` to help you spin up a dev environment with the correct versions of Rust, Solana, and Anchor for program development. + +Build the container and tag it `drift-dev`: +``` +cd .devcontainer && docker build -t drift-dev . +``` + +Open a shell to the container: +``` +# Find the container ID first +docker ps + +# Then exec into it +docker exec -it /bin/bash +``` + +Alternatively use an extension provided by your IDE to make use of the dev container. For example on vscode/cursor: + +``` +1. Press Ctrl+Shift+P (or Cmd+Shift+P on Mac) +2. Type "Dev Containers: Reopen in Container" +3. Select it and wait for the container to build +4. The IDE terminal should be targeting the dev container now +``` + +Use the dev container as you would a local build environment: +``` +# build program +anchor build + +# update idl +anchor build -- --features anchor-test && cp target/idl/drift.json sdk/src/idl/drift.json + +# run cargo tests +cargo test + +# run typescript tests +bash test-scripts/run-anchor-tests.sh +``` + # Bug Bounty Information about the Bug Bounty can be found [here](./bug-bounty/README.md) From e38d04f223942e3d410e5330c1c08ceac9ab17d8 Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Fri, 3 Oct 2025 02:07:46 -0400 Subject: [PATCH 070/247] sdk: add devnet-spot-market-index-8 (#1920) --- sdk/src/constants/spotMarkets.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 349a60893c..3fc5eb9cad 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -132,7 +132,19 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ { symbol: 'GLXY', marketIndex: 7, - poolId: 0, + poolId: 2, + oracle: new PublicKey('4wFrjUQHzRBc6qjVtMDbt28aEVgn6GaNiWR6vEff4KxR'), + oracleSource: OracleSource.Prelaunch, + mint: new PublicKey('2vVfXmcWXEaFzp7iaTVnQ4y1gR41S6tJQQMo1S5asJyC'), + precision: new BN(10).pow(SIX), + precisionExp: SIX, + pythFeedId: + '0x67e031d1723e5c89e4a826d80b2f3b41a91b05ef6122d523b8829a02e0f563aa', + }, + { + symbol: 'GLXY', + marketIndex: 8, + poolId: 2, oracle: new PublicKey('4wFrjUQHzRBc6qjVtMDbt28aEVgn6GaNiWR6vEff4KxR'), oracleSource: OracleSource.Prelaunch, mint: new PublicKey('2vVfXmcWXEaFzp7iaTVnQ4y1gR41S6tJQQMo1S5asJyC'), From 56282c333ff4b28367d5ff8f3f2f314635defa3b Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 2 Oct 2025 23:09:17 -0700 Subject: [PATCH 071/247] Revert "fix ci sdk check" This reverts commit c507bcaebc5d921b217378274e927cc2b5566e18. --- sdk/src/constants/spotMarkets.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 3fc5eb9cad..70e152a323 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -153,16 +153,6 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ pythFeedId: '0x67e031d1723e5c89e4a826d80b2f3b41a91b05ef6122d523b8829a02e0f563aa', }, - { - symbol: 'bSOL', - marketIndex: 8, - poolId: 2, - oracle: new PublicKey('4wFrjUQHzRBc6qjVtMDbt28aEVgn6GaNiWR6vEff4KxR'), - oracleSource: OracleSource.Prelaunch, - mint: new PublicKey('2vVfXmcWXEaFzp7iaTVnQ4y1gR41S6tJQQMo1S5asJyC'), - precision: new BN(10).pow(NINE), - precisionExp: NINE, - }, ]; export const MainnetSpotMarkets: SpotMarketConfig[] = [ From af81cc8050daf43d67b829d52d6e3293c3b9a263 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:12:47 +0000 Subject: [PATCH 072/247] sdk: release v2.141.0-beta.7 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 41df0faa55..a0fd895927 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.141.0-beta.6 \ No newline at end of file +2.141.0-beta.7 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index f90fce70bb..6d00439c8b 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0-beta.6", + "version": "2.141.0-beta.7", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From b6190220c35cdc907f4f626d25e02b536b510b0a Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Fri, 3 Oct 2025 14:51:21 +0800 Subject: [PATCH 073/247] refactor(sdk): add IWalletV2 --- sdk/src/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/src/types.ts b/sdk/src/types.ts index d48b51b9bf..8c08954f20 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1410,6 +1410,10 @@ export interface IVersionedWallet { payer?: Keypair; } +export interface IWalletV2 extends IWallet { + signMessage(message: Uint8Array): Promise; +} + export type FeeStructure = { feeTiers: FeeTier[]; fillerRewardStructure: OrderFillerRewardStructure; From b20aa567632fba8c7f136b7f21409d8fdd5ab81c Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:56:29 +0000 Subject: [PATCH 074/247] sdk: release v2.141.0-beta.8 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index a0fd895927..69e4dcada9 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.141.0-beta.7 \ No newline at end of file +2.141.0-beta.8 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 6d00439c8b..34a9b3d890 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0-beta.7", + "version": "2.141.0-beta.8", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From ad749ad7def829d73ab5f895fd05d144c6de96b2 Mon Sep 17 00:00:00 2001 From: wphan Date: Fri, 3 Oct 2025 00:07:53 -0700 Subject: [PATCH 075/247] dont panic on settle-pnl when no position (#1928) * dont panic on settle-pnl when no position * move check inside settle_pnl, ensure balance checks are done * remove unnecessary error map --- programs/drift/src/controller/pnl.rs | 11 ++++++++++- programs/drift/src/instructions/keeper.rs | 12 ++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index c46d46763d..c08912beb5 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -79,7 +79,16 @@ pub fn settle_pnl( drop(market); - let position_index = get_position_index(&user.perp_positions, market_index)?; + let position_index = match get_position_index(&user.perp_positions, market_index) { + Ok(index) => index, + Err(e) => { + return mode.result( + e, + market_index, + &format!("User has no position in market {}", market_index), + ) + } + }; let unrealized_pnl = user.perp_positions[position_index].get_unrealized_pnl(oracle_price)?; // cannot settle negative pnl this way on a user who is in liquidation territory diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 3fb3db113a..cc8c87b321 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1104,8 +1104,7 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( &mut oracle_map, state, &clock, - ) - .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; + )?; controller::pnl::settle_pnl( market_index, @@ -1119,8 +1118,7 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( state, None, SettlePnlMode::MustSettle, - ) - .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; + )?; } if state.builder_codes_enabled() || state.builder_referral_enabled() { @@ -1220,8 +1218,7 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( &mut oracle_map, state, &clock, - ) - .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; + )?; controller::pnl::settle_pnl( *market_index, @@ -1235,8 +1232,7 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( state, Some(meets_margin_requirement), mode, - ) - .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; + )?; } if state.builder_codes_enabled() || state.builder_referral_enabled() { From 8bd019530b4e5ce4ef5ce6192e56bfa88af767ea Mon Sep 17 00:00:00 2001 From: wphan Date: Fri, 3 Oct 2025 00:08:04 -0700 Subject: [PATCH 076/247] program: disallow builder to be escrow authority (#1930) --- programs/drift/src/instructions/user.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index d49bc5ea62..db9b9f4268 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -582,6 +582,12 @@ pub fn handle_change_approved_builder<'c: 'info, 'info>( max_fee_tenth_bps: u16, add: bool, ) -> Result<()> { + validate!( + ctx.accounts.escrow.authority != builder, + ErrorCode::DefaultError, + "Builder cannot be the same as the escrow authority" + )?; + let existing_builder_index = ctx .accounts .escrow From c453b2a05de14e8506f9fa14bb728d05b243563d Mon Sep 17 00:00:00 2001 From: wphan Date: Fri, 3 Oct 2025 00:21:16 -0700 Subject: [PATCH 077/247] v2.141.0 --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec43602a87..6231c7a14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking +## [2.141.0] - 2025-10-03 + +### Features + +- program: disallow builder to be escrow authority ([#1930](https://github.com/drift-labs/protocol-v2/pull/1930)) +- dont panic on settle-pnl when no position ([#1928](https://github.com/drift-labs/protocol-v2/pull/1928)) + +### Fixes + +### Breaking + ## [2.140.0] - 2025-09-29 ### Features diff --git a/Cargo.lock b/Cargo.lock index 1f28352549..10989da811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.140.0" +version = "2.141.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 9da71196a1..7771b9c8e3 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.140.0" +version = "2.141.0" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index 34a9b3d890..76d90fcb64 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0-beta.8", + "version": "2.141.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 9db0b9deb3..837ac89677 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.140.0", + "version": "2.141.0", "name": "drift", "instructions": [ { From 1c281f165e489364caafbb3db77a4e905a3ee00e Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 07:30:04 +0000 Subject: [PATCH 078/247] sdk: release v2.142.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 69e4dcada9..48994d5802 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.141.0-beta.8 \ No newline at end of file +2.142.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 76d90fcb64..9f0e63596b 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.141.0", + "version": "2.142.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 6cd5194d1e84bb951427a716957c327fb5efa16a Mon Sep 17 00:00:00 2001 From: wphan Date: Fri, 3 Oct 2025 11:13:30 -0700 Subject: [PATCH 079/247] sdk: add missing builder export --- sdk/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 66c7ca9539..ea511618ab 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -54,6 +54,7 @@ export * from './events/parse'; export * from './events/pollingLogProvider'; export * from './jupiter/jupiterClient'; export * from './math/auction'; +export * from './math/builder'; export * from './math/spotMarket'; export * from './math/conversion'; export * from './math/exchangeStatus'; From d56a23a6f9ba8ce20b59f56e8dad5fedfcddaeda Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:18:12 +0000 Subject: [PATCH 080/247] sdk: release v2.142.0-beta.1 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 48994d5802..0a5513f8fe 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.0 \ No newline at end of file +2.142.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 9f0e63596b..b93072ba20 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.0", + "version": "2.142.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From da627e1187cbdc980ed18e217f593bf1526c294d Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Fri, 3 Oct 2025 13:49:12 -0600 Subject: [PATCH 081/247] feat: batch accounts for less connects grpc drift client subscriber (#1922) * feat: batch accounts for less connects grpc drift client subscriber * fix: lint issues and package upgrades * feat: group oracle accounts under 1 grpc connection * feat: separate multi grpc subscriber into v2 and test script * fix: broken test script * fix: unhandled resub logic * fix: lint and prettify * fix: more formatting --- sdk/bun.lock | 35 +- sdk/package.json | 4 +- sdk/scripts/client-test.ts | 87 +++++ sdk/src/accounts/grpcAccountSubscriber.ts | 15 +- .../grpcDriftClientAccountSubscriber.ts | 2 +- .../grpcDriftClientAccountSubscriberV2.ts | 294 ++++++++++++++++ .../accounts/grpcMultiAccountSubscriber.ts | 328 ++++++++++++++++++ sdk/src/driftClient.ts | 7 +- sdk/src/driftClientConfig.ts | 13 + sdk/src/isomorphic/grpc.node.ts | 18 +- sdk/yarn.lock | 28 +- 11 files changed, 794 insertions(+), 37 deletions(-) create mode 100644 sdk/scripts/client-test.ts create mode 100644 sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts create mode 100644 sdk/src/accounts/grpcMultiAccountSubscriber.ts diff --git a/sdk/bun.lock b/sdk/bun.lock index c3292b4520..77e0416d67 100644 --- a/sdk/bun.lock +++ b/sdk/bun.lock @@ -7,7 +7,7 @@ "@coral-xyz/anchor": "0.29.0", "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@ellipsis-labs/phoenix-sdk": "1.4.5", - "@grpc/grpc-js": "1.12.6", + "@grpc/grpc-js": "1.14.0", "@openbook-dex/openbook-v2": "0.2.10", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "2.5.3", @@ -17,7 +17,7 @@ "@solana/web3.js": "1.98.0", "@switchboard-xyz/common": "3.0.14", "@switchboard-xyz/on-demand": "2.4.1", - "@triton-one/yellowstone-grpc": "1.3.0", + "@triton-one/yellowstone-grpc": "1.4.1", "anchor-bankrun": "0.3.0", "gill": "^0.10.2", "helius-laserstream": "0.1.8", @@ -59,15 +59,24 @@ }, }, "overrides": { - "debug": "<4.4.2", - "supports-color": "7.2.0", "ansi-regex": "5.0.1", - "color-convert": "<3.1.1", "ansi-styles": "4.3.0", - "wrap-ansi": "7.0.0", + "backslash": "<0.2.1", "chalk": "4.1.2", - "strip-ansi": "6.0.1", + "chalk-template": "<1.1.1", + "color-convert": "<3.1.1", "color-name": "<2.0.1", + "color-string": "<2.1.1", + "debug": "<4.4.2", + "error-ex": "<1.3.3", + "has-ansi": "<6.0.1", + "is-arrayish": "<0.3.3", + "simple-swizzle": "<0.2.3", + "slice-ansi": "3.0.0", + "strip-ansi": "6.0.1", + "supports-color": "7.2.0", + "supports-hyperlinks": "<4.1.1", + "wrap-ansi": "7.0.0", }, "packages": { "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], @@ -104,9 +113,9 @@ "@eslint/js": ["@eslint/js@8.57.0", "", {}, "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.0", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg=="], - "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.11.14", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg=="], @@ -294,7 +303,7 @@ "@switchboard-xyz/on-demand": ["@switchboard-xyz/on-demand@2.4.1", "", { "dependencies": { "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@isaacs/ttlcache": "^1.4.1", "@switchboard-xyz/common": ">=3.0.0", "axios": "^1.8.3", "bs58": "^6.0.0", "buffer": "^6.0.3", "js-yaml": "^4.1.0" } }, "sha512-eSlBp+c8lxpcSgh0/2xK8OaLHPziTSZlcs8V96gZGdiCJz1KgWJRNE1qnIJDOwaGdFecZdwcmajfQRtLRLED3w=="], - "@triton-one/yellowstone-grpc": ["@triton-one/yellowstone-grpc@1.3.0", "", { "dependencies": { "@grpc/grpc-js": "^1.8.0" } }, "sha512-tuwHtoYzvqnahsMrecfNNkQceCYwgiY0qKS8RwqtaxvDEgjm0E+0bXwKz2eUD3ZFYifomJmRKDmSBx9yQzAeMQ=="], + "@triton-one/yellowstone-grpc": ["@triton-one/yellowstone-grpc@1.4.1", "", { "dependencies": { "@grpc/grpc-js": "^1.8.0" } }, "sha512-ZN49vooxFbOqWttll8u7AOsIVnX+srqX9ddhZ9ttE+OcehUo8c2p2suK8Gr2puab49cgsV0VGjiTn9Gua/ntIw=="], "@ts-graphviz/adapter": ["@ts-graphviz/adapter@2.0.6", "", { "dependencies": { "@ts-graphviz/common": "^2.1.5" } }, "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q=="], @@ -1176,6 +1185,8 @@ "@ellipsis-labs/phoenix-sdk/bs58": ["bs58@5.0.0", "", { "dependencies": { "base-x": "^4.0.0" } }, "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ=="], + "@grpc/proto-loader/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@metaplex-foundation/beet-solana/bs58": ["bs58@5.0.0", "", { "dependencies": { "base-x": "^4.0.0" } }, "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ=="], "@metaplex-foundation/solita/@metaplex-foundation/beet": ["@metaplex-foundation/beet@0.4.0", "", { "dependencies": { "ansicolors": "^0.3.2", "bn.js": "^5.2.0", "debug": "^4.3.3" } }, "sha512-2OAKJnLatCc3mBXNL0QmWVQKAWK2C7XDfepgL0p/9+8oSx4bmRAFHFqptl1A/C0U5O3dxGwKfmKluW161OVGcA=="], @@ -1248,6 +1259,8 @@ "jayson/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "jito-ts/@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], + "jito-ts/@solana/web3.js": ["@solana/web3.js@1.77.4", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@noble/curves": "^1.0.0", "@noble/hashes": "^1.3.0", "@solana/buffer-layout": "^4.0.0", "agentkeepalive": "^4.2.1", "bigint-buffer": "^1.1.5", "bn.js": "^5.0.0", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.0", "node-fetch": "^2.6.7", "rpc-websockets": "^7.5.1", "superstruct": "^0.14.2" } }, "sha512-XdN0Lh4jdY7J8FYMyucxCwzn6Ga2Sr1DHDWRbqVzk7ZPmmpSPOVWHzO67X1cVT+jNi1D6gZi2tgjHgDPuj6e9Q=="], "jito-ts/dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], @@ -1332,6 +1345,8 @@ "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "jito-ts/@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], + "jito-ts/@solana/web3.js/superstruct": ["superstruct@0.14.2", "", {}, "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ=="], "mocha/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], diff --git a/sdk/package.json b/sdk/package.json index b93072ba20..c9e614f477 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -42,7 +42,7 @@ "@coral-xyz/anchor": "0.29.0", "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@ellipsis-labs/phoenix-sdk": "1.4.5", - "@grpc/grpc-js": "1.12.6", + "@grpc/grpc-js": "1.14.0", "@openbook-dex/openbook-v2": "0.2.10", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "2.5.3", @@ -52,7 +52,7 @@ "@solana/web3.js": "1.98.0", "@switchboard-xyz/common": "3.0.14", "@switchboard-xyz/on-demand": "2.4.1", - "@triton-one/yellowstone-grpc": "1.3.0", + "@triton-one/yellowstone-grpc": "1.4.1", "anchor-bankrun": "0.3.0", "gill": "^0.10.2", "helius-laserstream": "0.1.8", diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/client-test.ts new file mode 100644 index 0000000000..79ba3d2915 --- /dev/null +++ b/sdk/scripts/client-test.ts @@ -0,0 +1,87 @@ +import { DriftClient } from '../src/driftClient'; +import { grpcDriftClientAccountSubscriberV2 } from '../src/accounts/grpcDriftClientAccountSubscriberV2'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { DriftClientConfig } from '../src/driftClientConfig'; +import { decodeName, DRIFT_PROGRAM_ID, Wallet } from '../src'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; +import dotenv from 'dotenv'; + +const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; +const TOKEN = process.env.TOKEN; + +async function initializeGrpcDriftClientV2() { + const connection = new Connection('https://api.mainnet-beta.solana.com'); + const wallet = new Wallet(new Keypair()); + dotenv.config({ path: '../' }); + const config: DriftClientConfig = { + connection, + wallet, + programID: new PublicKey(DRIFT_PROGRAM_ID), + accountSubscription: { + type: 'grpc', + grpcConfigs: { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: 'confirmed' as unknown as CommitmentLevel, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }, + driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, + }, + perpMarketIndexes: [0, 1, 2], // Example market indexes + spotMarketIndexes: [0, 1, 2], // Example market indexes + oracleInfos: [], // Add oracle information if needed + }; + + const driftClient = new DriftClient(config); + + let perpMarketUpdateCount = 0; + let spotMarketUpdateCount = 0; + let oraclePriceUpdateCount = 0; + let userAccountUpdateCount = 0; + + const updatePromise = new Promise((resolve) => { + driftClient.accountSubscriber.eventEmitter.on('perpMarketAccountUpdate', (data) => { + console.log('Perp market account update:', decodeName(data.name)); + perpMarketUpdateCount++; + if (perpMarketUpdateCount >= 10 && spotMarketUpdateCount >= 10 && oraclePriceUpdateCount >= 10 && userAccountUpdateCount >= 2) { + resolve(); + } + }); + + driftClient.accountSubscriber.eventEmitter.on('spotMarketAccountUpdate', (data) => { + console.log('Spot market account update:', decodeName(data.name)); + spotMarketUpdateCount++; + if (perpMarketUpdateCount >= 10 && spotMarketUpdateCount >= 10 && oraclePriceUpdateCount >= 10 && userAccountUpdateCount >= 2) { + resolve(); + } + }); + + driftClient.accountSubscriber.eventEmitter.on('oraclePriceUpdate', (data) => { + console.log('Oracle price update:', data.toBase58()); + oraclePriceUpdateCount++; + if (perpMarketUpdateCount >= 10 && spotMarketUpdateCount >= 10 && oraclePriceUpdateCount >= 10 && userAccountUpdateCount >= 2) { + resolve(); + } + }); + + driftClient.accountSubscriber.eventEmitter.on('userAccountUpdate', (data) => { + console.log('User account update:', decodeName(data.name)); + userAccountUpdateCount++; + if (perpMarketUpdateCount >= 10 && spotMarketUpdateCount >= 10 && oraclePriceUpdateCount >= 10 && userAccountUpdateCount >= 2) { + resolve(); + } + }); + }); + + await driftClient.subscribe(); + console.log('DriftClient initialized and listening for updates.'); + + await updatePromise; + console.log('Received required number of updates.'); +} + +initializeGrpcDriftClientV2().catch(console.error); diff --git a/sdk/src/accounts/grpcAccountSubscriber.ts b/sdk/src/accounts/grpcAccountSubscriber.ts index be141c3746..50adb2f318 100644 --- a/sdk/src/accounts/grpcAccountSubscriber.ts +++ b/sdk/src/accounts/grpcAccountSubscriber.ts @@ -39,13 +39,16 @@ export class grpcAccountSubscriber extends WebSocketAccountSubscriber { program: Program, accountPublicKey: PublicKey, decodeBuffer?: (buffer: Buffer) => U, - resubOpts?: ResubOpts + resubOpts?: ResubOpts, + clientProp?: Client ): Promise> { - const client = await createClient( - grpcConfigs.endpoint, - grpcConfigs.token, - grpcConfigs.channelOptions ?? {} - ); + const client = clientProp + ? clientProp + : await createClient( + grpcConfigs.endpoint, + grpcConfigs.token, + grpcConfigs.channelOptions ?? {} + ); const commitmentLevel = // @ts-ignore :: isomorphic exported enum fails typescript but will work at runtime grpcConfigs.commitmentLevel ?? CommitmentLevel.CONFIRMED; diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriber.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriber.ts index 8545b5f029..31d34e32ca 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriber.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriber.ts @@ -12,7 +12,7 @@ import { grpcAccountSubscriber } from './grpcAccountSubscriber'; import { PerpMarketAccount, SpotMarketAccount, StateAccount } from '../types'; import { getOracleId } from '../oracles/oracleId'; -export class gprcDriftClientAccountSubscriber extends WebSocketDriftClientAccountSubscriber { +export class grpcDriftClientAccountSubscriber extends WebSocketDriftClientAccountSubscriber { private grpcConfigs: GrpcConfigs; constructor( diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts new file mode 100644 index 0000000000..dcd2b1aeae --- /dev/null +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -0,0 +1,294 @@ +import { WebSocketDriftClientAccountSubscriber } from './webSocketDriftClientAccountSubscriber'; +import { OracleInfo, OraclePriceData } from '../oracles/types'; +import { Program } from '@coral-xyz/anchor'; +import { PublicKey } from '@solana/web3.js'; +import { findAllMarketAndOracles } from '../config'; +import { + getDriftStateAccountPublicKey, + getPerpMarketPublicKey, + getSpotMarketPublicKey, +} from '../addresses/pda'; +import { DelistedMarketSetting, GrpcConfigs, ResubOpts } from './types'; +import { grpcAccountSubscriber } from './grpcAccountSubscriber'; +import { grpcMultiAccountSubscriber } from './grpcMultiAccountSubscriber'; +import { PerpMarketAccount, SpotMarketAccount, StateAccount } from '../types'; +import { getOracleId } from '../oracles/oracleId'; + +export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAccountSubscriber { + private grpcConfigs: GrpcConfigs; + private perpMarketsSubscriber?: grpcMultiAccountSubscriber; + private spotMarketsSubscriber?: grpcMultiAccountSubscriber; + private oracleMultiSubscriber?: grpcMultiAccountSubscriber; + + constructor( + grpcConfigs: GrpcConfigs, + program: Program, + perpMarketIndexes: number[], + spotMarketIndexes: number[], + oracleInfos: OracleInfo[], + shouldFindAllMarketsAndOracles: boolean, + delistedMarketSetting: DelistedMarketSetting, + resubOpts?: ResubOpts + ) { + super( + program, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + shouldFindAllMarketsAndOracles, + delistedMarketSetting, + resubOpts + ); + this.grpcConfigs = grpcConfigs; + } + + public async subscribe(): Promise { + if (this.isSubscribed) { + return true; + } + + if (this.isSubscribing) { + return await this.subscriptionPromise; + } + + this.isSubscribing = true; + + this.subscriptionPromise = new Promise((res) => { + this.subscriptionPromiseResolver = res; + }); + + if (this.shouldFindAllMarketsAndOracles) { + const { + perpMarketIndexes, + perpMarketAccounts, + spotMarketIndexes, + spotMarketAccounts, + oracleInfos, + } = await findAllMarketAndOracles(this.program); + this.perpMarketIndexes = perpMarketIndexes; + this.spotMarketIndexes = spotMarketIndexes; + this.oracleInfos = oracleInfos; + // front run and set the initial data here to save extra gma call in set initial data + this.initialPerpMarketAccountData = new Map( + perpMarketAccounts.map((market) => [market.marketIndex, market]) + ); + this.initialSpotMarketAccountData = new Map( + spotMarketAccounts.map((market) => [market.marketIndex, market]) + ); + } + + const statePublicKey = await getDriftStateAccountPublicKey( + this.program.programId + ); + + // create and activate main state account subscription + this.stateAccountSubscriber = + await grpcAccountSubscriber.create( + this.grpcConfigs, + 'state', + this.program, + statePublicKey, + undefined, + undefined + ); + await this.stateAccountSubscriber.subscribe((data: StateAccount) => { + this.eventEmitter.emit('stateAccountUpdate', data); + this.eventEmitter.emit('update'); + }); + + // set initial data to avoid spamming getAccountInfo calls in webSocketAccountSubscriber + await this.setInitialData(); + + // subscribe to perp + spot markets (separate) and oracles + await Promise.all([ + this.subscribeToPerpMarketAccounts(), + this.subscribeToSpotMarketAccounts(), + this.subscribeToOracles(), + ]); + + this.eventEmitter.emit('update'); + + await this.handleDelistedMarkets(); + + await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]); + + this.subscriptionPromiseResolver(true); + + this.isSubscribing = false; + this.isSubscribed = true; + + // delete initial data + this.removeInitialData(); + + return true; + } + + override async subscribeToPerpMarketAccounts(): Promise { + const perpMarketPubkeys = await Promise.all( + this.perpMarketIndexes.map((marketIndex) => + getPerpMarketPublicKey(this.program.programId, marketIndex) + ) + ); + + this.perpMarketsSubscriber = + await grpcMultiAccountSubscriber.create( + this.grpcConfigs, + 'perpMarket', + this.program, + undefined, + this.resubOpts, + undefined, + async () => { + try { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] perp markets subscriber unsubscribed; resubscribing' + ); + } + await this.subscribeToPerpMarketAccounts(); + } catch (e) { + console.error('Perp markets resubscribe failed:', e); + } + } + ); + await this.perpMarketsSubscriber.subscribe( + perpMarketPubkeys, + (_accountId, data) => { + this.eventEmitter.emit( + 'perpMarketAccountUpdate', + data as PerpMarketAccount + ); + this.eventEmitter.emit('update'); + } + ); + + return true; + } + + override async subscribeToSpotMarketAccounts(): Promise { + const spotMarketPubkeys = await Promise.all( + this.spotMarketIndexes.map((marketIndex) => + getSpotMarketPublicKey(this.program.programId, marketIndex) + ) + ); + + this.spotMarketsSubscriber = + await grpcMultiAccountSubscriber.create( + this.grpcConfigs, + 'spotMarket', + this.program, + undefined, + this.resubOpts, + undefined, + async () => { + try { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] spot markets subscriber unsubscribed; resubscribing' + ); + } + await this.subscribeToSpotMarketAccounts(); + } catch (e) { + console.error('Spot markets resubscribe failed:', e); + } + } + ); + await this.spotMarketsSubscriber.subscribe( + spotMarketPubkeys, + (_accountId, data) => { + this.eventEmitter.emit( + 'spotMarketAccountUpdate', + data as SpotMarketAccount + ); + this.eventEmitter.emit('update'); + } + ); + + return true; + } + + override async subscribeToOracles(): Promise { + // Build list of unique oracle pubkeys and a lookup for sources + const uniqueOraclePubkeys = new Map(); + for (const info of this.oracleInfos) { + const id = getOracleId(info.publicKey, info.source); + if ( + !uniqueOraclePubkeys.has(id) && + !info.publicKey.equals((PublicKey as any).default) + ) { + uniqueOraclePubkeys.set(id, info); + } + } + + const oraclePubkeys = Array.from(uniqueOraclePubkeys.values()).map( + (i) => i.publicKey + ); + const pubkeyToSource = new Map( + Array.from(uniqueOraclePubkeys.values()).map((i) => [ + i.publicKey.toBase58(), + i.source, + ]) + ); + + this.oracleMultiSubscriber = + await grpcMultiAccountSubscriber.create( + this.grpcConfigs, + 'oracle', + this.program, + (buffer: Buffer, pubkey?: string) => { + if (!pubkey) { + throw new Error('Oracle pubkey missing in decode'); + } + const source = pubkeyToSource.get(pubkey); + const client = this.oracleClientCache.get( + source, + this.program.provider.connection, + this.program + ); + return client.getOraclePriceDataFromBuffer(buffer); + }, + this.resubOpts, + undefined, + async () => { + try { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] oracle subscriber unsubscribed; resubscribing' + ); + } + await this.subscribeToOracles(); + } catch (e) { + console.error('Oracle resubscribe failed:', e); + } + } + ); + + await this.oracleMultiSubscriber.subscribe( + oraclePubkeys, + (accountId, data) => { + const source = pubkeyToSource.get(accountId.toBase58()); + this.eventEmitter.emit('oraclePriceUpdate', accountId, source, data); + this.eventEmitter.emit('update'); + } + ); + + return true; + } + + async unsubscribeFromOracles(): Promise { + if (this.oracleMultiSubscriber) { + await this.oracleMultiSubscriber.unsubscribe(); + this.oracleMultiSubscriber = undefined; + return; + } + await super.unsubscribeFromOracles(); + } + + override async unsubscribe(): Promise { + if (this.isSubscribed) { + return; + } + + await this.stateAccountSubscriber.unsubscribe(); + } +} diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts new file mode 100644 index 0000000000..ab5b09ce97 --- /dev/null +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -0,0 +1,328 @@ +import { Program } from '@coral-xyz/anchor'; +import { Context, PublicKey } from '@solana/web3.js'; +import * as Buffer from 'buffer'; +import bs58 from 'bs58'; + +import { + Client, + ClientDuplexStream, + CommitmentLevel, + SubscribeRequest, + SubscribeUpdate, + createClient, +} from '../isomorphic/grpc'; +import { GrpcConfigs, ResubOpts } from './types'; + +interface AccountInfoLike { + owner: PublicKey; + lamports: number; + data: Buffer; + executable: boolean; + rentEpoch: number; +} + +export class grpcMultiAccountSubscriber { + private client: Client; + private stream: ClientDuplexStream; + private commitmentLevel: CommitmentLevel; + private program: Program; + private accountName: string; + private decodeBufferFn?: (buffer: Buffer, pubkey?: string) => T; + private resubOpts?: ResubOpts; + private onUnsubscribe?: () => Promise; + + public listenerId?: number; + public isUnsubscribing = false; + private timeoutId?: ReturnType; + private receivingData = false; + + private subscribedAccounts = new Set(); + private onChangeMap = new Map< + string, + (data: T, context: Context, buffer: Buffer) => void + >(); + + private constructor( + client: Client, + commitmentLevel: CommitmentLevel, + accountName: string, + program: Program, + decodeBuffer?: (buffer: Buffer, pubkey?: string) => T, + resubOpts?: ResubOpts, + onUnsubscribe?: () => Promise + ) { + this.client = client; + this.commitmentLevel = commitmentLevel; + this.accountName = accountName; + this.program = program; + this.decodeBufferFn = decodeBuffer; + this.resubOpts = resubOpts; + this.onUnsubscribe = onUnsubscribe; + } + + public static async create( + grpcConfigs: GrpcConfigs, + accountName: string, + program: Program, + decodeBuffer?: (buffer: Buffer, pubkey?: string) => U, + resubOpts?: ResubOpts, + clientProp?: Client, + onUnsubscribe?: () => Promise + ): Promise> { + const client = clientProp + ? clientProp + : await createClient( + grpcConfigs.endpoint, + grpcConfigs.token, + grpcConfigs.channelOptions ?? {} + ); + const commitmentLevel = + // @ts-ignore :: isomorphic exported enum fails typescript but will work at runtime + grpcConfigs.commitmentLevel ?? CommitmentLevel.CONFIRMED; + + return new grpcMultiAccountSubscriber( + client, + commitmentLevel, + accountName, + program, + decodeBuffer, + resubOpts, + onUnsubscribe + ); + } + + async subscribe( + accounts: PublicKey[], + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer + ) => void + ): Promise { + if (this.listenerId != null || this.isUnsubscribing) { + return; + } + + // Track accounts and single onChange for all + for (const pk of accounts) { + const key = pk.toBase58(); + this.subscribedAccounts.add(key); + this.onChangeMap.set(key, (data, ctx, buffer) => + onChange(new PublicKey(key), data, ctx, buffer) + ); + } + + this.stream = + (await this.client.subscribe()) as unknown as typeof this.stream; + const request: SubscribeRequest = { + slots: {}, + accounts: { + account: { + account: accounts.map((a) => a.toBase58()), + owner: [], + filters: [], + }, + }, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + commitment: this.commitmentLevel, + entry: {}, + transactionsStatus: {}, + }; + + this.stream.on('data', (chunk: SubscribeUpdate) => { + if (!chunk.account) { + return; + } + const slot = Number(chunk.account.slot); + const accountPubkeyBytes = chunk.account.account.pubkey; + const accountPubkey = bs58.encode( + accountPubkeyBytes as unknown as Uint8Array + ); + if (!accountPubkey || !this.subscribedAccounts.has(accountPubkey)) { + return; + } + const accountInfo: AccountInfoLike = { + owner: new PublicKey(chunk.account.account.owner), + lamports: Number(chunk.account.account.lamports), + data: Buffer.Buffer.from(chunk.account.account.data), + executable: chunk.account.account.executable, + rentEpoch: Number(chunk.account.account.rentEpoch), + }; + + const context = { slot } as Context; + const buffer = accountInfo.data; + const data = this.decodeBufferFn + ? this.decodeBufferFn(buffer, accountPubkey) + : this.program.account[this.accountName].coder.accounts.decode( + this.capitalize(this.accountName), + buffer + ); + + const handler = this.onChangeMap.get(accountPubkey); + if (handler) { + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + handler(data, context, buffer); + this.setTimeout(); + } else { + handler(data, context, buffer); + } + } + }); + + return new Promise((resolve, reject) => { + this.stream.write(request, (err) => { + if (err === null || err === undefined) { + this.listenerId = 1; + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + this.setTimeout(); + } + resolve(); + } else { + reject(err); + } + }); + }).catch((reason) => { + console.error(reason); + throw reason; + }); + } + + async addAccounts(accounts: PublicKey[]): Promise { + for (const pk of accounts) { + this.subscribedAccounts.add(pk.toBase58()); + } + const request: SubscribeRequest = { + slots: {}, + accounts: { + account: { + account: Array.from(this.subscribedAccounts.values()), + owner: [], + filters: [], + }, + }, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + commitment: this.commitmentLevel, + entry: {}, + transactionsStatus: {}, + }; + + await new Promise((resolve, reject) => { + this.stream.write(request, (err) => { + if (err === null || err === undefined) { + resolve(); + } else { + reject(err); + } + }); + }); + } + + async removeAccounts(accounts: PublicKey[]): Promise { + for (const pk of accounts) { + const k = pk.toBase58(); + this.subscribedAccounts.delete(k); + this.onChangeMap.delete(k); + } + const request: SubscribeRequest = { + slots: {}, + accounts: { + account: { + account: Array.from(this.subscribedAccounts.values()), + owner: [], + filters: [], + }, + }, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + commitment: this.commitmentLevel, + entry: {}, + transactionsStatus: {}, + }; + + await new Promise((resolve, reject) => { + this.stream.write(request, (err) => { + if (err === null || err === undefined) { + resolve(); + } else { + reject(err); + } + }); + }); + } + + async unsubscribe(): Promise { + this.isUnsubscribing = true; + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + + if (this.listenerId != null) { + const promise = new Promise((resolve, reject) => { + const request: SubscribeRequest = { + slots: {}, + accounts: {}, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + entry: {}, + transactionsStatus: {}, + }; + this.stream.write(request, (err) => { + if (err === null || err === undefined) { + this.listenerId = undefined; + this.isUnsubscribing = false; + resolve(); + } else { + reject(err); + } + }); + }).catch((reason) => { + console.error(reason); + throw reason; + }); + return promise; + } else { + this.isUnsubscribing = false; + } + + if (this.onUnsubscribe) { + try { + await this.onUnsubscribe(); + } catch (e) { + console.error(e); + } + } + } + + private setTimeout(): void { + this.timeoutId = setTimeout( + async () => { + if (this.isUnsubscribing) { + return; + } + if (this.receivingData) { + await this.unsubscribe(); + this.receivingData = false; + } + }, + this.resubOpts?.resubTimeoutMs + ); + } + + private capitalize(value: string): string { + if (!value) return value; + return value.charAt(0).toUpperCase() + value.slice(1); + } +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 9b2463f0c4..593b8c3207 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -189,7 +189,7 @@ import { } from './tx/utils'; import pythSolanaReceiverIdl from './idl/pyth_solana_receiver.json'; import { asV0Tx, PullFeed, AnchorUtils } from '@switchboard-xyz/on-demand'; -import { gprcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; +import { grpcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; import nacl from 'tweetnacl'; import { Slothash } from './slot/SlothashSubscriber'; import { getOracleId } from './oracles/oracleId'; @@ -434,7 +434,10 @@ export class DriftClient { delistedMarketSetting ); } else if (config.accountSubscription?.type === 'grpc') { - this.accountSubscriber = new gprcDriftClientAccountSubscriber( + const accountSubscriberClass = + config.accountSubscription?.driftClientAccountSubscriber ?? + grpcDriftClientAccountSubscriber; + this.accountSubscriber = new accountSubscriberClass( config.accountSubscription.grpcConfigs, this.program, config.perpMarketIndexes ?? [], diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index b3723a2ae8..ceb7b2603d 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -22,6 +22,8 @@ import { WebSocketAccountSubscriberV2 } from './accounts/webSocketAccountSubscri import { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; import { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; +import { grpcDriftClientAccountSubscriberV2 } from './accounts/grpcDriftClientAccountSubscriberV2'; +import { grpcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; export type DriftClientConfig = { connection: Connection; @@ -60,6 +62,17 @@ export type DriftClientSubscriptionConfig = grpcConfigs: GrpcConfigs; resubTimeoutMs?: number; logResubMessages?: boolean; + driftClientAccountSubscriber?: new ( + grpcConfigs: GrpcConfigs, + program: Program, + perpMarketIndexes: number[], + spotMarketIndexes: number[], + oracleInfos: OracleInfo[], + shouldFindAllMarketsAndOracles: boolean, + delistedMarketSetting: DelistedMarketSetting + ) => + | grpcDriftClientAccountSubscriberV2 + | grpcDriftClientAccountSubscriber; } | { type: 'websocket'; diff --git a/sdk/src/isomorphic/grpc.node.ts b/sdk/src/isomorphic/grpc.node.ts index 907bdcc432..267a81c8a6 100644 --- a/sdk/src/isomorphic/grpc.node.ts +++ b/sdk/src/isomorphic/grpc.node.ts @@ -4,30 +4,34 @@ import type { SubscribeUpdate, } from '@triton-one/yellowstone-grpc'; import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; -import { ClientDuplexStream, ChannelOptions } from '@grpc/grpc-js'; +import type { ClientDuplexStream, ChannelOptions } from '@grpc/grpc-js'; import { CommitmentLevel as LaserCommitmentLevel, subscribe as LaserSubscribe, + CompressionAlgorithms, +} from 'helius-laserstream'; +import type { LaserstreamConfig, SubscribeRequest as LaserSubscribeRequest, SubscribeUpdate as LaserSubscribeUpdate, - CompressionAlgorithms, } from 'helius-laserstream'; export { - ClientDuplexStream, - ChannelOptions, - SubscribeRequest, - SubscribeUpdate, CommitmentLevel, Client, LaserSubscribe, LaserCommitmentLevel, + CompressionAlgorithms, +}; +export type { + ClientDuplexStream, + ChannelOptions, + SubscribeRequest, + SubscribeUpdate, LaserstreamConfig, LaserSubscribeRequest, LaserSubscribeUpdate, - CompressionAlgorithms, }; // Export a function to create a new Client instance diff --git a/sdk/yarn.lock b/sdk/yarn.lock index c3130ae226..af07309b37 100644 --- a/sdk/yarn.lock +++ b/sdk/yarn.lock @@ -164,12 +164,12 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== -"@grpc/grpc-js@1.12.6": - version "1.12.6" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.12.6.tgz#a3586ffdfb6a1f5cd5b4866dec9074c4a1e65472" - integrity sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q== +"@grpc/grpc-js@1.14.0": + version "1.14.0" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.0.tgz#a3c47e7816ca2b4d5490cba9e06a3cf324e675ad" + integrity sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg== dependencies: - "@grpc/proto-loader" "^0.7.13" + "@grpc/proto-loader" "^0.8.0" "@js-sdsl/ordered-map" "^4.4.2" "@grpc/grpc-js@^1.8.0", "@grpc/grpc-js@^1.8.13": @@ -190,6 +190,16 @@ protobufjs "^7.2.5" yargs "^17.7.2" +"@grpc/proto-loader@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz#b6c324dd909c458a0e4aa9bfd3d69cf78a4b9bd8" + integrity sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.5.3" + yargs "^17.7.2" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -1139,10 +1149,10 @@ buffer "^6.0.3" js-yaml "^4.1.0" -"@triton-one/yellowstone-grpc@1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@triton-one/yellowstone-grpc/-/yellowstone-grpc-1.3.0.tgz#7caa7006b525149b4780d1295c7d4c34bc6a6ff6" - integrity sha512-tuwHtoYzvqnahsMrecfNNkQceCYwgiY0qKS8RwqtaxvDEgjm0E+0bXwKz2eUD3ZFYifomJmRKDmSBx9yQzAeMQ== +"@triton-one/yellowstone-grpc@1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@triton-one/yellowstone-grpc/-/yellowstone-grpc-1.4.1.tgz#b50434f68c3d73c6ce3e1f225656064c080f4b25" + integrity sha512-ZN49vooxFbOqWttll8u7AOsIVnX+srqX9ddhZ9ttE+OcehUo8c2p2suK8Gr2puab49cgsV0VGjiTn9Gua/ntIw== dependencies: "@grpc/grpc-js" "^1.8.0" From e5c215f129d40530eab21ced5b5a9befbca8b7f4 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 19:54:46 +0000 Subject: [PATCH 082/247] sdk: release v2.142.0-beta.2 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 0a5513f8fe..d908cec366 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.1 \ No newline at end of file +2.142.0-beta.2 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c9e614f477..c270a17191 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.1", + "version": "2.142.0-beta.2", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From e82f263feb8fe49bb23bdf0d5fd751e45c8a9782 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Fri, 3 Oct 2025 15:46:13 -0600 Subject: [PATCH 083/247] fix: methods for getting market and slot data bombing (#1933) --- sdk/scripts/client-test.ts | 174 +++++++++++------- .../grpcDriftClientAccountSubscriberV2.ts | 82 ++++++++- .../accounts/grpcMultiAccountSubscriber.ts | 12 +- 3 files changed, 194 insertions(+), 74 deletions(-) diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/client-test.ts index 79ba3d2915..28b132ce93 100644 --- a/sdk/scripts/client-test.ts +++ b/sdk/scripts/client-test.ts @@ -10,78 +10,124 @@ const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; const TOKEN = process.env.TOKEN; async function initializeGrpcDriftClientV2() { - const connection = new Connection('https://api.mainnet-beta.solana.com'); - const wallet = new Wallet(new Keypair()); - dotenv.config({ path: '../' }); - const config: DriftClientConfig = { - connection, - wallet, - programID: new PublicKey(DRIFT_PROGRAM_ID), - accountSubscription: { - type: 'grpc', - grpcConfigs: { - endpoint: GRPC_ENDPOINT, - token: TOKEN, - commitmentLevel: 'confirmed' as unknown as CommitmentLevel, - channelOptions: { - 'grpc.keepalive_time_ms': 10_000, - 'grpc.keepalive_timeout_ms': 1_000, - 'grpc.keepalive_permit_without_calls': 1, - }, - }, - driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, - }, - perpMarketIndexes: [0, 1, 2], // Example market indexes - spotMarketIndexes: [0, 1, 2], // Example market indexes - oracleInfos: [], // Add oracle information if needed - }; + const connection = new Connection('https://api.mainnet-beta.solana.com'); + const wallet = new Wallet(new Keypair()); + dotenv.config({ path: '../' }); + const config: DriftClientConfig = { + connection, + wallet, + programID: new PublicKey(DRIFT_PROGRAM_ID), + accountSubscription: { + type: 'grpc', + grpcConfigs: { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: 'confirmed' as unknown as CommitmentLevel, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }, + driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, + }, + perpMarketIndexes: [0, 1, 2], // Example market indexes + spotMarketIndexes: [0, 1, 2], // Example market indexes + oracleInfos: [], // Add oracle information if needed + }; - const driftClient = new DriftClient(config); + const driftClient = new DriftClient(config); - let perpMarketUpdateCount = 0; - let spotMarketUpdateCount = 0; - let oraclePriceUpdateCount = 0; - let userAccountUpdateCount = 0; + let perpMarketUpdateCount = 0; + let spotMarketUpdateCount = 0; + let oraclePriceUpdateCount = 0; + let userAccountUpdateCount = 0; - const updatePromise = new Promise((resolve) => { - driftClient.accountSubscriber.eventEmitter.on('perpMarketAccountUpdate', (data) => { - console.log('Perp market account update:', decodeName(data.name)); - perpMarketUpdateCount++; - if (perpMarketUpdateCount >= 10 && spotMarketUpdateCount >= 10 && oraclePriceUpdateCount >= 10 && userAccountUpdateCount >= 2) { - resolve(); - } - }); + const updatePromise = new Promise((resolve) => { + driftClient.accountSubscriber.eventEmitter.on( + 'perpMarketAccountUpdate', + (data) => { + console.log('Perp market account update:', decodeName(data.name)); + const perpMarketData = driftClient.getPerpMarketAccount( + data.marketIndex + ); + console.log( + 'Perp market data market index:', + perpMarketData?.marketIndex + ); + perpMarketUpdateCount++; + if ( + perpMarketUpdateCount >= 10 && + spotMarketUpdateCount >= 10 && + oraclePriceUpdateCount >= 10 && + userAccountUpdateCount >= 2 + ) { + resolve(); + } + } + ); - driftClient.accountSubscriber.eventEmitter.on('spotMarketAccountUpdate', (data) => { - console.log('Spot market account update:', decodeName(data.name)); - spotMarketUpdateCount++; - if (perpMarketUpdateCount >= 10 && spotMarketUpdateCount >= 10 && oraclePriceUpdateCount >= 10 && userAccountUpdateCount >= 2) { - resolve(); - } - }); + driftClient.accountSubscriber.eventEmitter.on( + 'spotMarketAccountUpdate', + (data) => { + console.log('Spot market account update:', decodeName(data.name)); + const spotMarketData = driftClient.getSpotMarketAccount( + data.marketIndex + ); + console.log( + 'Spot market data market index:', + spotMarketData?.marketIndex + ); + spotMarketUpdateCount++; + if ( + perpMarketUpdateCount >= 10 && + spotMarketUpdateCount >= 10 && + oraclePriceUpdateCount >= 10 && + userAccountUpdateCount >= 2 + ) { + resolve(); + } + } + ); - driftClient.accountSubscriber.eventEmitter.on('oraclePriceUpdate', (data) => { - console.log('Oracle price update:', data.toBase58()); - oraclePriceUpdateCount++; - if (perpMarketUpdateCount >= 10 && spotMarketUpdateCount >= 10 && oraclePriceUpdateCount >= 10 && userAccountUpdateCount >= 2) { - resolve(); - } - }); + driftClient.accountSubscriber.eventEmitter.on( + 'oraclePriceUpdate', + (data) => { + console.log('Oracle price update:', data.toBase58()); + oraclePriceUpdateCount++; + if ( + perpMarketUpdateCount >= 10 && + spotMarketUpdateCount >= 10 && + oraclePriceUpdateCount >= 10 && + userAccountUpdateCount >= 2 + ) { + resolve(); + } + } + ); - driftClient.accountSubscriber.eventEmitter.on('userAccountUpdate', (data) => { - console.log('User account update:', decodeName(data.name)); - userAccountUpdateCount++; - if (perpMarketUpdateCount >= 10 && spotMarketUpdateCount >= 10 && oraclePriceUpdateCount >= 10 && userAccountUpdateCount >= 2) { - resolve(); - } - }); - }); + driftClient.accountSubscriber.eventEmitter.on( + 'userAccountUpdate', + (data) => { + console.log('User account update:', decodeName(data.name)); + userAccountUpdateCount++; + if ( + perpMarketUpdateCount >= 10 && + spotMarketUpdateCount >= 10 && + oraclePriceUpdateCount >= 10 && + userAccountUpdateCount >= 2 + ) { + resolve(); + } + } + ); + }); - await driftClient.subscribe(); - console.log('DriftClient initialized and listening for updates.'); + await driftClient.subscribe(); + console.log('DriftClient initialized and listening for updates.'); - await updatePromise; - console.log('Received required number of updates.'); + await updatePromise; + console.log('Received required number of updates.'); } initializeGrpcDriftClientV2().catch(console.error); diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index dcd2b1aeae..3df8d558ef 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -8,7 +8,12 @@ import { getPerpMarketPublicKey, getSpotMarketPublicKey, } from '../addresses/pda'; -import { DelistedMarketSetting, GrpcConfigs, ResubOpts } from './types'; +import { + DataAndSlot, + DelistedMarketSetting, + GrpcConfigs, + ResubOpts, +} from './types'; import { grpcAccountSubscriber } from './grpcAccountSubscriber'; import { grpcMultiAccountSubscriber } from './grpcMultiAccountSubscriber'; import { PerpMarketAccount, SpotMarketAccount, StateAccount } from '../types'; @@ -19,6 +24,8 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco private perpMarketsSubscriber?: grpcMultiAccountSubscriber; private spotMarketsSubscriber?: grpcMultiAccountSubscriber; private oracleMultiSubscriber?: grpcMultiAccountSubscriber; + private perpMarketIndexToAccountPubkeyMap = new Map(); + private spotMarketIndexToAccountPubkeyMap = new Map(); constructor( grpcConfigs: GrpcConfigs, @@ -123,11 +130,39 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco return true; } + override getMarketAccountAndSlot( + marketIndex: number + ): DataAndSlot | undefined { + return this.perpMarketsSubscriber?.getAccountData( + this.perpMarketIndexToAccountPubkeyMap.get(marketIndex) + ); + } + + override getSpotMarketAccountAndSlot( + marketIndex: number + ): DataAndSlot | undefined { + return this.spotMarketsSubscriber?.getAccountData( + this.spotMarketIndexToAccountPubkeyMap.get(marketIndex) + ); + } + override async subscribeToPerpMarketAccounts(): Promise { - const perpMarketPubkeys = await Promise.all( - this.perpMarketIndexes.map((marketIndex) => - getPerpMarketPublicKey(this.program.programId, marketIndex) - ) + const perpMarketIndexToAccountPubkeys: Array<[number, PublicKey]> = + await Promise.all( + this.perpMarketIndexes.map(async (marketIndex) => [ + marketIndex, + await getPerpMarketPublicKey(this.program.programId, marketIndex), + ]) + ); + for (const [ + marketIndex, + accountPubkey, + ] of perpMarketIndexToAccountPubkeys) { + this.perpMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey); + } + + const perpMarketPubkeys = perpMarketIndexToAccountPubkeys.map( + ([_, accountPubkey]) => accountPubkey ); this.perpMarketsSubscriber = @@ -151,6 +186,11 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco } } ); + + for (const data of this.initialPerpMarketAccountData.values()) { + this.perpMarketsSubscriber.setAccountData(data.pubkey, data); + } + await this.perpMarketsSubscriber.subscribe( perpMarketPubkeys, (_accountId, data) => { @@ -166,10 +206,22 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco } override async subscribeToSpotMarketAccounts(): Promise { - const spotMarketPubkeys = await Promise.all( - this.spotMarketIndexes.map((marketIndex) => - getSpotMarketPublicKey(this.program.programId, marketIndex) - ) + const spotMarketIndexToAccountPubkeys: Array<[number, PublicKey]> = + await Promise.all( + this.spotMarketIndexes.map(async (marketIndex) => [ + marketIndex, + await getSpotMarketPublicKey(this.program.programId, marketIndex), + ]) + ); + for (const [ + marketIndex, + accountPubkey, + ] of spotMarketIndexToAccountPubkeys) { + this.spotMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey); + } + + const spotMarketPubkeys = spotMarketIndexToAccountPubkeys.map( + ([_, accountPubkey]) => accountPubkey ); this.spotMarketsSubscriber = @@ -193,6 +245,11 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco } } ); + + for (const data of this.initialSpotMarketAccountData.values()) { + this.spotMarketsSubscriber.setAccountData(data.pubkey, data); + } + await this.spotMarketsSubscriber.subscribe( spotMarketPubkeys, (_accountId, data) => { @@ -263,6 +320,13 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco } ); + for (const data of this.initialOraclePriceData.entries()) { + this.oracleMultiSubscriber.setAccountData( + new PublicKey(data[0]), + data[1] + ); + } + await this.oracleMultiSubscriber.subscribe( oraclePubkeys, (accountId, data) => { diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index ab5b09ce97..dadb173c28 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -11,7 +11,7 @@ import { SubscribeUpdate, createClient, } from '../isomorphic/grpc'; -import { GrpcConfigs, ResubOpts } from './types'; +import { DataAndSlot, GrpcConfigs, ResubOpts } from './types'; interface AccountInfoLike { owner: PublicKey; @@ -42,6 +42,8 @@ export class grpcMultiAccountSubscriber { (data: T, context: Context, buffer: Buffer) => void >(); + private dataMap = new Map>(); + private constructor( client: Client, commitmentLevel: CommitmentLevel, @@ -91,6 +93,14 @@ export class grpcMultiAccountSubscriber { ); } + setAccountData(accountPubkey: PublicKey, data: T, slot?: number): void { + this.dataMap.set(accountPubkey.toBase58(), { data, slot }); + } + + getAccountData(accountPubkey: PublicKey): DataAndSlot | undefined { + return this.dataMap.get(accountPubkey.toBase58()); + } + async subscribe( accounts: PublicKey[], onChange: ( From 4e55a4252c279b3f391ba08c0a06e29801e3277d Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 21:51:03 +0000 Subject: [PATCH 084/247] sdk: release v2.142.0-beta.3 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index d908cec366..7d04567374 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.2 \ No newline at end of file +2.142.0-beta.3 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c270a17191..7cd084291e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.2", + "version": "2.142.0-beta.3", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From a0545b9851217e30db6c8a5b342eababe0028ff3 Mon Sep 17 00:00:00 2001 From: wphan Date: Fri, 3 Oct 2025 14:51:22 -0700 Subject: [PATCH 085/247] update RevenueShareOrder ts type (#1934) --- sdk/src/types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 8c08954f20..e75c76b261 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1669,13 +1669,15 @@ export type RevenueShareEscrowAccount = { }; export type RevenueShareOrder = { - builderIdx: number; feesAccrued: BN; orderId: number; feeTenthBps: number; marketIndex: number; + subAccountId: number; + builderIdx: number; bitFlags: number; - marketType: MarketType; // 0: spot, 1: perp + userOrderIndex: number; + marketType: MarketType; padding: number[]; }; From 50f115d1775008b26c498489dc8ac91b3fd4fb5e Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 21:55:50 +0000 Subject: [PATCH 086/247] sdk: release v2.142.0-beta.4 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 7d04567374..800b595f4b 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.3 \ No newline at end of file +2.142.0-beta.4 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 7cd084291e..1f1ccf7960 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.3", + "version": "2.142.0-beta.4", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 068ed66b4f0827d3ace327ecd958b7b27c63da7c Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Fri, 3 Oct 2025 23:21:57 -0600 Subject: [PATCH 087/247] fix: oracle info wrong on grpc sub v2 (#1935) --- sdk/scripts/client-test.ts | 57 +++++++++++++++++-- .../grpcDriftClientAccountSubscriberV2.ts | 11 ++-- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/client-test.ts index 28b132ce93..a9aca485ee 100644 --- a/sdk/scripts/client-test.ts +++ b/sdk/scripts/client-test.ts @@ -2,9 +2,21 @@ import { DriftClient } from '../src/driftClient'; import { grpcDriftClientAccountSubscriberV2 } from '../src/accounts/grpcDriftClientAccountSubscriberV2'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { DriftClientConfig } from '../src/driftClientConfig'; -import { decodeName, DRIFT_PROGRAM_ID, Wallet } from '../src'; +import { + decodeName, + DRIFT_PROGRAM_ID, + PerpMarketAccount, + Wallet, +} from '../src'; import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; import dotenv from 'dotenv'; +import { + AnchorProvider, + Idl, + Program, + ProgramAccount, +} from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; const TOKEN = process.env.TOKEN; @@ -13,6 +25,43 @@ async function initializeGrpcDriftClientV2() { const connection = new Connection('https://api.mainnet-beta.solana.com'); const wallet = new Wallet(new Keypair()); dotenv.config({ path: '../' }); + + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const provider = new AnchorProvider( + connection, + // @ts-ignore + wallet, + { + commitment: 'confirmed', + } + ); + + const program = new Program(driftIDL as Idl, programId, provider); + + const perpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const solPerpMarket = perpMarketProgramAccounts.find( + (account) => account.account.marketIndex === 0 + ); + const solOracleInfo = { + publicKey: solPerpMarket.account.amm.oracle, + source: solPerpMarket.account.amm.oracleSource, + }; + const ethPerpMarket = perpMarketProgramAccounts.find( + (account) => account.account.marketIndex === 2 + ); + const ethOracleInfo = { + publicKey: ethPerpMarket.account.amm.oracle, + source: ethPerpMarket.account.amm.oracleSource, + }; + const btcPerpMarket = perpMarketProgramAccounts.find( + (account) => account.account.marketIndex === 1 + ); + const btcOracleInfo = { + publicKey: btcPerpMarket.account.amm.oracle, + source: btcPerpMarket.account.amm.oracleSource, + }; + const config: DriftClientConfig = { connection, wallet, @@ -31,9 +80,9 @@ async function initializeGrpcDriftClientV2() { }, driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, }, - perpMarketIndexes: [0, 1, 2], // Example market indexes - spotMarketIndexes: [0, 1, 2], // Example market indexes - oracleInfos: [], // Add oracle information if needed + perpMarketIndexes: [0, 1, 2], + spotMarketIndexes: [0, 1, 2], + oracleInfos: [solOracleInfo, ethOracleInfo, btcOracleInfo], }; const driftClient = new DriftClient(config); diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 3df8d558ef..cf2b714615 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -17,7 +17,10 @@ import { import { grpcAccountSubscriber } from './grpcAccountSubscriber'; import { grpcMultiAccountSubscriber } from './grpcMultiAccountSubscriber'; import { PerpMarketAccount, SpotMarketAccount, StateAccount } from '../types'; -import { getOracleId } from '../oracles/oracleId'; +import { + getOracleId, + getPublicKeyAndSourceFromOracleId, +} from '../oracles/oracleId'; export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAccountSubscriber { private grpcConfigs: GrpcConfigs; @@ -321,10 +324,8 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco ); for (const data of this.initialOraclePriceData.entries()) { - this.oracleMultiSubscriber.setAccountData( - new PublicKey(data[0]), - data[1] - ); + const { publicKey } = getPublicKeyAndSourceFromOracleId(data[0]); + this.oracleMultiSubscriber.setAccountData(publicKey, data[1]); } await this.oracleMultiSubscriber.subscribe( From da6fc9c5acccb400e803e361c5ef1e9de3de611e Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 05:26:24 +0000 Subject: [PATCH 088/247] sdk: release v2.142.0-beta.5 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 800b595f4b..c3fbc3bb3a 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.4 \ No newline at end of file +2.142.0-beta.5 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 1f1ccf7960..d932783cd5 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.4", + "version": "2.142.0-beta.5", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From 7bea1a93bb5347b25f75bee0ead169a20887bc11 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Sat, 4 Oct 2025 11:08:53 -0600 Subject: [PATCH 089/247] fix: oracle string map not initted grpc v2 (#1936) * fix: oracle string map not initted grpc v2 * fix: prettier --- sdk/scripts/client-test.ts | 23 ++++++++ .../grpcDriftClientAccountSubscriberV2.ts | 52 +++++++++++++++++++ .../accounts/grpcMultiAccountSubscriber.ts | 4 ++ 3 files changed, 79 insertions(+) diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/client-test.ts index a9aca485ee..34ac3ed46f 100644 --- a/sdk/scripts/client-test.ts +++ b/sdk/scripts/client-test.ts @@ -104,6 +104,12 @@ async function initializeGrpcDriftClientV2() { 'Perp market data market index:', perpMarketData?.marketIndex ); + const oracle = driftClient.getOracleDataForPerpMarket(data.marketIndex); + const mmOracle = driftClient.getMMOracleDataForPerpMarket( + data.marketIndex + ); + console.log('Perp oracle price:', oracle.price.toString()); + console.log('Perp MM oracle price:', mmOracle.price.toString()); perpMarketUpdateCount++; if ( perpMarketUpdateCount >= 10 && @@ -127,6 +133,8 @@ async function initializeGrpcDriftClientV2() { 'Spot market data market index:', spotMarketData?.marketIndex ); + const oracle = driftClient.getOracleDataForSpotMarket(data.marketIndex); + console.log('Spot oracle price:', oracle.price.toString()); spotMarketUpdateCount++; if ( perpMarketUpdateCount >= 10 && @@ -175,6 +183,21 @@ async function initializeGrpcDriftClientV2() { await driftClient.subscribe(); console.log('DriftClient initialized and listening for updates.'); + for (const marketIndex of config.perpMarketIndexes) { + const oracle = driftClient.getOracleDataForPerpMarket(marketIndex); + const mmOracle = driftClient.getMMOracleDataForPerpMarket(marketIndex); + console.log('Initial perp oracle price:', oracle.price.toString()); + console.log('Initial perp MM oracle price:', mmOracle.price.toString()); + } + + for (const marketIndex of config.spotMarketIndexes) { + const oracle = driftClient.getOracleDataForSpotMarket(marketIndex); + console.log('Initial spot oracle price:', oracle.price.toString()); + } + + const stateAccount = driftClient.getStateAccount(); + console.log('Initial state account:', stateAccount.toString()); + await updatePromise; console.log('Received required number of updates.'); } diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index cf2b714615..5c41e67870 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -149,6 +149,58 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco ); } + override async setPerpOracleMap() { + const perpMarketsMap = this.perpMarketsSubscriber?.getAccountDataMap(); + const perpMarkets = Array.from(perpMarketsMap.values()); + const addOraclePromises = []; + for (const perpMarket of perpMarkets) { + if (!perpMarket || !perpMarket.data) { + continue; + } + const perpMarketAccount = perpMarket.data; + const perpMarketIndex = perpMarketAccount.marketIndex; + const oracle = perpMarketAccount.amm.oracle; + const oracleId = getOracleId(oracle, perpMarket.data.amm.oracleSource); + if (!this.oracleSubscribers.has(oracleId)) { + addOraclePromises.push( + this.addOracle({ + publicKey: oracle, + source: perpMarket.data.amm.oracleSource, + }) + ); + } + this.perpOracleMap.set(perpMarketIndex, oracle); + this.perpOracleStringMap.set(perpMarketIndex, oracleId); + } + await Promise.all(addOraclePromises); + } + + override async setSpotOracleMap() { + const spotMarketsMap = this.spotMarketsSubscriber?.getAccountDataMap(); + const spotMarkets = Array.from(spotMarketsMap.values()); + const addOraclePromises = []; + for (const spotMarket of spotMarkets) { + if (!spotMarket || !spotMarket.data) { + continue; + } + const spotMarketAccount = spotMarket.data; + const spotMarketIndex = spotMarketAccount.marketIndex; + const oracle = spotMarketAccount.oracle; + const oracleId = getOracleId(oracle, spotMarketAccount.oracleSource); + if (!this.oracleSubscribers.has(oracleId)) { + addOraclePromises.push( + this.addOracle({ + publicKey: oracle, + source: spotMarketAccount.oracleSource, + }) + ); + } + this.spotOracleMap.set(spotMarketIndex, oracle); + this.spotOracleStringMap.set(spotMarketIndex, oracleId); + } + await Promise.all(addOraclePromises); + } + override async subscribeToPerpMarketAccounts(): Promise { const perpMarketIndexToAccountPubkeys: Array<[number, PublicKey]> = await Promise.all( diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index dadb173c28..aca8692015 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -101,6 +101,10 @@ export class grpcMultiAccountSubscriber { return this.dataMap.get(accountPubkey.toBase58()); } + getAccountDataMap(): Map> { + return this.dataMap; + } + async subscribe( accounts: PublicKey[], onChange: ( From a0d9011bc6493bd4fbade53615078b23e3b86dc4 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:13:57 +0000 Subject: [PATCH 090/247] sdk: release v2.142.0-beta.6 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index c3fbc3bb3a..0a48faa593 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.5 \ No newline at end of file +2.142.0-beta.6 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index d932783cd5..8ef9fd4c87 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.5", + "version": "2.142.0-beta.6", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "browser": "./lib/browser/index.js", From ec8d0648b79c17ecb3fd90651fbe342f49edbf0a Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Sun, 5 Oct 2025 09:34:29 +0800 Subject: [PATCH 091/247] refactor(sdk): fallback to false for node-only packages (#1931) --- sdk/package.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/package.json b/sdk/package.json index 8ef9fd4c87..6fb5eb0386 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -3,7 +3,7 @@ "version": "2.142.0-beta.6", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", - "browser": "./lib/browser/index.js", + "module": "./lib/browser/index.js", "author": "crispheaney", "homepage": "https://www.drift.trade/", "repository": { @@ -137,5 +137,11 @@ "chalk-template": "<1.1.1", "supports-hyperlinks": "<4.1.1", "has-ansi": "<6.0.1" + }, + "browser": { + "helius-laserstream": false, + "@triton-one/yellowstone-grpc": false, + "@grpc/grpc-js": false, + "zstddec": false } -} +} \ No newline at end of file From b9b8e89cf3cc1420c9452df64067a6d66f2bcb82 Mon Sep 17 00:00:00 2001 From: DecentralizedDev <181214587+DecentralizedDev@users.noreply.github.com> Date: Sun, 5 Oct 2025 03:38:43 +0200 Subject: [PATCH 092/247] grpc v2 use simpler types (#1937) --- sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts | 8 ++++---- sdk/src/accounts/grpcMultiAccountSubscriber.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 5c41e67870..b2319f5ecd 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -27,8 +27,8 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco private perpMarketsSubscriber?: grpcMultiAccountSubscriber; private spotMarketsSubscriber?: grpcMultiAccountSubscriber; private oracleMultiSubscriber?: grpcMultiAccountSubscriber; - private perpMarketIndexToAccountPubkeyMap = new Map(); - private spotMarketIndexToAccountPubkeyMap = new Map(); + private perpMarketIndexToAccountPubkeyMap = new Map(); + private spotMarketIndexToAccountPubkeyMap = new Map(); constructor( grpcConfigs: GrpcConfigs, @@ -213,7 +213,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco marketIndex, accountPubkey, ] of perpMarketIndexToAccountPubkeys) { - this.perpMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey); + this.perpMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey.toBase58()); } const perpMarketPubkeys = perpMarketIndexToAccountPubkeys.map( @@ -272,7 +272,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco marketIndex, accountPubkey, ] of spotMarketIndexToAccountPubkeys) { - this.spotMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey); + this.spotMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey.toBase58()); } const spotMarketPubkeys = spotMarketIndexToAccountPubkeys.map( diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index aca8692015..263e5c8c57 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -97,8 +97,8 @@ export class grpcMultiAccountSubscriber { this.dataMap.set(accountPubkey.toBase58(), { data, slot }); } - getAccountData(accountPubkey: PublicKey): DataAndSlot | undefined { - return this.dataMap.get(accountPubkey.toBase58()); + getAccountData(accountPubkey: string): DataAndSlot | undefined { + return this.dataMap.get(accountPubkey); } getAccountDataMap(): Map> { From ba0c8e348389127c9fc7bb0a96f3a3b7d079b207 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 01:39:02 +0000 Subject: [PATCH 093/247] sdk: release v2.142.0-beta.7 --- sdk/VERSION | 2 +- sdk/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 0a48faa593..055d3024f6 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.6 \ No newline at end of file +2.142.0-beta.7 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 6fb5eb0386..cda2220bfd 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.6", + "version": "2.142.0-beta.7", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", @@ -144,4 +144,4 @@ "@grpc/grpc-js": false, "zstddec": false } -} \ No newline at end of file +} From 8ceb1c62e61f32acaab5b5e346e946641fa49122 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Sat, 4 Oct 2025 20:36:23 -0600 Subject: [PATCH 094/247] fix: prettier broken on grpc v2 subscriber (#1938) --- sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index b2319f5ecd..7a0b5963cf 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -213,7 +213,10 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco marketIndex, accountPubkey, ] of perpMarketIndexToAccountPubkeys) { - this.perpMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey.toBase58()); + this.perpMarketIndexToAccountPubkeyMap.set( + marketIndex, + accountPubkey.toBase58() + ); } const perpMarketPubkeys = perpMarketIndexToAccountPubkeys.map( @@ -272,7 +275,10 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco marketIndex, accountPubkey, ] of spotMarketIndexToAccountPubkeys) { - this.spotMarketIndexToAccountPubkeyMap.set(marketIndex, accountPubkey.toBase58()); + this.spotMarketIndexToAccountPubkeyMap.set( + marketIndex, + accountPubkey.toBase58() + ); } const spotMarketPubkeys = spotMarketIndexToAccountPubkeys.map( From 2353b60feb8a64f623845234e6d1af199da65eaf Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 02:41:01 +0000 Subject: [PATCH 095/247] sdk: release v2.142.0-beta.8 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 055d3024f6..9038360c10 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.7 \ No newline at end of file +2.142.0-beta.8 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index cda2220bfd..eeee71120d 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.7", + "version": "2.142.0-beta.8", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From b9be1636f836b3416485ebb4ef6422de93a27c1d Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Mon, 6 Oct 2025 10:42:02 -0600 Subject: [PATCH 096/247] fix: grpc sub v2/multi not updating data map (#1940) --- sdk/scripts/client-test.ts | 33 ++++++++++++------- .../grpcDriftClientAccountSubscriberV2.ts | 6 ++-- .../accounts/grpcMultiAccountSubscriber.ts | 11 ++++--- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/client-test.ts index 34ac3ed46f..af78b56dbc 100644 --- a/sdk/scripts/client-test.ts +++ b/sdk/scripts/client-test.ts @@ -96,20 +96,29 @@ async function initializeGrpcDriftClientV2() { driftClient.accountSubscriber.eventEmitter.on( 'perpMarketAccountUpdate', (data) => { - console.log('Perp market account update:', decodeName(data.name)); - const perpMarketData = driftClient.getPerpMarketAccount( - data.marketIndex - ); console.log( - 'Perp market data market index:', - perpMarketData?.marketIndex - ); - const oracle = driftClient.getOracleDataForPerpMarket(data.marketIndex); - const mmOracle = driftClient.getMMOracleDataForPerpMarket( - data.marketIndex + 'Perp market account update:', + decodeName(data.name), + 'mmOracleSequenceId:', + data.amm.mmOracleSequenceId.toString() ); - console.log('Perp oracle price:', oracle.price.toString()); - console.log('Perp MM oracle price:', mmOracle.price.toString()); + // const perpMarketData = driftClient.getPerpMarketAccount( + // data.marketIndex + // ); + // console.log( + // 'Perp market data market index:', + // perpMarketData?.marketIndex + // ); + // const oracle = driftClient.getOracleDataForPerpMarket(data.marketIndex); + // const mmOracle = driftClient.getMMOracleDataForPerpMarket( + // data.marketIndex + // ); + // console.log('Perp oracle price:', oracle.price.toString()); + // console.log('Perp MM oracle price:', mmOracle.price.toString()); + // console.log( + // 'Perp MM oracle sequence id:', + // perpMarketData?.amm?.mmOracleSequenceId?.toString() + // ); perpMarketUpdateCount++; if ( perpMarketUpdateCount >= 10 && diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 7a0b5963cf..6b32e70ed6 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -246,7 +246,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco ); for (const data of this.initialPerpMarketAccountData.values()) { - this.perpMarketsSubscriber.setAccountData(data.pubkey, data); + this.perpMarketsSubscriber.setAccountData(data.pubkey.toBase58(), data); } await this.perpMarketsSubscriber.subscribe( @@ -308,7 +308,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco ); for (const data of this.initialSpotMarketAccountData.values()) { - this.spotMarketsSubscriber.setAccountData(data.pubkey, data); + this.spotMarketsSubscriber.setAccountData(data.pubkey.toBase58(), data); } await this.spotMarketsSubscriber.subscribe( @@ -383,7 +383,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco for (const data of this.initialOraclePriceData.entries()) { const { publicKey } = getPublicKeyAndSourceFromOracleId(data[0]); - this.oracleMultiSubscriber.setAccountData(publicKey, data[1]); + this.oracleMultiSubscriber.setAccountData(publicKey.toBase58(), data[1]); } await this.oracleMultiSubscriber.subscribe( diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index 263e5c8c57..005240d20e 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -93,8 +93,8 @@ export class grpcMultiAccountSubscriber { ); } - setAccountData(accountPubkey: PublicKey, data: T, slot?: number): void { - this.dataMap.set(accountPubkey.toBase58(), { data, slot }); + setAccountData(accountPubkey: string, data: T, slot?: number): void { + this.dataMap.set(accountPubkey, { data, slot }); } getAccountData(accountPubkey: string): DataAndSlot | undefined { @@ -122,9 +122,10 @@ export class grpcMultiAccountSubscriber { for (const pk of accounts) { const key = pk.toBase58(); this.subscribedAccounts.add(key); - this.onChangeMap.set(key, (data, ctx, buffer) => - onChange(new PublicKey(key), data, ctx, buffer) - ); + this.onChangeMap.set(key, (data, ctx, buffer) => { + this.setAccountData(key, data, ctx.slot); + onChange(new PublicKey(key), data, ctx, buffer); + }); } this.stream = From 373f42328687828d82d26d8c1292afaf7c283859 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:47:56 +0000 Subject: [PATCH 097/247] sdk: release v2.142.0-beta.9 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 9038360c10..f45bd97a52 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.8 \ No newline at end of file +2.142.0-beta.9 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index eeee71120d..ebe54d3c1f 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.8", + "version": "2.142.0-beta.9", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 6b4f02e7717630a5e9462d07b4d7ec4519f71168 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:36:13 -0700 Subject: [PATCH 098/247] update devnet spot market oracles --- sdk/src/constants/spotMarkets.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 70e152a323..707387d0f5 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -39,8 +39,8 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ symbol: 'USDC', marketIndex: 0, poolId: 0, - oracle: new PublicKey('En8hkHLkRe9d9DraYmBTrus518BvmVH448YcvmrFM6Ce'), - oracleSource: OracleSource.PYTH_STABLE_COIN_PULL, + oracle: new PublicKey('9VCioxmni2gDLv11qufWzT3RDERhQE4iY5Gf7NTfYyAV'), + oracleSource: OracleSource.PYTH_LAZER_STABLE_COIN, mint: new PublicKey('8zGuJQqwhZafTah7Uc7Z4tXRnguqkn5KLFAP8oV6PHe2'), precision: new BN(10).pow(SIX), precisionExp: SIX, @@ -52,8 +52,8 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ symbol: 'SOL', marketIndex: 1, poolId: 0, - oracle: new PublicKey('BAtFj4kQttZRVep3UZS2aZRDixkGYgWsbqTBVDbnSsPF'), - oracleSource: OracleSource.PYTH_PULL, + oracle: new PublicKey('3m6i4RFWEDw2Ft4tFHPJtYgmpPe21k56M3FHeWYrgGBz'), + oracleSource: OracleSource.PYTH_LAZER, mint: new PublicKey(WRAPPED_SOL_MINT), precision: LAMPORTS_PRECISION, precisionExp: LAMPORTS_EXP, From 4c75c0b47b29164d2d36f87a0d1649ca4cbaec81 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:41:29 +0000 Subject: [PATCH 099/247] sdk: release v2.142.0-beta.10 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index f45bd97a52..257066798f 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.9 \ No newline at end of file +2.142.0-beta.10 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index ebe54d3c1f..3bb349b487 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.9", + "version": "2.142.0-beta.10", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 80aac819d2efdd9049e08263cc2b699a9f44d1c8 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Mon, 6 Oct 2025 19:43:20 -0600 Subject: [PATCH 100/247] fix: oracle updates not emitting for all sources (#1941) * fix: oracle updates not emitting for all sources * fix: prettier --- .../grpcDriftClientAccountSubscriberV2.ts | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 6b32e70ed6..e0a8498539 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -326,26 +326,22 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco } override async subscribeToOracles(): Promise { - // Build list of unique oracle pubkeys and a lookup for sources - const uniqueOraclePubkeys = new Map(); + const pubkeyToSources = new Map>(); for (const info of this.oracleInfos) { - const id = getOracleId(info.publicKey, info.source); - if ( - !uniqueOraclePubkeys.has(id) && - !info.publicKey.equals((PublicKey as any).default) - ) { - uniqueOraclePubkeys.set(id, info); + if (info.publicKey.equals((PublicKey as any).default)) { + continue; + } + const key = info.publicKey.toBase58(); + let sources = pubkeyToSources.get(key); + if (!sources) { + sources = new Set(); + pubkeyToSources.set(key, sources); } + sources.add(info.source); } - const oraclePubkeys = Array.from(uniqueOraclePubkeys.values()).map( - (i) => i.publicKey - ); - const pubkeyToSource = new Map( - Array.from(uniqueOraclePubkeys.values()).map((i) => [ - i.publicKey.toBase58(), - i.source, - ]) + const oraclePubkeys = Array.from(pubkeyToSources.keys()).map( + (k) => new PublicKey(k) ); this.oracleMultiSubscriber = @@ -357,9 +353,13 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco if (!pubkey) { throw new Error('Oracle pubkey missing in decode'); } - const source = pubkeyToSource.get(pubkey); + const sources = pubkeyToSources.get(pubkey); + if (!sources || sources.size === 0) { + throw new Error('Oracle sources missing for pubkey in decode'); + } + const primarySource = sources.values().next().value; const client = this.oracleClientCache.get( - source, + primarySource, this.program.provider.connection, this.program ); @@ -389,8 +389,17 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco await this.oracleMultiSubscriber.subscribe( oraclePubkeys, (accountId, data) => { - const source = pubkeyToSource.get(accountId.toBase58()); - this.eventEmitter.emit('oraclePriceUpdate', accountId, source, data); + const sources = pubkeyToSources.get(accountId.toBase58()); + if (sources) { + for (const source of sources.values()) { + this.eventEmitter.emit( + 'oraclePriceUpdate', + accountId, + source, + data + ); + } + } this.eventEmitter.emit('update'); } ); From 4aa47ed8c1f0f86b251a90792e0bde6ad78fb1f3 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 01:48:38 +0000 Subject: [PATCH 101/247] sdk: release v2.142.0-beta.11 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 257066798f..cbde2437b9 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.10 \ No newline at end of file +2.142.0-beta.11 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 3bb349b487..2f98c71c0a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.10", + "version": "2.142.0-beta.11", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 0fe35319cadd36fc97ccff1ab98e3e16bc491708 Mon Sep 17 00:00:00 2001 From: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:23:37 -0700 Subject: [PATCH 102/247] add new lp event for devnet event subscriber --- programs/drift/src/state/events.rs | 29 +++++++++++- sdk/src/events/types.ts | 6 ++- sdk/src/idl/drift.json | 72 ++++++++++++++++++++++++++++++ sdk/src/types.ts | 21 +++++++++ 4 files changed, 124 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 9ebe15e0fd..cdf97002e7 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -789,6 +789,8 @@ pub struct LPSettleRecord { pub lp_aum: u128, // current mint price of lp pub lp_price: u128, + // lp pool pubkey + pub lp_pool: Pubkey, } #[event] @@ -830,10 +832,12 @@ pub struct LPSwapRecord { pub out_market_target_weight: i64, pub in_swap_id: u64, pub out_swap_id: u64, + // lp pool pubkey + pub lp_pool: Pubkey, } impl Size for LPSwapRecord { - const SIZE: usize = 376; + const SIZE: usize = 408; } #[event] @@ -868,8 +872,29 @@ pub struct LPMintRedeemRecord { /// PERCENTAGE_PRECISION pub in_market_current_weight: i64, pub in_market_target_weight: i64, + // lp pool pubkey + pub lp_pool: Pubkey, } impl Size for LPMintRedeemRecord { - const SIZE: usize = 328; + const SIZE: usize = 360; +} + +#[event] +#[derive(Default)] +pub struct LPBorrowLendDepositRecord { + pub ts: i64, + pub slot: u64, + pub spot_market_index: u16, + pub constituent_index: u16, + pub direction: DepositDirection, + pub token_balance: i64, + pub last_token_balance: i64, + pub interest_accrued_token_amount: i64, + pub amount_deposit_withdraw: u64, + pub lp_pool: Pubkey, +} + +impl Size for LPBorrowLendDepositRecord { + const SIZE: usize = 104; } diff --git a/sdk/src/events/types.ts b/sdk/src/events/types.ts index 7909992b4e..4614b7e753 100644 --- a/sdk/src/events/types.ts +++ b/sdk/src/events/types.ts @@ -24,6 +24,7 @@ import { LPMintRedeemRecord, LPSettleRecord, LPSwapRecord, + LPBorrowLendDepositRecord, } from '../types'; import { EventEmitter } from 'events'; @@ -116,8 +117,8 @@ export type EventMap = { FuelSeasonRecord: Event; InsuranceFundSwapRecord: Event; TransferProtocolIfSharesToRevenuePoolRecord: Event; - LPMintRedeemRecord: Event; LPSettleRecord: Event; + LPMintRedeemRecord: Event; LPSwapRecord: Event; }; @@ -146,8 +147,9 @@ export type DriftEvent = | Event | Event | Event + | Event | Event - | Event; + | Event; export interface EventSubscriberEvents { newEvent: (event: WrappedEvent) => void; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 837ac89677..10f90b9c8d 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -14875,6 +14875,11 @@ "name": "lpPrice", "type": "u128", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] }, @@ -14985,6 +14990,11 @@ "name": "outSwapId", "type": "u64", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] }, @@ -15080,6 +15090,68 @@ "name": "inMarketTargetWeight", "type": "i64", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "LPBorrowLendDepositRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "slot", + "type": "u64", + "index": false + }, + { + "name": "spotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "constituentIndex", + "type": "u16", + "index": false + }, + { + "name": "direction", + "type": { + "defined": "DepositDirection" + }, + "index": false + }, + { + "name": "tokenBalance", + "type": "i64", + "index": false + }, + { + "name": "lastTokenBalance", + "type": "i64", + "index": false + }, + { + "name": "interestAccruedTokenAmount", + "type": "i64", + "index": false + }, + { + "name": "amountDepositWithdraw", + "type": "u64", + "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index e75c76b261..ae9fc45533 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -767,6 +767,7 @@ export type LPSwapRecord = { outMarketTargetWeight: BN; inSwapId: BN; outSwapId: BN; + lpPool: PublicKey; }; export type LPMintRedeemRecord = { @@ -789,6 +790,7 @@ export type LPMintRedeemRecord = { lastAumSlot: BN; inMarketCurrentWeight: BN; inMarketTargetWeight: BN; + lpPool: PublicKey; }; export type LPSettleRecord = { @@ -803,6 +805,20 @@ export type LPSettleRecord = { perpAmmExFeeDelta: BN; lpAum: BN; lpPrice: BN; + lpPool: PublicKey; +}; + +export type LPBorrowLendDepositRecord = { + ts: BN; + slot: BN; + spotMarketIndex: number; + constituentIndex: number; + direction: DepositDirection; + tokenBalance: BN; + lastTokenBalance: BN; + interestAccruedTokenAmount: BN; + amountDepositWithdraw: BN; + lpPool: PublicKey; }; export type StateAccount = { @@ -878,6 +894,11 @@ export type PerpMarketAccount = { protectedMakerLimitPriceDivisor: number; protectedMakerDynamicDivisor: number; lastFillPrice: BN; + + lpFeeTransferScalar: number; + lpExchangeFeeExcluscionScalar: number; + lpStatus: number; + lpPausedOperations: number; }; export type HistoricalOracleData = { From 0cb9bd3b18a6a82b30b2a74d55b83884ccd40b45 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 02:28:47 +0000 Subject: [PATCH 103/247] sdk: release v2.142.0-beta.12 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index cbde2437b9..ac97ec8cbd 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.11 \ No newline at end of file +2.142.0-beta.12 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 2f98c71c0a..b6790f2084 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.11", + "version": "2.142.0-beta.12", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From fdd89d4ba60b4206b33899b922f60ab16316af7c Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 7 Oct 2025 15:07:57 +0800 Subject: [PATCH 104/247] sdk: add adminDisableUpdatePerpBidAskTwap to adminClient --- sdk/src/adminClient.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index efca781880..b5b6244866 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4835,4 +4835,38 @@ export class AdminClient extends DriftClient { } ); } + + public async adminDisableUpdatePerpBidAskTwap( + authority: PublicKey, + disable: boolean + ): Promise { + const disableBidAskTwapUpdateIx = + await this.getAdminDisableUpdatePerpBidAskTwapIx(authority, disable); + + const tx = await this.buildTransaction(disableBidAskTwapUpdateIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getAdminDisableUpdatePerpBidAskTwapIx( + authority: PublicKey, + disable: boolean + ): Promise { + return await this.program.instruction.adminDisableUpdatePerpBidAskTwap( + disable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + }, + } + ); + } } From 32a31a247ee8794d474958c0e9f2b4919f8a67ed Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:13:51 +0000 Subject: [PATCH 105/247] sdk: release v2.142.0-beta.13 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index ac97ec8cbd..c077ba1aea 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.12 \ No newline at end of file +2.142.0-beta.13 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index b6790f2084..7d34aa81d0 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.12", + "version": "2.142.0-beta.13", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 09a0654ef731cdfa4d1a8d003a37fba20e21d157 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Tue, 7 Oct 2025 12:44:35 -0600 Subject: [PATCH 106/247] fix: rm inheritance from grpc v2 subscriber and implement everything (#1942) * fix: rm inheritance from grpc v2 subscriber and implement everything * fix: lint unused import --- .../grpcDriftClientAccountSubscriberV2.ts | 331 ++++++++++++++++-- .../accounts/grpcMultiAccountSubscriber.ts | 67 +++- 2 files changed, 375 insertions(+), 23 deletions(-) diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index e0a8498539..145a1723b4 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -1,4 +1,5 @@ -import { WebSocketDriftClientAccountSubscriber } from './webSocketDriftClientAccountSubscriber'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; import { OracleInfo, OraclePriceData } from '../oracles/types'; import { Program } from '@coral-xyz/anchor'; import { PublicKey } from '@solana/web3.js'; @@ -6,11 +7,17 @@ import { findAllMarketAndOracles } from '../config'; import { getDriftStateAccountPublicKey, getPerpMarketPublicKey, + getPerpMarketPublicKeySync, getSpotMarketPublicKey, + getSpotMarketPublicKeySync, } from '../addresses/pda'; import { + AccountSubscriber, DataAndSlot, DelistedMarketSetting, + DriftClientAccountEvents, + DriftClientAccountSubscriber, + NotSubscribedError, GrpcConfigs, ResubOpts, } from './types'; @@ -21,14 +28,44 @@ import { getOracleId, getPublicKeyAndSourceFromOracleId, } from '../oracles/oracleId'; +import { OracleClientCache } from '../oracles/oracleClientCache'; +import { findDelistedPerpMarketsAndOracles } from './utils'; -export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAccountSubscriber { +export class grpcDriftClientAccountSubscriberV2 + implements DriftClientAccountSubscriber +{ private grpcConfigs: GrpcConfigs; private perpMarketsSubscriber?: grpcMultiAccountSubscriber; private spotMarketsSubscriber?: grpcMultiAccountSubscriber; private oracleMultiSubscriber?: grpcMultiAccountSubscriber; private perpMarketIndexToAccountPubkeyMap = new Map(); private spotMarketIndexToAccountPubkeyMap = new Map(); + private delistedMarketSetting: DelistedMarketSetting; + + public eventEmitter: StrictEventEmitter< + EventEmitter, + DriftClientAccountEvents + >; + public isSubscribed: boolean; + public isSubscribing: boolean; + public program: Program; + public perpMarketIndexes: number[]; + public spotMarketIndexes: number[]; + public shouldFindAllMarketsAndOracles: boolean; + public oracleInfos: OracleInfo[]; + public initialPerpMarketAccountData: Map; + public initialSpotMarketAccountData: Map; + public initialOraclePriceData: Map; + public perpOracleMap = new Map(); + public perpOracleStringMap = new Map(); + public spotOracleMap = new Map(); + public spotOracleStringMap = new Map(); + public stateAccountSubscriber?: AccountSubscriber; + oracleClientCache = new OracleClientCache(); + private resubOpts?: ResubOpts; + + private subscriptionPromise: Promise; + protected subscriptionPromiseResolver: (val: boolean) => void; constructor( grpcConfigs: GrpcConfigs, @@ -40,16 +77,156 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco delistedMarketSetting: DelistedMarketSetting, resubOpts?: ResubOpts ) { - super( - program, - perpMarketIndexes, - spotMarketIndexes, - oracleInfos, - shouldFindAllMarketsAndOracles, - delistedMarketSetting, - resubOpts - ); + this.eventEmitter = new EventEmitter(); + this.isSubscribed = false; + this.isSubscribing = false; + this.program = program; + this.perpMarketIndexes = perpMarketIndexes; + this.spotMarketIndexes = spotMarketIndexes; + this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles; + this.oracleInfos = oracleInfos; + this.initialPerpMarketAccountData = new Map(); + this.initialSpotMarketAccountData = new Map(); + this.initialOraclePriceData = new Map(); + this.perpOracleMap = new Map(); + this.perpOracleStringMap = new Map(); + this.spotOracleMap = new Map(); + this.spotOracleStringMap = new Map(); this.grpcConfigs = grpcConfigs; + this.resubOpts = resubOpts; + this.delistedMarketSetting = delistedMarketSetting; + } + + chunks = (array: readonly T[], size: number): T[][] => { + return new Array(Math.ceil(array.length / size)) + .fill(null) + .map((_, index) => index * size) + .map((begin) => array.slice(begin, begin + size)); + }; + + async setInitialData(): Promise { + const connection = this.program.provider.connection; + + if ( + !this.initialPerpMarketAccountData || + this.initialPerpMarketAccountData.size === 0 + ) { + const perpMarketPublicKeys = this.perpMarketIndexes.map((marketIndex) => + getPerpMarketPublicKeySync(this.program.programId, marketIndex) + ); + const perpMarketPublicKeysChunks = this.chunks(perpMarketPublicKeys, 75); + const perpMarketAccountInfos = ( + await Promise.all( + perpMarketPublicKeysChunks.map((perpMarketPublicKeysChunk) => + connection.getMultipleAccountsInfo(perpMarketPublicKeysChunk) + ) + ) + ).flat(); + this.initialPerpMarketAccountData = new Map( + perpMarketAccountInfos + .filter((accountInfo) => !!accountInfo) + .map((accountInfo) => { + const perpMarket = this.program.coder.accounts.decode( + 'PerpMarket', + accountInfo.data + ); + return [perpMarket.marketIndex, perpMarket]; + }) + ); + } + + if ( + !this.initialSpotMarketAccountData || + this.initialSpotMarketAccountData.size === 0 + ) { + const spotMarketPublicKeys = this.spotMarketIndexes.map((marketIndex) => + getSpotMarketPublicKeySync(this.program.programId, marketIndex) + ); + const spotMarketPublicKeysChunks = this.chunks(spotMarketPublicKeys, 75); + const spotMarketAccountInfos = ( + await Promise.all( + spotMarketPublicKeysChunks.map((spotMarketPublicKeysChunk) => + connection.getMultipleAccountsInfo(spotMarketPublicKeysChunk) + ) + ) + ).flat(); + this.initialSpotMarketAccountData = new Map( + spotMarketAccountInfos + .filter((accountInfo) => !!accountInfo) + .map((accountInfo) => { + const spotMarket = this.program.coder.accounts.decode( + 'SpotMarket', + accountInfo.data + ); + return [spotMarket.marketIndex, spotMarket]; + }) + ); + } + + const oracleAccountPubkeyChunks = this.chunks( + this.oracleInfos.map((oracleInfo) => oracleInfo.publicKey), + 75 + ); + const oracleAccountInfos = ( + await Promise.all( + oracleAccountPubkeyChunks.map((oracleAccountPublicKeysChunk) => + connection.getMultipleAccountsInfo(oracleAccountPublicKeysChunk) + ) + ) + ).flat(); + this.initialOraclePriceData = new Map( + this.oracleInfos.reduce((result, oracleInfo, i) => { + if (!oracleAccountInfos[i]) { + return result; + } + const oracleClient = this.oracleClientCache.get( + oracleInfo.source, + connection, + this.program + ); + const oraclePriceData = oracleClient.getOraclePriceDataFromBuffer( + oracleAccountInfos[i].data + ); + result.push([ + getOracleId(oracleInfo.publicKey, oracleInfo.source), + oraclePriceData, + ]); + return result; + }, []) + ); + } + + async addPerpMarket(_marketIndex: number): Promise { + if (!this.perpMarketIndexes.includes(_marketIndex)) { + this.perpMarketIndexes = this.perpMarketIndexes.concat(_marketIndex); + } + return true; + } + + async addSpotMarket(_marketIndex: number): Promise { + return true; + } + + async addOracle(oracleInfo: OracleInfo): Promise { + if (oracleInfo.publicKey.equals(PublicKey.default)) { + return true; + } + + const exists = this.oracleInfos.some( + (o) => + o.source === oracleInfo.source && + o.publicKey.equals(oracleInfo.publicKey) + ); + if (!exists) { + this.oracleInfos = this.oracleInfos.concat(oracleInfo); + } + + if (this.oracleMultiSubscriber) { + await this.unsubscribeFromOracles(); + await this.subscribeToOracles(); + } + + return true; } public async subscribe(): Promise { @@ -133,7 +310,37 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco return true; } - override getMarketAccountAndSlot( + public async fetch(): Promise { + await this.stateAccountSubscriber?.fetch(); + await this.perpMarketsSubscriber?.fetch(); + await this.spotMarketsSubscriber?.fetch(); + await this.oracleMultiSubscriber?.fetch(); + } + + private assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } + + public getStateAccountAndSlot(): DataAndSlot { + this.assertIsSubscribed(); + return this.stateAccountSubscriber.dataAndSlot; + } + + public getMarketAccountsAndSlots(): DataAndSlot[] { + const map = this.perpMarketsSubscriber?.getAccountDataMap(); + return Array.from(map?.values() ?? []); + } + + public getSpotMarketAccountsAndSlots(): DataAndSlot[] { + const map = this.spotMarketsSubscriber?.getAccountDataMap(); + return Array.from(map?.values() ?? []); + } + + getMarketAccountAndSlot( marketIndex: number ): DataAndSlot | undefined { return this.perpMarketsSubscriber?.getAccountData( @@ -141,7 +348,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco ); } - override getSpotMarketAccountAndSlot( + getSpotMarketAccountAndSlot( marketIndex: number ): DataAndSlot | undefined { return this.spotMarketsSubscriber?.getAccountData( @@ -149,7 +356,51 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco ); } - override async setPerpOracleMap() { + public getOraclePriceDataAndSlot( + oracleId: string + ): DataAndSlot | undefined { + this.assertIsSubscribed(); + const { publicKey } = getPublicKeyAndSourceFromOracleId(oracleId); + return this.oracleMultiSubscriber?.getAccountData(publicKey.toBase58()); + } + + public getOraclePriceDataAndSlotForPerpMarket( + marketIndex: number + ): DataAndSlot | undefined { + const perpMarketAccount = this.getMarketAccountAndSlot(marketIndex); + const oracle = this.perpOracleMap.get(marketIndex); + const oracleId = this.perpOracleStringMap.get(marketIndex); + if (!perpMarketAccount || !oracleId) { + return undefined; + } + + if (!perpMarketAccount.data.amm.oracle.equals(oracle)) { + // If the oracle has changed, we need to update the oracle map in background + this.setPerpOracleMap(); + } + + return this.getOraclePriceDataAndSlot(oracleId); + } + + public getOraclePriceDataAndSlotForSpotMarket( + marketIndex: number + ): DataAndSlot | undefined { + const spotMarketAccount = this.getSpotMarketAccountAndSlot(marketIndex); + const oracle = this.spotOracleMap.get(marketIndex); + const oracleId = this.spotOracleStringMap.get(marketIndex); + if (!spotMarketAccount || !oracleId) { + return undefined; + } + + if (!spotMarketAccount.data.oracle.equals(oracle)) { + // If the oracle has changed, we need to update the oracle map in background + this.setSpotOracleMap(); + } + + return this.getOraclePriceDataAndSlot(oracleId); + } + + async setPerpOracleMap() { const perpMarketsMap = this.perpMarketsSubscriber?.getAccountDataMap(); const perpMarkets = Array.from(perpMarketsMap.values()); const addOraclePromises = []; @@ -161,7 +412,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco const perpMarketIndex = perpMarketAccount.marketIndex; const oracle = perpMarketAccount.amm.oracle; const oracleId = getOracleId(oracle, perpMarket.data.amm.oracleSource); - if (!this.oracleSubscribers.has(oracleId)) { + if (!this.oracleMultiSubscriber?.getAccountDataMap().has(oracleId)) { addOraclePromises.push( this.addOracle({ publicKey: oracle, @@ -175,7 +426,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco await Promise.all(addOraclePromises); } - override async setSpotOracleMap() { + async setSpotOracleMap() { const spotMarketsMap = this.spotMarketsSubscriber?.getAccountDataMap(); const spotMarkets = Array.from(spotMarketsMap.values()); const addOraclePromises = []; @@ -187,7 +438,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco const spotMarketIndex = spotMarketAccount.marketIndex; const oracle = spotMarketAccount.oracle; const oracleId = getOracleId(oracle, spotMarketAccount.oracleSource); - if (!this.oracleSubscribers.has(oracleId)) { + if (!this.oracleMultiSubscriber?.getAccountDataMap().has(oracleId)) { addOraclePromises.push( this.addOracle({ publicKey: oracle, @@ -201,7 +452,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco await Promise.all(addOraclePromises); } - override async subscribeToPerpMarketAccounts(): Promise { + async subscribeToPerpMarketAccounts(): Promise { const perpMarketIndexToAccountPubkeys: Array<[number, PublicKey]> = await Promise.all( this.perpMarketIndexes.map(async (marketIndex) => [ @@ -263,7 +514,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco return true; } - override async subscribeToSpotMarketAccounts(): Promise { + async subscribeToSpotMarketAccounts(): Promise { const spotMarketIndexToAccountPubkeys: Array<[number, PublicKey]> = await Promise.all( this.spotMarketIndexes.map(async (marketIndex) => [ @@ -325,7 +576,7 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco return true; } - override async subscribeToOracles(): Promise { + async subscribeToOracles(): Promise { const pubkeyToSources = new Map>(); for (const info of this.oracleInfos) { if (info.publicKey.equals((PublicKey as any).default)) { @@ -407,20 +658,56 @@ export class grpcDriftClientAccountSubscriberV2 extends WebSocketDriftClientAcco return true; } + async handleDelistedMarkets(): Promise { + if (this.delistedMarketSetting === DelistedMarketSetting.Subscribe) { + return; + } + + const { perpMarketIndexes, oracles } = findDelistedPerpMarketsAndOracles( + Array.from( + this.perpMarketsSubscriber?.getAccountDataMap().values() || [] + ), + Array.from(this.spotMarketsSubscriber?.getAccountDataMap().values() || []) + ); + + for (const perpMarketIndex of perpMarketIndexes) { + await this.perpMarketsSubscriber.removeAccounts([ + new PublicKey( + this.perpMarketIndexToAccountPubkeyMap.get(perpMarketIndex) || '' + ), + ]); + if (this.delistedMarketSetting === DelistedMarketSetting.Discard) { + this.perpMarketIndexToAccountPubkeyMap.delete(perpMarketIndex); + } + } + + for (const oracle of oracles) { + await this.oracleMultiSubscriber.removeAccounts([oracle.publicKey]); + } + } + + removeInitialData() { + this.initialPerpMarketAccountData = new Map(); + this.initialSpotMarketAccountData = new Map(); + this.initialOraclePriceData = new Map(); + } + async unsubscribeFromOracles(): Promise { if (this.oracleMultiSubscriber) { await this.oracleMultiSubscriber.unsubscribe(); this.oracleMultiSubscriber = undefined; return; } - await super.unsubscribeFromOracles(); } - override async unsubscribe(): Promise { + async unsubscribe(): Promise { if (this.isSubscribed) { return; } await this.stateAccountSubscriber.unsubscribe(); + await this.unsubscribeFromOracles(); + await this.perpMarketsSubscriber?.unsubscribe(); + await this.spotMarketsSubscriber?.unsubscribe(); } } diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index 005240d20e..6b13b2fdec 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -1,5 +1,5 @@ import { Program } from '@coral-xyz/anchor'; -import { Context, PublicKey } from '@solana/web3.js'; +import { Commitment, Context, PublicKey } from '@solana/web3.js'; import * as Buffer from 'buffer'; import bs58 from 'bs58'; @@ -21,6 +21,21 @@ interface AccountInfoLike { rentEpoch: number; } +function commitmentLevelToCommitment( + commitmentLevel: CommitmentLevel +): Commitment { + switch (commitmentLevel) { + case CommitmentLevel.PROCESSED: + return 'processed'; + case CommitmentLevel.CONFIRMED: + return 'confirmed'; + case CommitmentLevel.FINALIZED: + return 'finalized'; + default: + return 'confirmed'; + } +} + export class grpcMultiAccountSubscriber { private client: Client; private stream: ClientDuplexStream; @@ -105,6 +120,56 @@ export class grpcMultiAccountSubscriber { return this.dataMap; } + async fetch(): Promise { + try { + // Chunk account IDs into groups of 100 (getMultipleAccounts limit) + const chunkSize = 100; + const chunks: string[][] = []; + const accountIds = Array.from(this.subscribedAccounts.values()); + for (let i = 0; i < accountIds.length; i += chunkSize) { + chunks.push(accountIds.slice(i, i + chunkSize)); + } + + // Process all chunks concurrently + await Promise.all( + chunks.map(async (chunk) => { + const accountAddresses = chunk.map( + (accountId) => new PublicKey(accountId) + ); + const rpcResponseAndContext = + await this.program.provider.connection.getMultipleAccountsInfoAndContext( + accountAddresses, + { + commitment: commitmentLevelToCommitment(this.commitmentLevel), + } + ); + + const rpcResponse = rpcResponseAndContext.value; + const currentSlot = rpcResponseAndContext.context.slot; + + for (let i = 0; i < chunk.length; i++) { + const accountId = chunk[i]; + const accountInfo = rpcResponse[i]; + if (accountInfo) { + const perpMarket = this.program.coder.accounts.decode( + 'PerpMarket', + accountInfo.data + ); + this.setAccountData(accountId, perpMarket, currentSlot); + } + } + }) + ); + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.accountName}] grpcMultiAccountSubscriber error fetching accounts:`, + error + ); + } + } + } + async subscribe( accounts: PublicKey[], onChange: ( From 16107953f111f1a0027a123646d8559ed83cfe96 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:50:02 +0000 Subject: [PATCH 107/247] sdk: release v2.142.0-beta.14 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index c077ba1aea..36798e69b4 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.13 \ No newline at end of file +2.142.0-beta.14 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 7d34aa81d0..ae28882771 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.13", + "version": "2.142.0-beta.14", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 461ba964ab6d01e553cae2211bd9c8df82e8cdf6 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Tue, 7 Oct 2025 19:05:16 -0600 Subject: [PATCH 108/247] fix: grpc v2 subscriber bug with multiple sources on same oracle (#1944) --- sdk/scripts/client-test.ts | 285 ++++++++---------- .../grpcDriftClientAccountSubscriberV2.ts | 130 ++++---- .../accounts/grpcMultiAccountSubscriber.ts | 81 +++-- 3 files changed, 245 insertions(+), 251 deletions(-) diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/client-test.ts index af78b56dbc..ca5cedd0fa 100644 --- a/sdk/scripts/client-test.ts +++ b/sdk/scripts/client-test.ts @@ -1,12 +1,14 @@ import { DriftClient } from '../src/driftClient'; import { grpcDriftClientAccountSubscriberV2 } from '../src/accounts/grpcDriftClientAccountSubscriberV2'; +import { grpcDriftClientAccountSubscriber } from '../src/accounts/grpcDriftClientAccountSubscriber'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { DriftClientConfig } from '../src/driftClientConfig'; import { - decodeName, DRIFT_PROGRAM_ID, PerpMarketAccount, + SpotMarketAccount, Wallet, + OracleInfo, } from '../src'; import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; import dotenv from 'dotenv'; @@ -21,8 +23,8 @@ import driftIDL from '../src/idl/drift.json'; const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; const TOKEN = process.env.TOKEN; -async function initializeGrpcDriftClientV2() { - const connection = new Connection('https://api.mainnet-beta.solana.com'); +async function initializeGrpcDriftClientV2VersusV1() { + const connection = new Connection(''); const wallet = new Wallet(new Keypair()); dotenv.config({ path: '../' }); @@ -38,177 +40,136 @@ async function initializeGrpcDriftClientV2() { const program = new Program(driftIDL as Idl, programId, provider); - const perpMarketProgramAccounts = - (await program.account.perpMarket.all()) as ProgramAccount[]; - const solPerpMarket = perpMarketProgramAccounts.find( - (account) => account.account.marketIndex === 0 - ); - const solOracleInfo = { - publicKey: solPerpMarket.account.amm.oracle, - source: solPerpMarket.account.amm.oracleSource, - }; - const ethPerpMarket = perpMarketProgramAccounts.find( - (account) => account.account.marketIndex === 2 - ); - const ethOracleInfo = { - publicKey: ethPerpMarket.account.amm.oracle, - source: ethPerpMarket.account.amm.oracleSource, - }; - const btcPerpMarket = perpMarketProgramAccounts.find( - (account) => account.account.marketIndex === 1 - ); - const btcOracleInfo = { - publicKey: btcPerpMarket.account.amm.oracle, - source: btcPerpMarket.account.amm.oracleSource, + const perpMarketIndexes = [4]; + const spotMarketIndexes = [32]; + + const perpMarketProgramAccounts = ( + await program.account.perpMarket.all() + ).filter((a) => + perpMarketIndexes.includes(a.account.marketIndex as number) + ) as ProgramAccount[]; + const spotMarketProgramAccounts = ( + await program.account.spotMarket.all() + ).filter((a) => + spotMarketIndexes.includes(a.account.marketIndex as number) + ) as ProgramAccount[]; + + // const perpMarketIndexes = perpMarketProgramAccounts.map( + // (a) => a.account.marketIndex + // ); + // const spotMarketIndexes = spotMarketProgramAccounts.map( + // (a) => a.account.marketIndex + // ); + // const oracleInfos = [ + // { + // publicKey: new PublicKey('BERaNi6cpEresbq6HC1EQGaB1H1UjvEo4NGnmYSSJof4'), + // source: OracleSource.PYTH_LAZER, + // }, + // { + // publicKey: new PublicKey('BERaNi6cpEresbq6HC1EQGaB1H1UjvEo4NGnmYSSJof4'), + // source: OracleSource.PYTH_LAZER_1M, + // }, + // ]; + + const seen = new Set(); + const oracleInfos: OracleInfo[] = []; + for (const acct of perpMarketProgramAccounts) { + const key = `${acct.account.amm.oracle.toBase58()}-${Object.keys( + acct.account.amm.oracleSource ?? {} + )?.[0]}`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.amm.oracle, + source: acct.account.amm.oracleSource, + }); + } + } + for (const acct of spotMarketProgramAccounts) { + const key = `${acct.account.oracle.toBase58()}-${Object.keys( + acct.account.oracleSource ?? {} + )?.[0]}`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.oracle, + source: acct.account.oracleSource, + }); + } + } + + const baseAccountSubscription = { + type: 'grpc' as const, + grpcConfigs: { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: 'confirmed' as unknown as CommitmentLevel, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }, }; - const config: DriftClientConfig = { + const configV2: DriftClientConfig = { connection, wallet, programID: new PublicKey(DRIFT_PROGRAM_ID), accountSubscription: { - type: 'grpc', - grpcConfigs: { - endpoint: GRPC_ENDPOINT, - token: TOKEN, - commitmentLevel: 'confirmed' as unknown as CommitmentLevel, - channelOptions: { - 'grpc.keepalive_time_ms': 10_000, - 'grpc.keepalive_timeout_ms': 1_000, - 'grpc.keepalive_permit_without_calls': 1, - }, - }, + ...baseAccountSubscription, driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, }, - perpMarketIndexes: [0, 1, 2], - spotMarketIndexes: [0, 1, 2], - oracleInfos: [solOracleInfo, ethOracleInfo, btcOracleInfo], + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, }; - const driftClient = new DriftClient(config); - - let perpMarketUpdateCount = 0; - let spotMarketUpdateCount = 0; - let oraclePriceUpdateCount = 0; - let userAccountUpdateCount = 0; - - const updatePromise = new Promise((resolve) => { - driftClient.accountSubscriber.eventEmitter.on( - 'perpMarketAccountUpdate', - (data) => { - console.log( - 'Perp market account update:', - decodeName(data.name), - 'mmOracleSequenceId:', - data.amm.mmOracleSequenceId.toString() - ); - // const perpMarketData = driftClient.getPerpMarketAccount( - // data.marketIndex - // ); - // console.log( - // 'Perp market data market index:', - // perpMarketData?.marketIndex - // ); - // const oracle = driftClient.getOracleDataForPerpMarket(data.marketIndex); - // const mmOracle = driftClient.getMMOracleDataForPerpMarket( - // data.marketIndex - // ); - // console.log('Perp oracle price:', oracle.price.toString()); - // console.log('Perp MM oracle price:', mmOracle.price.toString()); - // console.log( - // 'Perp MM oracle sequence id:', - // perpMarketData?.amm?.mmOracleSequenceId?.toString() - // ); - perpMarketUpdateCount++; - if ( - perpMarketUpdateCount >= 10 && - spotMarketUpdateCount >= 10 && - oraclePriceUpdateCount >= 10 && - userAccountUpdateCount >= 2 - ) { - resolve(); - } - } - ); - - driftClient.accountSubscriber.eventEmitter.on( - 'spotMarketAccountUpdate', - (data) => { - console.log('Spot market account update:', decodeName(data.name)); - const spotMarketData = driftClient.getSpotMarketAccount( - data.marketIndex - ); - console.log( - 'Spot market data market index:', - spotMarketData?.marketIndex - ); - const oracle = driftClient.getOracleDataForSpotMarket(data.marketIndex); - console.log('Spot oracle price:', oracle.price.toString()); - spotMarketUpdateCount++; - if ( - perpMarketUpdateCount >= 10 && - spotMarketUpdateCount >= 10 && - oraclePriceUpdateCount >= 10 && - userAccountUpdateCount >= 2 - ) { - resolve(); - } - } - ); - - driftClient.accountSubscriber.eventEmitter.on( - 'oraclePriceUpdate', - (data) => { - console.log('Oracle price update:', data.toBase58()); - oraclePriceUpdateCount++; - if ( - perpMarketUpdateCount >= 10 && - spotMarketUpdateCount >= 10 && - oraclePriceUpdateCount >= 10 && - userAccountUpdateCount >= 2 - ) { - resolve(); - } - } - ); - - driftClient.accountSubscriber.eventEmitter.on( - 'userAccountUpdate', - (data) => { - console.log('User account update:', decodeName(data.name)); - userAccountUpdateCount++; - if ( - perpMarketUpdateCount >= 10 && - spotMarketUpdateCount >= 10 && - oraclePriceUpdateCount >= 10 && - userAccountUpdateCount >= 2 - ) { - resolve(); - } - } - ); - }); - - await driftClient.subscribe(); - console.log('DriftClient initialized and listening for updates.'); - - for (const marketIndex of config.perpMarketIndexes) { - const oracle = driftClient.getOracleDataForPerpMarket(marketIndex); - const mmOracle = driftClient.getMMOracleDataForPerpMarket(marketIndex); - console.log('Initial perp oracle price:', oracle.price.toString()); - console.log('Initial perp MM oracle price:', mmOracle.price.toString()); - } + const configV1: DriftClientConfig = { + connection, + wallet, + programID: new PublicKey(DRIFT_PROGRAM_ID), + accountSubscription: { + ...baseAccountSubscription, + driftClientAccountSubscriber: grpcDriftClientAccountSubscriber, + }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + }; - for (const marketIndex of config.spotMarketIndexes) { - const oracle = driftClient.getOracleDataForSpotMarket(marketIndex); - console.log('Initial spot oracle price:', oracle.price.toString()); - } + const clientV2 = new DriftClient(configV2); + const clientV1 = new DriftClient(configV1); + + await Promise.all([clientV1.subscribe(), clientV2.subscribe()]); + const compare = () => { + for (const idx of perpMarketIndexes) { + const p1 = clientV1.getOracleDataForPerpMarket(idx).price; + const p2 = clientV2.getOracleDataForPerpMarket(idx).price; + console.log( + `perp mkt ${idx} | v1 ${p1.toString()} | v2 ${p2.toString()}` + ); + } + for (const idx of spotMarketIndexes) { + const s1 = clientV1.getOracleDataForSpotMarket(idx).price; + const s2 = clientV2.getOracleDataForSpotMarket(idx).price; + console.log( + `spot mkt ${idx} | v1 ${s1.toString()} | v2 ${s2.toString()}` + ); + } + }; + + compare(); + const interval = setInterval(compare, 1000); - const stateAccount = driftClient.getStateAccount(); - console.log('Initial state account:', stateAccount.toString()); + const cleanup = async () => { + clearInterval(interval); + await Promise.all([clientV1.unsubscribe(), clientV2.unsubscribe()]); + process.exit(0); + }; - await updatePromise; - console.log('Received required number of updates.'); + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); } -initializeGrpcDriftClientV2().catch(console.error); +initializeGrpcDriftClientV2VersusV1().catch(console.error); diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 145a1723b4..2ef8121c84 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -37,7 +37,10 @@ export class grpcDriftClientAccountSubscriberV2 private grpcConfigs: GrpcConfigs; private perpMarketsSubscriber?: grpcMultiAccountSubscriber; private spotMarketsSubscriber?: grpcMultiAccountSubscriber; - private oracleMultiSubscriber?: grpcMultiAccountSubscriber; + private oracleMultiSubscriber?: grpcMultiAccountSubscriber< + OraclePriceData, + OracleInfo + >; private perpMarketIndexToAccountPubkeyMap = new Map(); private spotMarketIndexToAccountPubkeyMap = new Map(); private delistedMarketSetting: DelistedMarketSetting; @@ -60,6 +63,10 @@ export class grpcDriftClientAccountSubscriberV2 public perpOracleStringMap = new Map(); public spotOracleMap = new Map(); public spotOracleStringMap = new Map(); + private oracleIdToOracleDataMap = new Map< + string, + DataAndSlot + >(); public stateAccountSubscriber?: AccountSubscriber; oracleClientCache = new OracleClientCache(); private resubOpts?: ResubOpts; @@ -360,8 +367,9 @@ export class grpcDriftClientAccountSubscriberV2 oracleId: string ): DataAndSlot | undefined { this.assertIsSubscribed(); - const { publicKey } = getPublicKeyAndSourceFromOracleId(oracleId); - return this.oracleMultiSubscriber?.getAccountData(publicKey.toBase58()); + // we need to rely on a map we store in this class because the grpcMultiAccountSubscriber does not track a mapping or oracle ID. + // DO NOT call getAccountData on the oracleMultiSubscriber, it will not return the correct data in certain cases(BONK spot and perp market subscribed too at once). + return this.oracleIdToOracleDataMap.get(oracleId); } public getOraclePriceDataAndSlotForPerpMarket( @@ -577,80 +585,80 @@ export class grpcDriftClientAccountSubscriberV2 } async subscribeToOracles(): Promise { - const pubkeyToSources = new Map>(); + const oraclePubkeyToInfosMap = new Map(); for (const info of this.oracleInfos) { - if (info.publicKey.equals((PublicKey as any).default)) { - continue; + const pubkey = info.publicKey.toBase58(); + if (!oraclePubkeyToInfosMap.has(pubkey)) { + oraclePubkeyToInfosMap.set(pubkey, []); } - const key = info.publicKey.toBase58(); - let sources = pubkeyToSources.get(key); - if (!sources) { - sources = new Set(); - pubkeyToSources.set(key, sources); - } - sources.add(info.source); + oraclePubkeyToInfosMap.get(pubkey).push(info); } - const oraclePubkeys = Array.from(pubkeyToSources.keys()).map( - (k) => new PublicKey(k) + const oraclePubkeys = Array.from( + new Set(this.oracleInfos.map((info) => info.publicKey)) ); - this.oracleMultiSubscriber = - await grpcMultiAccountSubscriber.create( - this.grpcConfigs, - 'oracle', - this.program, - (buffer: Buffer, pubkey?: string) => { - if (!pubkey) { - throw new Error('Oracle pubkey missing in decode'); - } - const sources = pubkeyToSources.get(pubkey); - if (!sources || sources.size === 0) { - throw new Error('Oracle sources missing for pubkey in decode'); - } - const primarySource = sources.values().next().value; - const client = this.oracleClientCache.get( - primarySource, - this.program.provider.connection, - this.program - ); - return client.getOraclePriceDataFromBuffer(buffer); - }, - this.resubOpts, - undefined, - async () => { - try { - if (this.resubOpts?.logResubMessages) { - console.log( - '[grpcDriftClientAccountSubscriberV2] oracle subscriber unsubscribed; resubscribing' - ); - } - await this.subscribeToOracles(); - } catch (e) { - console.error('Oracle resubscribe failed:', e); + this.oracleMultiSubscriber = await grpcMultiAccountSubscriber.create< + OraclePriceData, + OracleInfo + >( + this.grpcConfigs, + 'oracle', + this.program, + (buffer: Buffer, pubkey?: string, accountProps?: OracleInfo) => { + if (!pubkey) { + throw new Error('Oracle pubkey missing in decode'); + } + + const client = this.oracleClientCache.get( + accountProps.source, + this.program.provider.connection, + this.program + ); + const price = client.getOraclePriceDataFromBuffer(buffer); + return price; + }, + this.resubOpts, + undefined, + async () => { + try { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] oracle subscriber unsubscribed; resubscribing' + ); } + await this.subscribeToOracles(); + } catch (e) { + console.error('Oracle resubscribe failed:', e); } - ); + }, + oraclePubkeyToInfosMap + ); for (const data of this.initialOraclePriceData.entries()) { const { publicKey } = getPublicKeyAndSourceFromOracleId(data[0]); this.oracleMultiSubscriber.setAccountData(publicKey.toBase58(), data[1]); + this.oracleIdToOracleDataMap.set(data[0], { + data: data[1], + slot: 0, + }); } await this.oracleMultiSubscriber.subscribe( oraclePubkeys, - (accountId, data) => { - const sources = pubkeyToSources.get(accountId.toBase58()); - if (sources) { - for (const source of sources.values()) { - this.eventEmitter.emit( - 'oraclePriceUpdate', - accountId, - source, - data - ); - } - } + (accountId, data, context, _b, accountProps) => { + const oracleId = getOracleId(accountId, accountProps.source); + this.oracleIdToOracleDataMap.set(oracleId, { + data, + slot: context.slot, + }); + this.eventEmitter.emit( + 'oraclePriceUpdate', + accountId, + accountProps.source, + data + ); + this.eventEmitter.emit('update'); } ); diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index 6b13b2fdec..bae6db213e 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -36,13 +36,17 @@ function commitmentLevelToCommitment( } } -export class grpcMultiAccountSubscriber { +export class grpcMultiAccountSubscriber { private client: Client; private stream: ClientDuplexStream; private commitmentLevel: CommitmentLevel; private program: Program; private accountName: string; - private decodeBufferFn?: (buffer: Buffer, pubkey?: string) => T; + private decodeBufferFn?: ( + buffer: Buffer, + pubkey?: string, + accountProps?: U + ) => T; private resubOpts?: ResubOpts; private onUnsubscribe?: () => Promise; @@ -54,10 +58,11 @@ export class grpcMultiAccountSubscriber { private subscribedAccounts = new Set(); private onChangeMap = new Map< string, - (data: T, context: Context, buffer: Buffer) => void + (data: T, context: Context, buffer: Buffer, accountProps: U) => void >(); private dataMap = new Map>(); + private accountPropsMap = new Map>(); private constructor( client: Client, @@ -66,7 +71,8 @@ export class grpcMultiAccountSubscriber { program: Program, decodeBuffer?: (buffer: Buffer, pubkey?: string) => T, resubOpts?: ResubOpts, - onUnsubscribe?: () => Promise + onUnsubscribe?: () => Promise, + accountPropsMap?: Map> ) { this.client = client; this.commitmentLevel = commitmentLevel; @@ -75,17 +81,19 @@ export class grpcMultiAccountSubscriber { this.decodeBufferFn = decodeBuffer; this.resubOpts = resubOpts; this.onUnsubscribe = onUnsubscribe; + this.accountPropsMap = accountPropsMap; } - public static async create( + public static async create( grpcConfigs: GrpcConfigs, accountName: string, program: Program, - decodeBuffer?: (buffer: Buffer, pubkey?: string) => U, + decodeBuffer?: (buffer: Buffer, pubkey?: string, accountProps?: U) => T, resubOpts?: ResubOpts, clientProp?: Client, - onUnsubscribe?: () => Promise - ): Promise> { + onUnsubscribe?: () => Promise, + accountPropsMap?: Map> + ): Promise> { const client = clientProp ? clientProp : await createClient( @@ -104,7 +112,8 @@ export class grpcMultiAccountSubscriber { program, decodeBuffer, resubOpts, - onUnsubscribe + onUnsubscribe, + accountPropsMap ); } @@ -176,7 +185,8 @@ export class grpcMultiAccountSubscriber { accountId: PublicKey, data: T, context: Context, - buffer: Buffer + buffer: Buffer, + accountProps: U ) => void ): Promise { if (this.listenerId != null || this.isUnsubscribing) { @@ -187,9 +197,9 @@ export class grpcMultiAccountSubscriber { for (const pk of accounts) { const key = pk.toBase58(); this.subscribedAccounts.add(key); - this.onChangeMap.set(key, (data, ctx, buffer) => { + this.onChangeMap.set(key, (data, ctx, buffer, accountProps) => { this.setAccountData(key, data, ctx.slot); - onChange(new PublicKey(key), data, ctx, buffer); + onChange(new PublicKey(key), data, ctx, buffer, accountProps); }); } @@ -235,23 +245,38 @@ export class grpcMultiAccountSubscriber { const context = { slot } as Context; const buffer = accountInfo.data; - const data = this.decodeBufferFn - ? this.decodeBufferFn(buffer, accountPubkey) - : this.program.account[this.accountName].coder.accounts.decode( - this.capitalize(this.accountName), - buffer - ); - - const handler = this.onChangeMap.get(accountPubkey); - if (handler) { - if (this.resubOpts?.resubTimeoutMs) { - this.receivingData = true; - clearTimeout(this.timeoutId); - handler(data, context, buffer); - this.setTimeout(); - } else { - handler(data, context, buffer); + const accountProps = this.accountPropsMap?.get(accountPubkey); + + const handleDataBuffer = ( + context: Context, + buffer: Buffer, + accountProps: U + ) => { + const data = this.decodeBufferFn + ? this.decodeBufferFn(buffer, accountPubkey, accountProps) + : this.program.account[this.accountName].coder.accounts.decode( + this.capitalize(this.accountName), + buffer + ); + const handler = this.onChangeMap.get(accountPubkey); + if (handler) { + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + handler(data, context, buffer, accountProps); + this.setTimeout(); + } else { + handler(data, context, buffer, accountProps); + } + } + }; + + if (Array.isArray(accountProps)) { + for (const props of accountProps) { + handleDataBuffer(context, buffer, props); } + } else { + handleDataBuffer(context, buffer, accountProps); } }); From 061bba6a71e36b2ddd81f85bae3f61fe897ae531 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 01:10:11 +0000 Subject: [PATCH 109/247] sdk: release v2.142.0-beta.15 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 36798e69b4..93172f59bf 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.14 \ No newline at end of file +2.142.0-beta.15 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index ae28882771..e401ec20a7 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.14", + "version": "2.142.0-beta.15", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 3a20a280253ad4dee1832cda0b902a642df9d8ea Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Wed, 8 Oct 2025 10:39:44 +0800 Subject: [PATCH 110/247] docs(sdk): add docs for changeApprovedBuilder --- sdk/src/driftClient.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 593b8c3207..0071bfb7d9 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1373,6 +1373,16 @@ export class DriftClient { }); } + /** + * Creates the transaction to add or update an approved builder. + * This allows the builder to receive revenue share from referrals. + * + * @param builder - The public key of the builder to add or update. + * @param maxFeeTenthBps - The maximum fee tenth bps to set for the builder. + * @param add - Whether to add or update the builder. If the builder already exists, `add = true` will update the `maxFeeTenthBps`, otherwise it will add the builder. If `add = false`, the builder's `maxFeeTenthBps` will be set to 0. + * @param txParams - The transaction parameters to use for the transaction. + * @returns The transaction to add or update an approved builder. + */ public async changeApprovedBuilder( builder: PublicKey, maxFeeTenthBps: number, @@ -1389,6 +1399,15 @@ export class DriftClient { return txSig; } + /** + * Creates the transaction instruction to add or update an approved builder. + * This allows the builder to receive revenue share from referrals. + * + * @param builder - The public key of the builder to add or update. + * @param maxFeeTenthBps - The maximum fee tenth bps to set for the builder. + * @param add - Whether to add or update the builder. If the builder already exists, `add = true` will update the `maxFeeTenthBps`, otherwise it will add the builder. If `add = false`, the builder's `maxFeeTenthBps` will be set to 0. + * @returns The transaction instruction to add or update an approved builder. + */ public async getChangeApprovedBuilderIx( builder: PublicKey, maxFeeTenthBps: number, From a9198ab414e8dde157d6a59b10cf6026cf2ce34d Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 02:45:08 +0000 Subject: [PATCH 111/247] sdk: release v2.142.0-beta.16 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 93172f59bf..211d8d386c 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.15 \ No newline at end of file +2.142.0-beta.16 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index e401ec20a7..c19dcf66ff 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.15", + "version": "2.142.0-beta.16", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 44bbd3f323359bca712bae869a31c21eddc22faf Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 8 Oct 2025 09:29:51 -0600 Subject: [PATCH 112/247] fix: grpc v2 doing extra subscribes for oracles (#1945) --- sdk/scripts/client-test.ts | 12 ++++++++++- .../grpcDriftClientAccountSubscriberV2.ts | 21 +++++++++++++++---- .../accounts/grpcMultiAccountSubscriber.ts | 3 +++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/client-test.ts index ca5cedd0fa..2579ab97e3 100644 --- a/sdk/scripts/client-test.ts +++ b/sdk/scripts/client-test.ts @@ -24,7 +24,7 @@ const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; const TOKEN = process.env.TOKEN; async function initializeGrpcDriftClientV2VersusV1() { - const connection = new Connection(''); + const connection = new Connection('https://api.mainnet-beta.solana.com'); const wallet = new Wallet(new Keypair()); dotenv.config({ path: '../' }); @@ -119,6 +119,7 @@ async function initializeGrpcDriftClientV2VersusV1() { accountSubscription: { ...baseAccountSubscription, driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, + logResubMessages: true, }, perpMarketIndexes, spotMarketIndexes, @@ -132,6 +133,7 @@ async function initializeGrpcDriftClientV2VersusV1() { accountSubscription: { ...baseAccountSubscription, driftClientAccountSubscriber: grpcDriftClientAccountSubscriber, + // logResubMessages: true, }, perpMarketIndexes, spotMarketIndexes, @@ -142,6 +144,14 @@ async function initializeGrpcDriftClientV2VersusV1() { const clientV1 = new DriftClient(configV1); await Promise.all([clientV1.subscribe(), clientV2.subscribe()]); + + clientV2.eventEmitter.on('oraclePriceUpdate', (pubkey, source, data) => { + const key = pubkey.toBase58(); + const src = Object.keys(source ?? {})[0]; + console.log( + `v2 oracle update ${key} (${src}) price ${data.price.toString()}` + ); + }); const compare = () => { for (const idx of perpMarketIndexes) { const p1 = clientV1.getOracleDataForPerpMarket(idx).price; diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 2ef8121c84..6008806ddd 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -215,6 +215,9 @@ export class grpcDriftClientAccountSubscriberV2 } async addOracle(oracleInfo: OracleInfo): Promise { + if (this.resubOpts?.logResubMessages) { + console.log('[grpcDriftClientAccountSubscriberV2] addOracle'); + } if (oracleInfo.publicKey.equals(PublicKey.default)) { return true; } @@ -228,10 +231,7 @@ export class grpcDriftClientAccountSubscriberV2 this.oracleInfos = this.oracleInfos.concat(oracleInfo); } - if (this.oracleMultiSubscriber) { - await this.unsubscribeFromOracles(); - await this.subscribeToOracles(); - } + this.oracleMultiSubscriber?.addAccounts([oracleInfo.publicKey]); return true; } @@ -461,6 +461,11 @@ export class grpcDriftClientAccountSubscriberV2 } async subscribeToPerpMarketAccounts(): Promise { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] subscribeToPerpMarketAccounts' + ); + } const perpMarketIndexToAccountPubkeys: Array<[number, PublicKey]> = await Promise.all( this.perpMarketIndexes.map(async (marketIndex) => [ @@ -523,6 +528,11 @@ export class grpcDriftClientAccountSubscriberV2 } async subscribeToSpotMarketAccounts(): Promise { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] subscribeToSpotMarketAccounts' + ); + } const spotMarketIndexToAccountPubkeys: Array<[number, PublicKey]> = await Promise.all( this.spotMarketIndexes.map(async (marketIndex) => [ @@ -585,6 +595,9 @@ export class grpcDriftClientAccountSubscriberV2 } async subscribeToOracles(): Promise { + if (this.resubOpts?.logResubMessages) { + console.log('grpcDriftClientAccountSubscriberV2 subscribeToOracles'); + } const oraclePubkeyToInfosMap = new Map(); for (const info of this.oracleInfos) { const pubkey = info.publicKey.toBase58(); diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index bae6db213e..7e47b71fa9 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -189,6 +189,9 @@ export class grpcMultiAccountSubscriber { accountProps: U ) => void ): Promise { + if (this.resubOpts?.logResubMessages) { + console.log(`[${this.accountName}] grpcMultiAccountSubscriber subscribe`); + } if (this.listenerId != null || this.isUnsubscribing) { return; } From 40ce34518141e9b6ed47b60b003e514996b894f6 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:35:28 +0000 Subject: [PATCH 113/247] sdk: release v2.142.0-beta.17 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 211d8d386c..5958012b54 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.16 \ No newline at end of file +2.142.0-beta.17 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c19dcf66ff..fa96fe00cf 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.16", + "version": "2.142.0-beta.17", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From a21332f5bb6adb49f67b72a45e234cb95a26938c Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Thu, 9 Oct 2025 11:19:56 -0600 Subject: [PATCH 114/247] fix: grpc multi acct subscriber missing slot check (#1947) * fix: grpc multi acct subscriber missing slot check * fix: lint issue --- sdk/scripts/client-test.ts | 308 ++++++++++++++---- .../accounts/grpcMultiAccountSubscriber.ts | 19 +- 2 files changed, 262 insertions(+), 65 deletions(-) diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/client-test.ts index 2579ab97e3..c2d7c7947f 100644 --- a/sdk/scripts/client-test.ts +++ b/sdk/scripts/client-test.ts @@ -19,12 +19,14 @@ import { ProgramAccount, } from '@coral-xyz/anchor'; import driftIDL from '../src/idl/drift.json'; +import assert from 'assert'; const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; const TOKEN = process.env.TOKEN; +const RPC_ENDPOINT = process.env.RPC_ENDPOINT; async function initializeGrpcDriftClientV2VersusV1() { - const connection = new Connection('https://api.mainnet-beta.solana.com'); + const connection = new Connection(RPC_ENDPOINT); const wallet = new Wallet(new Keypair()); dotenv.config({ path: '../' }); @@ -34,49 +36,38 @@ async function initializeGrpcDriftClientV2VersusV1() { // @ts-ignore wallet, { - commitment: 'confirmed', + commitment: 'processed', } ); const program = new Program(driftIDL as Idl, programId, provider); - const perpMarketIndexes = [4]; - const spotMarketIndexes = [32]; - - const perpMarketProgramAccounts = ( - await program.account.perpMarket.all() - ).filter((a) => - perpMarketIndexes.includes(a.account.marketIndex as number) - ) as ProgramAccount[]; - const spotMarketProgramAccounts = ( - await program.account.spotMarket.all() - ).filter((a) => - spotMarketIndexes.includes(a.account.marketIndex as number) - ) as ProgramAccount[]; - - // const perpMarketIndexes = perpMarketProgramAccounts.map( - // (a) => a.account.marketIndex - // ); - // const spotMarketIndexes = spotMarketProgramAccounts.map( - // (a) => a.account.marketIndex - // ); - // const oracleInfos = [ - // { - // publicKey: new PublicKey('BERaNi6cpEresbq6HC1EQGaB1H1UjvEo4NGnmYSSJof4'), - // source: OracleSource.PYTH_LAZER, - // }, - // { - // publicKey: new PublicKey('BERaNi6cpEresbq6HC1EQGaB1H1UjvEo4NGnmYSSJof4'), - // source: OracleSource.PYTH_LAZER_1M, - // }, - // ]; + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketProgramAccounts = allPerpMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].includes( + val.account.marketIndex + ) + ); + const perpMarketIndexes = perpMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketProgramAccounts = allSpotMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5].includes(val.account.marketIndex) + ); + const spotMarketIndexes = spotMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); const seen = new Set(); const oracleInfos: OracleInfo[] = []; for (const acct of perpMarketProgramAccounts) { - const key = `${acct.account.amm.oracle.toBase58()}-${Object.keys( - acct.account.amm.oracleSource ?? {} - )?.[0]}`; + const key = `${acct.account.amm.oracle.toBase58()}-${ + Object.keys(acct.account.amm.oracleSource)[0] + }`; if (!seen.has(key)) { seen.add(key); oracleInfos.push({ @@ -86,9 +77,9 @@ async function initializeGrpcDriftClientV2VersusV1() { } } for (const acct of spotMarketProgramAccounts) { - const key = `${acct.account.oracle.toBase58()}-${Object.keys( - acct.account.oracleSource ?? {} - )?.[0]}`; + const key = `${acct.account.oracle.toBase58()}-${ + Object.keys(acct.account.oracleSource)[0] + }`; if (!seen.has(key)) { seen.add(key); oracleInfos.push({ @@ -103,7 +94,7 @@ async function initializeGrpcDriftClientV2VersusV1() { grpcConfigs: { endpoint: GRPC_ENDPOINT, token: TOKEN, - commitmentLevel: 'confirmed' as unknown as CommitmentLevel, + commitmentLevel: CommitmentLevel.PROCESSED, channelOptions: { 'grpc.keepalive_time_ms': 10_000, 'grpc.keepalive_timeout_ms': 1_000, @@ -119,7 +110,6 @@ async function initializeGrpcDriftClientV2VersusV1() { accountSubscription: { ...baseAccountSubscription, driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, - logResubMessages: true, }, perpMarketIndexes, spotMarketIndexes, @@ -133,7 +123,6 @@ async function initializeGrpcDriftClientV2VersusV1() { accountSubscription: { ...baseAccountSubscription, driftClientAccountSubscriber: grpcDriftClientAccountSubscriber, - // logResubMessages: true, }, perpMarketIndexes, spotMarketIndexes, @@ -144,28 +133,227 @@ async function initializeGrpcDriftClientV2VersusV1() { const clientV1 = new DriftClient(configV1); await Promise.all([clientV1.subscribe(), clientV2.subscribe()]); - - clientV2.eventEmitter.on('oraclePriceUpdate', (pubkey, source, data) => { - const key = pubkey.toBase58(); - const src = Object.keys(source ?? {})[0]; - console.log( - `v2 oracle update ${key} (${src}) price ${data.price.toString()}` - ); - }); const compare = () => { - for (const idx of perpMarketIndexes) { - const p1 = clientV1.getOracleDataForPerpMarket(idx).price; - const p2 = clientV2.getOracleDataForPerpMarket(idx).price; - console.log( - `perp mkt ${idx} | v1 ${p1.toString()} | v2 ${p2.toString()}` + try { + // 1. Test getStateAccountAndSlot + const state1 = clientV1.accountSubscriber.getStateAccountAndSlot(); + const state2 = clientV2.accountSubscriber.getStateAccountAndSlot(); + assert.deepStrictEqual( + state1.data, + state2.data, + 'State accounts should match' ); - } - for (const idx of spotMarketIndexes) { - const s1 = clientV1.getOracleDataForSpotMarket(idx).price; - const s2 = clientV2.getOracleDataForSpotMarket(idx).price; - console.log( - `spot mkt ${idx} | v1 ${s1.toString()} | v2 ${s2.toString()}` + if ( + state1.slot !== undefined && + state2.slot !== undefined && + state2.slot < state1.slot + ) { + console.error( + `State account slot regression: v2 slot ${state2.slot} < v1 slot ${state1.slot}` + ); + } + + // 2. Test getMarketAccountsAndSlots (all perp markets) - sorted comparison + const allPerpMarkets1 = clientV1.accountSubscriber + .getMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + const allPerpMarkets2 = clientV2.accountSubscriber + .getMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + assert.strictEqual( + allPerpMarkets1.length, + allPerpMarkets2.length, + 'Number of perp markets should match' ); + + // Compare each perp market in the sorted arrays + for (let i = 0; i < allPerpMarkets1.length; i++) { + const market1 = allPerpMarkets1[i]; + const market2 = allPerpMarkets2[i]; + assert.strictEqual( + market1.data.marketIndex, + market2.data.marketIndex, + `Perp market at position ${i} should have same marketIndex` + ); + // assert.deepStrictEqual( + // market1.data, + // market2.data, + // `Perp market ${market1.data.marketIndex} (from getMarketAccountsAndSlots) should match` + // ); + } + + // 3. Test getMarketAccountAndSlot for each perp market + for (const idx of perpMarketIndexes) { + const market1 = clientV1.accountSubscriber.getMarketAccountAndSlot(idx); + const market2 = clientV2.accountSubscriber.getMarketAccountAndSlot(idx); + // assert.deepStrictEqual( + // market1?.data, + // market2?.data, + // `Perp market ${idx} data should match` + // ); + // assert.strictEqual( + // market1?.slot, + // market2?.slot, + // `Perp market ${idx} slot should match` + // ); + if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot < market1.slot + ) { + console.error( + `Perp market ${idx} slot regression: v2 slot ${market2.slot} < v1 slot ${market1.slot}` + ); + } else if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot > market1.slot + ) { + console.info( + `Perp market ${idx} slot is FASTER! v2: ${market2.slot}, v1: ${market1.slot}` + ); + } + } + + // 4. Test getSpotMarketAccountsAndSlots (all spot markets) - sorted comparison + const allSpotMarkets1 = clientV1.accountSubscriber + .getSpotMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + const allSpotMarkets2 = clientV2.accountSubscriber + .getSpotMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + assert.strictEqual( + allSpotMarkets1.length, + allSpotMarkets2.length, + 'Number of spot markets should match' + ); + + // Compare each spot market in the sorted arrays + for (let i = 0; i < allSpotMarkets1.length; i++) { + const market1 = allSpotMarkets1[i]; + const market2 = allSpotMarkets2[i]; + assert.strictEqual( + market1.data.marketIndex, + market2.data.marketIndex, + `Spot market at position ${i} should have same marketIndex` + ); + // assert.deepStrictEqual( + // market1.data, + // market2.data, + // `Spot market ${market1.data.marketIndex} (from getSpotMarketAccountsAndSlots) should match` + // ); + } + + // 5. Test getSpotMarketAccountAndSlot for each spot market + for (const idx of spotMarketIndexes) { + const market1 = + clientV1.accountSubscriber.getSpotMarketAccountAndSlot(idx); + const market2 = + clientV2.accountSubscriber.getSpotMarketAccountAndSlot(idx); + // assert.deepStrictEqual( + // market1?.data, + // market2?.data, + // `Spot market ${idx} data should match` + // ); + // assert.strictEqual( + // market1?.slot, + // market2?.slot, + // `Spot market ${idx} slot should match` + // ); + if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot < market1.slot + ) { + console.error( + `Spot market ${idx} slot regression: v2 slot ${market2.slot} < v1 slot ${market1.slot}` + ); + } else if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot > market1.slot + ) { + console.info( + `Spot market ${idx} slot is FASTER! v2: ${market2.slot}, v1: ${market1.slot}` + ); + } + } + + // 6. Test getOraclePriceDataAndSlotForPerpMarket + for (const idx of perpMarketIndexes) { + const oracle1 = + clientV1.accountSubscriber.getOraclePriceDataAndSlotForPerpMarket( + idx + ); + const oracle2 = + clientV2.accountSubscriber.getOraclePriceDataAndSlotForPerpMarket( + idx + ); + // assert.deepStrictEqual( + // oracle1?.data, + // oracle2?.data, + // `Perp market ${idx} oracle data should match` + // ); + // Note: slots might differ slightly due to timing, so we can optionally skip this check or be lenient + // assert.strictEqual(oracle1?.slot, oracle2?.slot, `Perp market ${idx} oracle slot should match`); + if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot < oracle1.slot + ) { + console.error( + `Perp market ${idx} oracle slot regression: v2 slot ${oracle2.slot} < v1 slot ${oracle1.slot}` + ); + } else if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot > oracle1.slot + ) { + console.info( + `Perp market ${idx} oracle slot is FASTER! v2: ${oracle2.slot}, v1: ${oracle1.slot}` + ); + } + } + + // 7. Test getOraclePriceDataAndSlotForSpotMarket + for (const idx of spotMarketIndexes) { + const oracle1 = + clientV1.accountSubscriber.getOraclePriceDataAndSlotForSpotMarket( + idx + ); + const oracle2 = + clientV2.accountSubscriber.getOraclePriceDataAndSlotForSpotMarket( + idx + ); + // assert.deepStrictEqual( + // oracle1?.data, + // oracle2?.data, + // `Spot market ${idx} oracle data should match` + // ); + // Note: slots might differ slightly due to timing + // assert.strictEqual(oracle1?.slot, oracle2?.slot, `Spot market ${idx} oracle slot should match`); + if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot < oracle1.slot + ) { + console.error( + `Spot market ${idx} oracle slot regression: v2 slot ${oracle2.slot} < v1 slot ${oracle1.slot}` + ); + } else if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot > oracle1.slot + ) { + console.info( + `Spot market ${idx} oracle slot is FASTER! v2: ${oracle2.slot}, v1: ${oracle1.slot}` + ); + } + } + + console.log('✓ All comparisons passed'); + } catch (error) { + console.error('✗ Comparison failed:', error); } }; diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index 7e47b71fa9..fc79e06c6e 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -160,11 +160,14 @@ export class grpcMultiAccountSubscriber { const accountId = chunk[i]; const accountInfo = rpcResponse[i]; if (accountInfo) { - const perpMarket = this.program.coder.accounts.decode( - 'PerpMarket', - accountInfo.data - ); - this.setAccountData(accountId, perpMarket, currentSlot); + const existingData = this.getAccountData(accountId); + if (!existingData || currentSlot > existingData.slot) { + const accountDecoded = this.program.coder.accounts.decode( + this.capitalize(this.accountName), + accountInfo.data + ); + this.setAccountData(accountId, accountDecoded, currentSlot); + } } } }) @@ -238,6 +241,12 @@ export class grpcMultiAccountSubscriber { if (!accountPubkey || !this.subscribedAccounts.has(accountPubkey)) { return; } + + // Skip processing if we already have data for this account at an equal or newer slot + const existing = this.dataMap.get(accountPubkey); + if (existing?.slot !== undefined && existing.slot >= slot) { + return; + } const accountInfo: AccountInfoLike = { owner: new PublicKey(chunk.account.account.owner), lamports: Number(chunk.account.account.lamports), From 52a46a6404cae3d3f732ff2c5670feb138095dd5 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:24:53 +0000 Subject: [PATCH 115/247] sdk: release v2.142.0-beta.18 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 5958012b54..d5d16927b1 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.17 \ No newline at end of file +2.142.0-beta.18 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index fa96fe00cf..c61915d70c 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.17", + "version": "2.142.0-beta.18", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 9f12589e65c511a741b54feccc22c0bd22a0dab0 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Thu, 9 Oct 2025 13:02:45 -0600 Subject: [PATCH 116/247] fix: buffer equality check on multi grpc (#1949) --- .../accounts/grpcMultiAccountSubscriber.ts | 70 ++++++++++++++----- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index fc79e06c6e..2c31df6b17 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -11,7 +11,7 @@ import { SubscribeUpdate, createClient, } from '../isomorphic/grpc'; -import { DataAndSlot, GrpcConfigs, ResubOpts } from './types'; +import { BufferAndSlot, DataAndSlot, GrpcConfigs, ResubOpts } from './types'; interface AccountInfoLike { owner: PublicKey; @@ -63,6 +63,7 @@ export class grpcMultiAccountSubscriber { private dataMap = new Map>(); private accountPropsMap = new Map>(); + private bufferMap = new Map(); private constructor( client: Client, @@ -160,14 +161,29 @@ export class grpcMultiAccountSubscriber { const accountId = chunk[i]; const accountInfo = rpcResponse[i]; if (accountInfo) { - const existingData = this.getAccountData(accountId); - if (!existingData || currentSlot > existingData.slot) { - const accountDecoded = this.program.coder.accounts.decode( - this.capitalize(this.accountName), - accountInfo.data - ); - this.setAccountData(accountId, accountDecoded, currentSlot); + const prev = this.bufferMap.get(accountId); + const newBuffer = accountInfo.data as Buffer; + if (prev && currentSlot < prev.slot) { + continue; } + if ( + prev && + prev.buffer && + newBuffer && + newBuffer.equals(prev.buffer) + ) { + continue; + } + this.bufferMap.set(accountId, { + buffer: newBuffer, + slot: currentSlot, + }); + + const accountDecoded = this.program.coder.accounts.decode( + this.capitalize(this.accountName), + newBuffer + ); + this.setAccountData(accountId, accountDecoded, currentSlot); } } }) @@ -242,9 +258,16 @@ export class grpcMultiAccountSubscriber { return; } - // Skip processing if we already have data for this account at an equal or newer slot + // Touch resub timer on any incoming account update for subscribed keys + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + this.setTimeout(); + } + + // Skip processing if we already have data for this account at a newer slot const existing = this.dataMap.get(accountPubkey); - if (existing?.slot !== undefined && existing.slot >= slot) { + if (existing?.slot !== undefined && existing.slot > slot) { return; } const accountInfo: AccountInfoLike = { @@ -257,6 +280,21 @@ export class grpcMultiAccountSubscriber { const context = { slot } as Context; const buffer = accountInfo.data; + + // Check existing buffer for this account and skip if unchanged or slot regressed + const prevBuffer = this.bufferMap.get(accountPubkey); + if (prevBuffer && slot < prevBuffer.slot) { + return; + } + if ( + prevBuffer && + prevBuffer.buffer && + buffer && + buffer.equals(prevBuffer.buffer) + ) { + return; + } + this.bufferMap.set(accountPubkey, { buffer, slot }); const accountProps = this.accountPropsMap?.get(accountPubkey); const handleDataBuffer = ( @@ -272,14 +310,7 @@ export class grpcMultiAccountSubscriber { ); const handler = this.onChangeMap.get(accountPubkey); if (handler) { - if (this.resubOpts?.resubTimeoutMs) { - this.receivingData = true; - clearTimeout(this.timeoutId); - handler(data, context, buffer, accountProps); - this.setTimeout(); - } else { - handler(data, context, buffer, accountProps); - } + handler(data, context, buffer, accountProps); } }; @@ -298,7 +329,6 @@ export class grpcMultiAccountSubscriber { this.listenerId = 1; if (this.resubOpts?.resubTimeoutMs) { this.receivingData = true; - this.setTimeout(); } resolve(); } else { @@ -349,6 +379,8 @@ export class grpcMultiAccountSubscriber { const k = pk.toBase58(); this.subscribedAccounts.delete(k); this.onChangeMap.delete(k); + this.dataMap.delete(k); + this.bufferMap.delete(k); } const request: SubscribeRequest = { slots: {}, From 28ae85a3e4ae9790683e4dca2cff77226860b956 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:08:09 +0000 Subject: [PATCH 117/247] sdk: release v2.142.0-beta.19 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index d5d16927b1..ede15ae793 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.18 \ No newline at end of file +2.142.0-beta.19 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c61915d70c..58d7aedbb3 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.18", + "version": "2.142.0-beta.19", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From b58f53bc50da35633afd8a648cf0a1a43e70736f Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Fri, 10 Oct 2025 08:06:26 -0600 Subject: [PATCH 118/247] chore: dont delete accounts from datamap and buffer map grpcmulti (#1950) --- sdk/src/accounts/grpcMultiAccountSubscriber.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index 2c31df6b17..8bdd1cbccd 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -379,8 +379,6 @@ export class grpcMultiAccountSubscriber { const k = pk.toBase58(); this.subscribedAccounts.delete(k); this.onChangeMap.delete(k); - this.dataMap.delete(k); - this.bufferMap.delete(k); } const request: SubscribeRequest = { slots: {}, From c3a43e411def66c74d2bc0063bd8268e2037eb7b Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:12:46 +0000 Subject: [PATCH 119/247] sdk: release v2.142.0-beta.20 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index ede15ae793..1b701a59e8 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.19 \ No newline at end of file +2.142.0-beta.20 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 58d7aedbb3..177317e5be 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.19", + "version": "2.142.0-beta.20", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From a2d7fe6f159a8dc6da82564c0ab4e649b09c65db Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Mon, 13 Oct 2025 15:27:09 +0800 Subject: [PATCH 120/247] refactor(sdk): add authority overrides to place order ixs --- sdk/src/driftClient.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 0071bfb7d9..c5aa2ec1af 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -4943,7 +4943,10 @@ export class DriftClient { public async getPlaceOrdersIx( params: OptionalOrderParams[], - subAccountId?: number + subAccountId?: number, + overrides?: { + authority?: PublicKey; + } ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); @@ -4978,13 +4981,14 @@ export class DriftClient { } const formattedParams = params.map((item) => getOrderParams(item)); + const authority = overrides?.authority ?? this.wallet.publicKey; return await this.program.instruction.placeOrders(formattedParams, { accounts: { state: await this.getStatePublicKey(), user, userStats: this.getUserStatsAccountPublicKey(), - authority: this.wallet.publicKey, + authority, }, remainingAccounts, }); @@ -6688,7 +6692,10 @@ export class DriftClient { referrerInfo?: ReferrerInfo, successCondition?: PlaceAndTakeOrderSuccessCondition, auctionDurationPercentage?: number, - subAccountId?: number + subAccountId?: number, + overrides?: { + authority?: PublicKey; + } ): Promise { orderParams = getOrderParams(orderParams, { marketType: MarketType.PERP }); const userStatsPublicKey = await this.getUserStatsAccountPublicKey(); @@ -6756,6 +6763,8 @@ export class DriftClient { ((auctionDurationPercentage ?? 100) << 8) | (successCondition ?? 0); } + const authority = overrides?.authority ?? this.wallet.publicKey; + return await this.program.instruction.placeAndTakePerpOrder( orderParams, optionalParams, @@ -6764,7 +6773,7 @@ export class DriftClient { state: await this.getStatePublicKey(), user, userStats: userStatsPublicKey, - authority: this.wallet.publicKey, + authority, }, remainingAccounts, } From 93ba1e26d2cdbafc7901d9ac3bced6060f811428 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:17:46 +0000 Subject: [PATCH 121/247] sdk: release v2.142.0-beta.21 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 1b701a59e8..15d6fb7750 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.20 \ No newline at end of file +2.142.0-beta.21 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 177317e5be..72811681eb 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.20", + "version": "2.142.0-beta.21", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 57a530d388d6f7256b8553c2ac446780631dac78 Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Mon, 13 Oct 2025 18:58:55 +0800 Subject: [PATCH 122/247] refactor(sdk): add overrides for cancel orders --- sdk/src/driftClient.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index c5aa2ec1af..2c20c344a3 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -4761,11 +4761,19 @@ export class DriftClient { orderIds?: number[], txParams?: TxParams, subAccountId?: number, - user?: User + user?: User, + overrides?: { + authority?: PublicKey; + } ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( - await this.getCancelOrdersByIdsIx(orderIds, subAccountId, user), + await this.getCancelOrdersByIdsIx( + orderIds, + subAccountId, + user, + overrides + ), txParams ), [], @@ -4785,7 +4793,10 @@ export class DriftClient { public async getCancelOrdersByIdsIx( orderIds?: number[], subAccountId?: number, - user?: User + user?: User, + overrides?: { + authority?: PublicKey; + } ): Promise { const userAccountPubKey = user?.userAccountPublicKey ?? @@ -4798,11 +4809,13 @@ export class DriftClient { useMarketLastSlotCache: true, }); + const authority = overrides?.authority ?? this.wallet.publicKey; + return await this.program.instruction.cancelOrdersByIds(orderIds, { accounts: { state: await this.getStatePublicKey(), user: userAccountPubKey, - authority: this.wallet.publicKey, + authority, }, remainingAccounts, }); From 07d03daebc58d840ae524ee6d907aa54bdfe61cd Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:04:02 +0000 Subject: [PATCH 123/247] sdk: release v2.142.0-beta.22 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 15d6fb7750..4cdb4a14ae 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.21 \ No newline at end of file +2.142.0-beta.22 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 72811681eb..c04fa1fceb 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.21", + "version": "2.142.0-beta.22", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 09bbc4c792a699b13dbb4e1a4c75a259cf516561 Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:55:49 -0400 Subject: [PATCH 124/247] sdk: add market index 78 79 80 to constants (#1956) * sdk: add market index 78 79 80 * rm dup 77 * fix missing 78 * update 81 oracle src --------- Co-authored-by: moosecat <14929853+moosecat2@users.noreply.github.com> --- sdk/src/constants/perpMarkets.ts | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 1e9aa6e440..26fe5637dc 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1351,6 +1351,43 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ '0xf2b3ab1c49e35e881003c3c0482d18b181a1560b697b844c24c8f85aba1cab95', pythLazerId: 2316, }, + { + fullName: 'ZCash', + category: ['Privacy'], + symbol: 'ZEC-PERP', + baseAssetSymbol: 'ZEC', + marketIndex: 79, + oracle: new PublicKey('BXunfRSyiQWJHv88qMvE42mpMpksWEC8Bf13p2msnRms'), + launchTs: 1760366017000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0xbe9b59d178f0d6a97ab4c343bff2aa69caa1eaae3e9048a65788c529b125bb24', + pythLazerId: 66, + }, + { + fullName: 'Mantle', + category: ['L1'], + symbol: 'MNT-PERP', + baseAssetSymbol: 'MNT', + marketIndex: 80, + oracle: new PublicKey('Gy7cJ4U1nxMA44XXC3hwqkpcxEB1mZTYiwJVkaqZfU7u'), + launchTs: 1760366017000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0x4e3037c822d852d79af3ac80e35eb420ee3b870dca49f9344a38ef4773fb0585', + pythLazerId: 199, + }, + { + fullName: '1KPUMP', + category: ['Launchpad'], + symbol: '1KPUMP-PERP', + baseAssetSymbol: '1KPUMP', + marketIndex: 81, + oracle: new PublicKey('5r8RWTaRiMgr9Lph3FTUE3sGb1vymhpCrm83Bovjfcps'), + launchTs: 1760366017000, + oracleSource: OracleSource.PYTH_LAZER_1K, + pythLazerId: 1578, + }, ]; export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = { From 2c0271018a8bf2890d679a69a84a078f7ac03947 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:05:48 +0000 Subject: [PATCH 125/247] sdk: release v2.142.0-beta.23 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 4cdb4a14ae..679c1b8e99 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.22 \ No newline at end of file +2.142.0-beta.23 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c04fa1fceb..8ceff8b7b2 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.22", + "version": "2.142.0-beta.23", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 4e1f214378bccef9cd7c5acdf8201bacaeb08593 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 13 Oct 2025 21:25:33 -0700 Subject: [PATCH 126/247] sdk: add parseLogsForCuUsage (#1953) * sdk: add parseLogsForCuUsage * linter --- sdk/package.json | 1 + sdk/src/events/parse.ts | 115 ++++++++++++++++++++ sdk/src/events/types.ts | 24 +++- sdk/tests/events/parseLogsForCuUsage.ts | 139 ++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 sdk/tests/events/parseLogsForCuUsage.ts diff --git a/sdk/package.json b/sdk/package.json index 8ceff8b7b2..3b05a9ff70 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -20,6 +20,7 @@ "test:bignum": "mocha -r ts-node/register tests/bn/**/*.ts", "test:ci": "mocha -r ts-node/register tests/ci/**/*.ts", "test:dlob": "mocha -r ts-node/register tests/dlob/**/*.ts", + "test:events": "mocha -r ts-node/register tests/events/**/*.ts", "patch-and-pub": "npm version patch --force && npm publish", "prettify": "prettier --check './src/***/*.ts'", "prettify:fix": "prettier --write './{src,tests}/***/*.ts'", diff --git a/sdk/src/events/parse.ts b/sdk/src/events/parse.ts index 80287550da..96223134e3 100644 --- a/sdk/src/events/parse.ts +++ b/sdk/src/events/parse.ts @@ -1,10 +1,13 @@ import { Program, Event } from '@coral-xyz/anchor'; +import { CuUsageEvent } from './types'; const driftProgramId = 'dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH'; const PROGRAM_LOG = 'Program log: '; +const PROGRAM_INSTRUCTION = 'Program log: Instruction: '; const PROGRAM_DATA = 'Program data: '; const PROGRAM_LOG_START_INDEX = PROGRAM_LOG.length; const PROGRAM_DATA_START_INDEX = PROGRAM_DATA.length; +const PROGRAM_INSTRUCTION_START_INDEX = PROGRAM_INSTRUCTION.length; export function parseLogs( program: Program, @@ -112,6 +115,7 @@ function handleSystemLog( // executing for a given log. class ExecutionContext { stack: string[] = []; + ixStack: string[] = []; program(): string { if (!this.stack.length) { @@ -130,4 +134,115 @@ class ExecutionContext { } this.stack.pop(); } + + ix(): string { + if (!this.ixStack.length) { + throw new Error('Expected the ix stack to have elements'); + } + return this.ixStack[this.ixStack.length - 1]; + } + + pushIx(newIx: string) { + this.ixStack.push(newIx); + } + + popIx() { + this.ixStack.pop(); + } +} + +export function parseLogsForCuUsage( + logs: string[], + programId = driftProgramId +): Event[] { + const cuUsageEvents: Event[] = []; + + const execution = new ExecutionContext(); + for (const log of logs) { + if (log.startsWith('Log truncated')) { + break; + } + + const [newProgram, newIx, didPopProgram, didPopIx] = handleLogForCuUsage( + execution, + log, + programId + ); + if (newProgram) { + execution.push(newProgram); + } + if (newIx) { + execution.pushIx(newIx); + } + if (didPopProgram) { + execution.pop(); + } + if (didPopIx !== null) { + cuUsageEvents.push({ + name: 'CuUsage', + data: { + instruction: execution.ix(), + cuUsage: didPopIx!, + }, + } as any); + execution.popIx(); + } + } + return cuUsageEvents; +} + +function handleLogForCuUsage( + execution: ExecutionContext, + log: string, + programId = driftProgramId +): [string | null, string | null, boolean, number | null] { + if (execution.stack.length > 0 && execution.program() === programId) { + return handleProgramLogForCuUsage(log, programId); + } else { + return handleSystemLogForCuUsage(log, programId); + } +} + +function handleProgramLogForCuUsage( + log: string, + programId = driftProgramId +): [string | null, string | null, boolean, number | null] { + if (log.startsWith(PROGRAM_INSTRUCTION)) { + const ixStr = log.slice(PROGRAM_INSTRUCTION_START_INDEX); + return [null, ixStr, false, null]; + } else { + return handleSystemLogForCuUsage(log, programId); + } +} + +function handleSystemLogForCuUsage( + log: string, + programId = driftProgramId +): [string | null, string | null, boolean, number | null] { + // System component. + const logStart = log.split(':')[0]; + const programStart = `Program ${programId} invoke`; + + // Did the program finish executing? + if (logStart.match(/^Program (.*) success/g) !== null) { + return [null, null, true, null]; + // Recursive call. + } else if (logStart.startsWith(programStart)) { + return [programId, null, false, null]; + // Consumed CU log. + } else if (log.startsWith(`Program ${programId} consumed `)) { + // Extract CU usage, e.g. 'Program ... consumed 29242 of 199700 compute units' + // We need to extract the consumed value (29242) + const matches = log.match(/consumed (\d+) of \d+ compute units/); + if (matches) { + return [null, null, false, Number(matches[1])]; + } + return [null, null, false, null]; + } + // CPI call. + else if (logStart.includes('invoke')) { + return ['cpi', null, false, null]; // Any string will do. + } else { + return [null, null, false, null]; + } } diff --git a/sdk/src/events/types.ts b/sdk/src/events/types.ts index 4614b7e753..69ea7e078d 100644 --- a/sdk/src/events/types.ts +++ b/sdk/src/events/types.ts @@ -149,7 +149,8 @@ export type DriftEvent = | Event | Event | Event - | Event; + | Event + | Event; export interface EventSubscriberEvents { newEvent: (event: WrappedEvent) => void; @@ -213,3 +214,24 @@ export type LogProviderConfig = | WebSocketLogProviderConfig | PollingLogProviderConfig | EventsServerLogProviderConfig; + +export type CuUsageEvent = { + name: 'CuUsage'; + fields: [ + { + name: 'instruction'; + type: 'string'; + index: false; + }, + { + name: 'cuUsage'; + type: 'u32'; + index: false; + }, + ]; +}; + +export type CuUsage = { + instruction: string; + cuUsage: number; +}; diff --git a/sdk/tests/events/parseLogsForCuUsage.ts b/sdk/tests/events/parseLogsForCuUsage.ts new file mode 100644 index 0000000000..3b49f28c5f --- /dev/null +++ b/sdk/tests/events/parseLogsForCuUsage.ts @@ -0,0 +1,139 @@ +import { expect } from 'chai'; +import { parseLogsForCuUsage } from '../../src/events/parse'; + +// if you used the '@types/mocha' method to install mocha type definitions, uncomment the following line +// import 'mocha'; + +describe('parseLogsForCuUsage Tests', () => { + it('can parse single ix', () => { + const logs = [ + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: UpdateFundingRate', + 'Program log: correcting mark twap update (oracle previously invalid for 1625 seconds)', + 'Program data: Ze4o5EYuPXWjSrNoAAAAADkUAAAAAAAAZTIKAAAAAAAAAAAAAAAAAGwIAGYvdqgAAAAAAAAAAAAxpQahVMKdAAAAAAAAAAAAK0cfnMcFowAAAAAAAAAAAGUyCgAAAAAAAAAAAAAAAAAbXpy4n6WoAAAAAAAAAAAAGAAXbsHunQAAAAAAAAAAAObum9evM6MAAAAAAAAAAAAA9PA5+UYKAAAAAAAAAAAAAKhuDZ0RCgAAAAAAAAAAAABMgixcNQAAAAAAAAAAAAA9NqYKSgAAAAAAAAAAAAAA4nylcy0AAAAAAAAAAAAAAMvCAAAAAAAAAAAAAAAAAACOjAkAAAAAAET3AgAAAAAAAAAAAAAAAAAMAQAAHgA=', + 'Program data: RAP/GoVbk/6jSrNoAAAAAA0rAAAAAAAAHgBxcgEAAAAAAHFyAQAAAAAAAAAAAAAAAABxcgEAAAAAAAAAAAAAAAAAZRuYAQAAAAAAAAAAAAAAAJMNmAEAAAAAAAAAAAAAAAC0eAkAAAAAAByBCQAAAAAAWUbv4v////8ATIIsXDUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 102636 of 143817 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + ]; + const cuUsage = parseLogsForCuUsage(logs); + expect(cuUsage).to.deep.equal([ + { + name: 'CuUsage', + data: { + instruction: 'UpdateFundingRate', + cuUsage: 102636, + } + }, + ]); + }); + + it('can parse multiple ixs', () => { + const logs = [ + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: PostPythLazerOracleUpdate', + 'Program log: Skipping new lazer update. current ts 1756622092550000 >= next ts 1756622092000000', + 'Program log: Skipping new lazer update. current ts 1756622092550000 >= next ts 1756622092000000', + 'Program log: Skipping new lazer update. current ts 1756622092550000 >= next ts 1756622092000000', + 'Program log: Price updated to 433158894', + 'Program log: Posting new lazer update. current ts 1756622079000000 < next ts 1756622092000000', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 29242 of 199700 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: UpdatePerpBidAskTwap', + 'Program log: estimated_bid = None estimated_ask = None', + 'Program log: after amm bid twap = 204332308 -> 204328128 \n ask twap = 204350474 -> 204347149 \n ts = 1756622080 -> 1756622092', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 71006 of 170458 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + ]; + const cuUsage = parseLogsForCuUsage(logs); + expect(cuUsage).to.deep.equal([ + { + name: 'CuUsage', + data: { + instruction: 'PostPythLazerOracleUpdate', + cuUsage: 29242, + } + }, + { + name: 'CuUsage', + data: { + instruction: 'UpdatePerpBidAskTwap', + cuUsage: 71006, + } + }, + ]); + }); + + it('can parse ixs with CPI (swaps)', () => { + const logs = [ + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: BeginSwap', + 'Program data: t7rLuuG7X4K7X+loAAAAAAAAoDz72UK0JwQAAAAAAAAAAMS7r8ACAAAAAAAAAAAAAAB36Aiv26cZAwAAAAAAAAAArDDOLAMAAAAAAAAAAAAAAAA1DAAUzQAAoLsNAA==', + 'Program data: t7rLuuG7X4K7X+loAAAAAAEASQcRhBUVAQAAAAAAAAAAAG3WGn8CAAAAAAAAAAAAAADBBakRTIoAAAAAAAAAAAAAFfFDwgIAAAAAAAAAAAAAAAA1DACghgEAYOMWAA==', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]', + 'Program log: Instruction: Transfer', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 1336324 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 79071 of 1399700 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [1]', + 'Program log: Instruction: Route', + 'Program SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF invoke [2]', + 'Program data: S3VCwUhV8CXSyrcV3EtPUNCsQJvXpBqCGUobEJZVRnl5bVAAAAAAAFUFNBYAAAAAAAAAAAAAAAAAAAAAAAAAAA==', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]', + 'Program log: Instruction: Transfer', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 1255262 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]', + 'Program log: Instruction: Transfer', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 1249195 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF consumed 69257 of 1311915 compute units', + 'Program SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF success', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [2]', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 199 of 1241147 compute units', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 81059 of 1320629 compute units', + 'Program return: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 Z/tXxXEAAAA=', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: EndSwap', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]', + 'Program log: Instruction: Transfer', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 1187840 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program data: ort7woo4+vG7X+loAAAAAJ1Bg8Gp9WhWrw9VRm1UiC0KW6LRC2am2mjhfd3lzm6WZ/tXxXEAAAAA6HZIFwAAAAEAAAAJdDEMAAAAANFBDwAAAAAAAAAAAAAAAAA=', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 156076 of 1239570 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + ]; + const cuUsage = parseLogsForCuUsage(logs); + expect(cuUsage).to.deep.equal([ + { + name: 'CuUsage', + data: { + instruction: 'BeginSwap', + cuUsage: 79071, + } + }, + { + name: 'CuUsage', + data: { + instruction: 'EndSwap', + cuUsage: 156076, + } + }, + ]); + }); +}); From 40e2455e889a6cc723e06ff4a032d60fe5ca544d Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 04:31:00 +0000 Subject: [PATCH 127/247] sdk: release v2.142.0-beta.24 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 679c1b8e99..a89e07ad71 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.23 \ No newline at end of file +2.142.0-beta.24 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 3b05a9ff70..302aeddb8d 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.23", + "version": "2.142.0-beta.24", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From a541e872689a7cb924141cd695ef105fd197a257 Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Tue, 14 Oct 2025 16:06:40 +0800 Subject: [PATCH 128/247] refactor(sdk): allow authority override for getModifyOrderIx --- sdk/src/driftClient.ts | 22 ++++++++++++++++------ sdk/tests/events/parseLogsForCuUsage.ts | 10 +++++----- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 2c20c344a3..0a86382d5b 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -7649,13 +7649,19 @@ export class DriftClient { policy?: number; }, subAccountId?: number, - userPublicKey?: PublicKey + overrides?: { + user?: User; + authority?: PublicKey; + } ): Promise { - const user = - userPublicKey ?? (await this.getUserAccountPublicKey(subAccountId)); + const userPubKey = + overrides?.user?.getUserAccountPublicKey() ?? + (await this.getUserAccountPublicKey(subAccountId)); + const userAccount = + overrides?.user?.getUserAccount() ?? this.getUserAccount(subAccountId); const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [this.getUserAccount(subAccountId)], + userAccounts: [userAccount], useMarketLastSlotCache: true, }); @@ -7676,12 +7682,16 @@ export class DriftClient { maxTs: maxTs || null, }; + const authority = + overrides?.authority ?? + overrides?.user?.getUserAccount().authority ?? + this.wallet.publicKey; return await this.program.instruction.modifyOrder(orderId, orderParams, { accounts: { state: await this.getStatePublicKey(), - user, + user: userPubKey, userStats: this.getUserStatsAccountPublicKey(), - authority: this.wallet.publicKey, + authority, }, remainingAccounts, }); diff --git a/sdk/tests/events/parseLogsForCuUsage.ts b/sdk/tests/events/parseLogsForCuUsage.ts index 3b49f28c5f..5308664e84 100644 --- a/sdk/tests/events/parseLogsForCuUsage.ts +++ b/sdk/tests/events/parseLogsForCuUsage.ts @@ -26,7 +26,7 @@ describe('parseLogsForCuUsage Tests', () => { data: { instruction: 'UpdateFundingRate', cuUsage: 102636, - } + }, }, ]); }); @@ -60,14 +60,14 @@ describe('parseLogsForCuUsage Tests', () => { data: { instruction: 'PostPythLazerOracleUpdate', cuUsage: 29242, - } + }, }, { name: 'CuUsage', data: { instruction: 'UpdatePerpBidAskTwap', cuUsage: 71006, - } + }, }, ]); }); @@ -125,14 +125,14 @@ describe('parseLogsForCuUsage Tests', () => { data: { instruction: 'BeginSwap', cuUsage: 79071, - } + }, }, { name: 'CuUsage', data: { instruction: 'EndSwap', cuUsage: 156076, - } + }, }, ]); }); From f530ca9f5fffff5786f41eb4fd2ba1b5a3d13df4 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 08:12:17 +0000 Subject: [PATCH 129/247] sdk: release v2.142.0-beta.25 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index a89e07ad71..f0145dcf5b 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.24 \ No newline at end of file +2.142.0-beta.25 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 302aeddb8d..e66287af39 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.24", + "version": "2.142.0-beta.25", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 758dab40d97e0f08674321eea09d0c83b34777fd Mon Sep 17 00:00:00 2001 From: wphan Date: Tue, 14 Oct 2025 07:58:13 -0700 Subject: [PATCH 130/247] program: add titan to whitelisted swap programs (#1952) * program: add titan to whitelisted swap programs * update CHANGELOG.md * cargo fmt --- CHANGELOG.md | 2 ++ programs/drift/src/ids.rs | 5 +++++ programs/drift/src/instructions/keeper.rs | 9 ++++++--- programs/drift/src/instructions/user.rs | 8 ++++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6231c7a14c..5bd53f4c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: add titan to whitelisted swap programs ([#1952](https://github.com/drift-labs/protocol-v2/pull/1952)) + ### Fixes ### Breaking diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index 2e65c5de29..e3dce8aa73 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -112,3 +112,8 @@ pub mod dflow_mainnet_aggregator_4 { use solana_program::declare_id; declare_id!("DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"); } + +pub mod titan_mainnet_argos_v1 { + use solana_program::declare_id; + declare_id!("T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT"); +} diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index cc8c87b321..0fc1fc3850 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -21,8 +21,10 @@ use crate::controller::spot_balance::update_spot_balances; use crate::controller::token::{receive, send_from_program_vault}; use crate::error::ErrorCode; use crate::ids::admin_hot_wallet; -use crate::ids::dflow_mainnet_aggregator_4; -use crate::ids::{jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, serum_program}; +use crate::ids::{ + dflow_mainnet_aggregator_4, jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, + serum_program, titan_mainnet_argos_v1, +}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; @@ -1719,11 +1721,12 @@ pub fn handle_liquidate_spot_with_swap_begin<'c: 'info, 'info>( jupiter_mainnet_4::ID, jupiter_mainnet_6::ID, dflow_mainnet_aggregator_4::ID, + titan_mainnet_argos_v1::ID, ]; validate!( whitelisted_programs.contains(&ix.program_id), ErrorCode::InvalidLiquidateSpotWithSwap, - "only allowed to pass in ixs to token, openbook, and Jupiter v3/v4/v6 programs" + "only allowed to pass in ixs to ATA, openbook, Jupiter v3/v4/v6, dflow, or titan programs" )?; for meta in ix.accounts.iter() { diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index db9b9f4268..3cb9d8e41a 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -22,10 +22,9 @@ use crate::controller::spot_position::{ }; use crate::error::ErrorCode; use crate::ids::admin_hot_wallet; -use crate::ids::dflow_mainnet_aggregator_4; use crate::ids::{ - jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, lighthouse, marinade_mainnet, - serum_program, + dflow_mainnet_aggregator_4, jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, + lighthouse, marinade_mainnet, serum_program, titan_mainnet_argos_v1, }; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; @@ -3686,6 +3685,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( jupiter_mainnet_4::ID, jupiter_mainnet_6::ID, dflow_mainnet_aggregator_4::ID, + titan_mainnet_argos_v1::ID, ]; if !delegate_is_signer { whitelisted_programs.push(Token::id()); @@ -3695,7 +3695,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( validate!( whitelisted_programs.contains(&ix.program_id), ErrorCode::InvalidSwap, - "only allowed to pass in ixs to token, openbook, and Jupiter v3/v4/v6 programs" + "only allowed to pass in ixs to ATA, openbook, Jupiter v3/v4/v6, dflow, or titan programs" )?; for meta in ix.accounts.iter() { From e1bde42ac567abe7bece46526e3ffc3245741369 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 25 Aug 2025 11:11:09 -0600 Subject: [PATCH 131/247] feat: initial SDK Changes for iso pos --- sdk/src/margin/README.md | 143 ++++++++++ sdk/src/user.ts | 602 +++++++++++++++++++++++++++++++++++---- 2 files changed, 685 insertions(+), 60 deletions(-) create mode 100644 sdk/src/margin/README.md diff --git a/sdk/src/margin/README.md b/sdk/src/margin/README.md new file mode 100644 index 0000000000..b074e96ff2 --- /dev/null +++ b/sdk/src/margin/README.md @@ -0,0 +1,143 @@ +## Margin Calculation Snapshot (SDK) + +This document describes the single-source-of-truth margin engine in the SDK that mirrors the on-chain `MarginCalculation` and related semantics. The goal is to compute an immutable snapshot in one pass and have existing `User` getters delegate to it, eliminating duplicative work across getters and UI hooks while maintaining parity with the program. + +### Alignment with on-chain + +- The SDK snapshot shape mirrors `programs/drift/src/state/margin_calculation.rs` field-for-field. +- The inputs and ordering mirror `calculate_margin_requirement_and_total_collateral_and_liability_info` in `programs/drift/src/math/margin.rs`. +- Isolated positions are represented as `isolated_margin_calculations` keyed by perp `market_index`, matching program logic. + +### Core SDK types (shape parity) + +```ts +// Types reflect on-chain names and numeric signs +export type MarginRequirementType = 'Initial' | 'Fill' | 'Maintenance'; +export type MarketType = 'Spot' | 'Perp'; + +export type MarketIdentifier = { + marketType: MarketType; + marketIndex: number; // u16 +}; + +export type MarginCalculationMode = + | { kind: 'Standard' } + | { kind: 'Liquidation'; marketToTrackMarginRequirement?: MarketIdentifier }; + +export type MarginContext = { + marginType: MarginRequirementType; + mode: MarginCalculationMode; + strict: boolean; + ignoreInvalidDepositOracles: boolean; + marginBuffer: BN; // u128 + fuelBonusNumerator: number; // i64 + fuelBonus: number; // u64 + fuelPerpDelta?: { marketIndex: number; delta: BN }; // (u16, i64) + fuelSpotDeltas: Array<{ marketIndex: number; delta: BN }>; // up to 2 entries + marginRatioOverride?: number; // u32 +}; + +export type IsolatedMarginCalculation = { + marginRequirement: BN; // u128 + totalCollateral: BN; // i128 + totalCollateralBuffer: BN; // i128 + marginRequirementPlusBuffer: BN; // u128 +}; + +export type MarginCalculation = { + context: MarginContext; + + totalCollateral: BN; // i128 + totalCollateralBuffer: BN; // i128 + marginRequirement: BN; // u128 + marginRequirementPlusBuffer: BN; // u128 + + isolatedMarginCalculations: Map; // BTreeMap + + numSpotLiabilities: number; // u8 + numPerpLiabilities: number; // u8 + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; + + totalSpotAssetValue: BN; // i128 + totalSpotLiabilityValue: BN; // u128 + totalPerpLiabilityValue: BN; // u128 + totalPerpPnl: BN; // i128 + + trackedMarketMarginRequirement: BN; // u128 + fuelDeposits: number; // u32 + fuelBorrows: number; // u32 + fuelPositions: number; // u32 +}; +``` + +### Engine API + +```ts +// Pure computation, no I/O; uses data already cached in the client/subscribers +export function computeMarginCalculation(user: User, context: MarginContext): MarginCalculation; + +// Helpers that mirror on-chain semantics +export function meets_margin_requirement(calc: MarginCalculation): boolean; +export function meets_margin_requirement_with_buffer(calc: MarginCalculation): boolean; +export function get_cross_free_collateral(calc: MarginCalculation): BN; +export function get_isolated_free_collateral(calc: MarginCalculation, marketIndex: number): BN; +export function cross_margin_shortage(calc: MarginCalculation): BN; // requires buffer mode +export function isolated_margin_shortage(calc: MarginCalculation, marketIndex: number): BN; // requires buffer mode +``` + +### Computation model (on-demand) + +- The SDK computes the snapshot on-demand when `getMarginCalculation(...)` is called. +- No event-driven recomputation by default (oracle prices can change every slot; recomputing every update would be wasteful). +- Callers (UI/bots) decide polling frequency (e.g., UI can refresh every ~1s on active trade forms). + +### User integration + +- Add `user.getMarginCalculation(margin_type = 'Initial', overrides?: Partial)`. +- Existing getters delegate to the snapshot to avoid duplicate work: + - `getTotalCollateral()` → `snapshot.total_collateral` + - `getMarginRequirement(mode)` → `snapshot.margin_requirement` + - `getFreeCollateral()` → `get_cross_free_collateral(snapshot)` + - Per-market isolated FC → `get_isolated_free_collateral(snapshot, marketIndex)` + +Suggested `User` API surface (non-breaking): + +```ts +// Primary entrypoint +getMarginCalculation( + marginType: 'Initial' | 'Maintenance' | 'Fill' = 'Initial', + contextOverrides?: Partial +): MarginCalculation; + +// Optional conveniences for consumers +getIsolatedMarginCalculation( + marketIndex: number, + marginType: 'Initial' | 'Maintenance' | 'Fill' = 'Initial', + contextOverrides?: Partial +): IsolatedMarginCalculation | undefined; + +// Cross views can continue to use helpers on the snapshot: +// get_cross_free_collateral(snapshot), meets_margin_requirement(snapshot), etc. +``` + +### UI compatibility + +- All existing `User` getters remain and delegate to the snapshot, so current UI keeps working without call-site changes. +- New consumers can call `user.getMarginCalculation()` to access isolated breakdowns. + +### Testing and parity + +- Golden tests comparing SDK snapshot against program outputs (cross and isolated, edge cases). +- Keep math/rounding identical to program (ordering, buffers, funding, open-order IM, oracle strictness). + +### Migration plan (brief) + +1. Implement `types` and `engine` with strict parity; land behind a feature flag. +2. Add `user.getMarginCalculation()` and delegate legacy getters. +3. Optionally update UI hooks to read richer fields; not required for compatibility. +4. Expand parity tests; enable by default after validation. + + diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 7b78495e21..cbcc2ccf47 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -107,6 +107,32 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice'; import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber'; +// type for account-level margin calculation results. +// Mirrors key fields from on-chain MarginCalculation; can be extended as needed. +export type IsolatedMarginCalculation = { + marginRequirement: BN; + totalCollateral: BN; + totalCollateralBuffer: BN; + marginRequirementPlusBuffer: BN; +}; + +export type UserMarginCalculation = { + context: { marginType: MarginCategory; strict: boolean; marginBuffer?: BN }; + totalCollateral: BN; + totalCollateralBuffer: BN; + marginRequirement: BN; + marginRequirementPlusBuffer: BN; + isolatedMarginCalculations: Map; + numSpotLiabilities: number; + numPerpLiabilities: number; + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; +}; + +export type MarginType = 'Cross' | 'Isolated'; + export class User { driftClient: DriftClient; userAccountPublicKey: PublicKey; @@ -118,6 +144,313 @@ export class User { return this._isSubscribed && this.accountSubscriber.isSubscribed; } + /** + * Compute a consolidated margin snapshot once, without caching. + * Consumers can use this to avoid duplicating work across separate calls. + */ + // TODO: verify this truly matches on-chain logic well + public getMarginCalculation( + marginCategory: MarginCategory = 'Initial', + opts?: { + strict?: boolean; // mirror StrictOraclePrice application + includeOpenOrders?: boolean; + enteringHighLeverage?: boolean; + liquidationBuffer?: BN; // margin_buffer analog for buffer mode + marginRatioOverride?: number; // mirrors context.margin_ratio_override + } + ): UserMarginCalculation { + const strict = opts?.strict ?? false; + const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? + const enteringHighLeverage = opts?.enteringHighLeverage ?? false; + const marginBuffer = opts?.liquidationBuffer; // treat as MARGIN_BUFFER ratio if provided + const marginRatioOverride = opts?.marginRatioOverride; + + // Equivalent to on-chain user_custom_margin_ratio + let userCustomMarginRatio = + marginCategory === 'Initial' ? this.getUserAccount().maxMarginRatio : 0; + if (marginRatioOverride !== undefined) { + userCustomMarginRatio = Math.max( + userCustomMarginRatio, + marginRatioOverride + ); + } + + // Initialize calculation (mirrors MarginCalculation::new) + let totalCollateral = ZERO; + let totalCollateralBuffer = ZERO; + let marginRequirement = ZERO; + let marginRequirementPlusBuffer = ZERO; + const isolatedMarginCalculations: Map = + new Map(); + let numSpotLiabilities = 0; + let numPerpLiabilities = 0; + let allDepositOraclesValid = true; + let allLiabilityOraclesValid = true; + let withPerpIsolatedLiability = false; + let withSpotIsolatedLiability = false; + + // SPOT POSITIONS + for (const spotPosition of this.getUserAccount().spotPositions) { + if (isSpotPositionAvailable(spotPosition)) continue; + + const spotMarket = this.driftClient.getSpotMarketAccount( + spotPosition.marketIndex + ); + const oraclePriceData = this.getOracleDataForSpotMarket( + spotPosition.marketIndex + ); + const twap5 = strict + ? calculateLiveOracleTwap( + spotMarket.historicalOracleData, + oraclePriceData, + new BN(Math.floor(Date.now() / 1000)), + FIVE_MINUTE + ) + : undefined; + const strictOracle = new StrictOraclePrice(oraclePriceData.price, twap5); + + if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX) { + const tokenAmount = getSignedTokenAmount( + getTokenAmount( + spotPosition.scaledBalance, + spotMarket, + spotPosition.balanceType + ), + spotPosition.balanceType + ); + if (isVariant(spotPosition.balanceType, 'deposit')) { + // add deposit value to total collateral + const tokenValue = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ); + totalCollateral = totalCollateral.add(tokenValue); + // deposit oracle validity only affects flags; keep it true by default + } else { + // borrow on quote contributes to margin requirement + const tokenValueAbs = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ).abs(); + marginRequirement = marginRequirement.add(tokenValueAbs); + if (marginBuffer) { + marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( + tokenValueAbs.mul(marginBuffer).div(MARGIN_PRECISION) + ); + } + numSpotLiabilities += 1; + } + continue; + } + + // Non-quote spot: worst-case simulation + const { + tokenAmount: worstCaseTokenAmount, + ordersValue: worstCaseOrdersValue, + tokenValue: worstCaseTokenValue, + weightedTokenValue: worstCaseWeightedTokenValue, + } = getWorstCaseTokenAmounts( + spotPosition, + spotMarket, + strictOracle, + marginCategory, + userCustomMarginRatio + ); + + // open order IM + marginRequirement = marginRequirement.add( + new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) + ); + + if (worstCaseTokenAmount.gt(ZERO)) { + // asset side increases total collateral (weighted) + totalCollateral = totalCollateral.add(worstCaseWeightedTokenValue); + } else if (worstCaseTokenAmount.lt(ZERO)) { + // liability side increases margin requirement (weighted >= abs(token_value)) + const liabilityWeighted = worstCaseWeightedTokenValue.abs(); + const liabilityBase = worstCaseTokenValue.abs(); + marginRequirement = marginRequirement.add(liabilityWeighted); + if (marginBuffer) { + marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( + liabilityBase.mul(marginBuffer).div(MARGIN_PRECISION) + ); + } + numSpotLiabilities += 1; + // flag isolated tier if applicable (approx: isolated asset tier → not available here) + } else if (spotPosition.openOrders !== 0) { + numSpotLiabilities += 1; + } + + // orders value contributes to collateral or requirement + if (worstCaseOrdersValue.gt(ZERO)) { + totalCollateral = totalCollateral.add(worstCaseOrdersValue); + } else if (worstCaseOrdersValue.lt(ZERO)) { + const absVal = worstCaseOrdersValue.abs(); + marginRequirement = marginRequirement.add(absVal); + if (marginBuffer) { + marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( + absVal.mul(marginBuffer).div(MARGIN_PRECISION) + ); + } + } + } + + // PERP POSITIONS + for (const marketPosition of this.getActivePerpPositions()) { + const market = this.driftClient.getPerpMarketAccount( + marketPosition.marketIndex + ); + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const oraclePriceData = this.getOracleDataForPerpMarket( + market.marketIndex + ); + + // Worst-case perp liability and weighted pnl + const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = + calculateWorstCasePerpLiabilityValue( + marketPosition, + market, + oraclePriceData.price + ); + + // margin ratio for this perp + let marginRatio = new BN( + calculateMarketMarginRatio( + market, + worstCaseBaseAssetAmount.abs(), + marginCategory, + this.getUserAccount().maxMarginRatio, + this.isHighLeverageMode() || enteringHighLeverage + ) + ); + if (isVariant(market.status, 'settlement')) { + marginRatio = ZERO; + } + + // convert liability to quote value and apply margin ratio + const quotePrice = strict + ? BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ) + : quoteOraclePriceData.price; + let perpMarginRequirement = worstCaseLiabilityValue + .mul(quotePrice) + .div(PRICE_PRECISION) + .mul(marginRatio) + .div(MARGIN_PRECISION); + // add open orders IM + perpMarginRequirement = perpMarginRequirement.add( + new BN(marketPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) + ); + + // weighted unrealized pnl + let positionUnrealizedPnl = calculatePositionPNL( + market, + marketPosition, + true, + oraclePriceData + ); + let pnlQuotePrice: BN; + if (strict && positionUnrealizedPnl.gt(ZERO)) { + pnlQuotePrice = BN.min( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); + } else if (strict && positionUnrealizedPnl.lt(ZERO)) { + pnlQuotePrice = BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); + } else { + pnlQuotePrice = quoteOraclePriceData.price; + } + positionUnrealizedPnl = positionUnrealizedPnl + .mul(pnlQuotePrice) + .div(PRICE_PRECISION); + + // Add perp contribution: isolated vs cross + const isIsolated = false; // TODO: wire to marketPosition.is_isolated when available in TS types + if (isIsolated) { + const existing = isolatedMarginCalculations.get(market.marketIndex) || { + marginRequirement: ZERO, + totalCollateral: ZERO, + totalCollateralBuffer: ZERO, + marginRequirementPlusBuffer: ZERO, + }; + existing.marginRequirement = existing.marginRequirement.add( + perpMarginRequirement + ); + existing.totalCollateral = existing.totalCollateral.add( + positionUnrealizedPnl + ); + if (marginBuffer) { + existing.totalCollateralBuffer = existing.totalCollateralBuffer.add( + positionUnrealizedPnl.isNeg() + ? positionUnrealizedPnl.mul(marginBuffer).div(MARGIN_PRECISION) + : ZERO + ); + existing.marginRequirementPlusBuffer = + existing.marginRequirementPlusBuffer.add( + perpMarginRequirement.add( + worstCaseLiabilityValue.mul(marginBuffer).div(MARGIN_PRECISION) + ) + ); + } + isolatedMarginCalculations.set(market.marketIndex, existing); + numPerpLiabilities += 1; + withPerpIsolatedLiability = withPerpIsolatedLiability || false; // TODO: derive from market tier + } else { + // cross: add to global requirement and collateral + marginRequirement = marginRequirement.add(perpMarginRequirement); + totalCollateral = totalCollateral.add(positionUnrealizedPnl); + numPerpLiabilities += + marketPosition.baseAssetAmount.eq(ZERO) && + marketPosition.openOrders === 0 + ? 0 + : 1; + if (marginBuffer) { + marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( + perpMarginRequirement.add( + worstCaseLiabilityValue.mul(marginBuffer).div(MARGIN_PRECISION) + ) + ); + if (positionUnrealizedPnl.isNeg()) { + totalCollateralBuffer = totalCollateralBuffer.add( + positionUnrealizedPnl.mul(marginBuffer).div(MARGIN_PRECISION) + ); + } + } + } + } + + return { + context: { + marginType: marginCategory, + strict, + marginBuffer: marginBuffer, + }, + totalCollateral, + totalCollateralBuffer, + marginRequirement, + marginRequirementPlusBuffer, + isolatedMarginCalculations, + numSpotLiabilities, + numPerpLiabilities, + allDepositOraclesValid, + allLiabilityOraclesValid, + withPerpIsolatedLiability, + withSpotIsolatedLiability, + }; + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -521,62 +854,128 @@ export class User { */ public getFreeCollateral( marginCategory: MarginCategory = 'Initial', - enterHighLeverageMode = undefined + enterHighLeverageMode = false, + perpMarketIndex?: number ): BN { const totalCollateral = this.getTotalCollateral(marginCategory, true); - const marginRequirement = - marginCategory === 'Initial' - ? this.getInitialMarginRequirement(enterHighLeverageMode) - : this.getMaintenanceMarginRequirement(); + const marginRequirement = this.getMarginRequirement( + marginCategory, + undefined, + true, + true, // includeOpenOrders default + enterHighLeverageMode, + perpMarketIndex ? 'Isolated' : undefined, + perpMarketIndex + ); const freeCollateral = totalCollateral.sub(marginRequirement); return freeCollateral.gte(ZERO) ? freeCollateral : ZERO; } /** - * @returns The margin requirement of a certain type (Initial or Maintenance) in USDC. : QUOTE_PRECISION + * @deprecated Use the overload that includes { marginType, perpMarketIndex } */ public getMarginRequirement( marginCategory: MarginCategory, liquidationBuffer?: BN, - strict = false, - includeOpenOrders = true, - enteringHighLeverage = undefined + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean + ): BN; + + /** + * Calculates the margin requirement based on the specified parameters. + * + * @param marginCategory - The category of margin to calculate ('Initial' or 'Maintenance'). + * @param liquidationBuffer - Optional buffer amount to consider during liquidation scenarios. + * @param strict - Optional flag to enforce strict margin calculations. + * @param includeOpenOrders - Optional flag to include open orders in the margin calculation. + * @param enteringHighLeverage - Optional flag indicating if the user is entering high leverage mode. + * @param marginType - Optional type of margin ('Cross' or 'Isolated'). If 'Isolated', perpMarketIndex must be provided. + * @param perpMarketIndex - Optional index of the perpetual market. Required if marginType is 'Isolated'. + * + * @returns The calculated margin requirement as a BN (BigNumber). + */ + public getMarginRequirement( + marginCategory: MarginCategory, + liquidationBuffer?: BN, + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean, + marginType?: MarginType, + perpMarketIndex?: number + ): BN; + + public getMarginRequirement( + marginCategory: MarginCategory, + liquidationBuffer?: BN, + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean, + marginType?: MarginType, + perpMarketIndex?: number ): BN { - return this.getTotalPerpPositionLiability( - marginCategory, - liquidationBuffer, - includeOpenOrders, + const marginCalc = this.getMarginCalculation(marginCategory, { strict, - enteringHighLeverage - ).add( - this.getSpotMarketLiabilityValue( - undefined, - marginCategory, - liquidationBuffer, - includeOpenOrders, - strict - ) - ); + includeOpenOrders, + enteringHighLeverage, + liquidationBuffer, + }); + + // If marginType is provided and is Isolated, compute only for that market index + if (marginType === 'Isolated') { + if (perpMarketIndex === undefined) { + throw new Error( + 'perpMarketIndex is required when marginType = Isolated' + ); + } + const isolatedMarginCalculation = + marginCalc.isolatedMarginCalculations.get(perpMarketIndex); + const { marginRequirement } = isolatedMarginCalculation; + + return marginRequirement; + } + + // Default: Cross margin requirement + // TODO: should we be using plus buffer sometimes? + return marginCalc.marginRequirement; } /** * @returns The initial margin requirement in USDC. : QUOTE_PRECISION */ - public getInitialMarginRequirement(enterHighLeverageMode = undefined): BN { + public getInitialMarginRequirement( + enterHighLeverageMode = false, + marginType?: MarginType, + perpMarketIndex?: number + ): BN { return this.getMarginRequirement( 'Initial', undefined, true, undefined, - enterHighLeverageMode + enterHighLeverageMode, + marginType, + perpMarketIndex ); } /** * @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION */ - public getMaintenanceMarginRequirement(liquidationBuffer?: BN): BN { - return this.getMarginRequirement('Maintenance', liquidationBuffer); + public getMaintenanceMarginRequirement( + liquidationBuffer?: BN, + marginType?: MarginType, + perpMarketIndex?: number + ): BN { + return this.getMarginRequirement( + 'Maintenance', + liquidationBuffer, + true, // strict default + true, // includeOpenOrders default + false, // enteringHighLeverage default + marginType, + perpMarketIndex + ); } public getActivePerpPositionsForUserAccount( @@ -1162,20 +1561,11 @@ export class User { includeOpenOrders = true, liquidationBuffer?: BN ): BN { - return this.getSpotMarketAssetValue( - undefined, - marginCategory, + return this.getMarginCalculation(marginCategory, { + strict, includeOpenOrders, - strict - ).add( - this.getUnrealizedPNL( - true, - undefined, - marginCategory, - strict, - liquidationBuffer - ) - ); + liquidationBuffer, + }).totalCollateral; } public getLiquidationBuffer(): BN | undefined { @@ -1193,13 +1583,27 @@ export class User { * calculates User Health by comparing total collateral and maint. margin requirement * @returns : number (value from [0, 100]) */ - public getHealth(): number { - if (this.isBeingLiquidated()) { + public getHealth(perpMarketIndex?: number): number { + if (this.isBeingLiquidated() && !perpMarketIndex) { return 0; } - const totalCollateral = this.getTotalCollateral('Maintenance'); - const maintenanceMarginReq = this.getMaintenanceMarginRequirement(); + const marginCalc = this.getMarginCalculation('Maintenance'); + + let totalCollateral: BN; + let maintenanceMarginReq: BN; + + if (perpMarketIndex) { + const isolatedMarginCalc = + marginCalc.isolatedMarginCalculations.get(perpMarketIndex); + if (isolatedMarginCalc) { + totalCollateral = isolatedMarginCalc.totalCollateral; + maintenanceMarginReq = isolatedMarginCalc.marginRequirement; + } + } else { + totalCollateral = marginCalc.totalCollateral; + maintenanceMarginReq = marginCalc.marginRequirement; + } let health: number; @@ -1495,9 +1899,9 @@ export class User { * calculates current user leverage which is (total liability size) / (net asset value) * @returns : Precision TEN_THOUSAND */ - public getLeverage(includeOpenOrders = true): BN { + public getLeverage(includeOpenOrders = true, perpMarketIndex?: number): BN { return this.calculateLeverageFromComponents( - this.getLeverageComponents(includeOpenOrders) + this.getLeverageComponents(includeOpenOrders, undefined, perpMarketIndex) ); } @@ -1525,13 +1929,44 @@ export class User { getLeverageComponents( includeOpenOrders = true, - marginCategory: MarginCategory = undefined + marginCategory: MarginCategory = undefined, + perpMarketIndex?: number ): { perpLiabilityValue: BN; perpPnl: BN; spotAssetValue: BN; spotLiabilityValue: BN; } { + if (perpMarketIndex) { + const perpPosition = this.getPerpPositionOrEmpty(perpMarketIndex); + const perpLiability = this.calculateWeightedPerpPositionLiability( + perpPosition, + marginCategory, + undefined, + includeOpenOrders + ); + const perpMarket = this.driftClient.getPerpMarketAccount( + perpPosition.marketIndex + ); + + const oraclePriceData = this.getOracleDataForPerpMarket( + perpPosition.marketIndex + ); + + const positionUnrealizedPnl = calculatePositionPNL( + perpMarket, + perpPosition, + true, + oraclePriceData + ); + return { + perpLiabilityValue: perpLiability, + perpPnl: positionUnrealizedPnl, + spotAssetValue: ZERO, + spotLiabilityValue: ZERO, + }; + } + const perpLiability = this.getTotalPerpPositionLiability( marginCategory, undefined, @@ -1821,7 +2256,7 @@ export class User { return netAssetValue.mul(TEN_THOUSAND).div(totalLiabilityValue); } - public canBeLiquidated(): { + public canBeLiquidated(perpMarketIndex?: number): { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN; @@ -1835,8 +2270,11 @@ export class User { liquidationBuffer ); - const marginRequirement = - this.getMaintenanceMarginRequirement(liquidationBuffer); + const marginRequirement = this.getMaintenanceMarginRequirement( + liquidationBuffer, + perpMarketIndex ? 'Isolated' : 'Cross', + perpMarketIndex + ); const canBeLiquidated = totalCollateral.lt(marginRequirement); return { @@ -2010,8 +2448,61 @@ export class User { marginCategory: MarginCategory = 'Maintenance', includeOpenOrders = false, offsetCollateral = ZERO, - enteringHighLeverage = undefined + enteringHighLeverage = false, + marginType?: MarginType ): BN { + const market = this.driftClient.getPerpMarketAccount(marketIndex); + + const oracle = + this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle; + + const oraclePrice = + this.driftClient.getOracleDataForPerpMarket(marketIndex).price; + + const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); + + if (marginType === 'Isolated') { + const marginCalculation = this.getMarginCalculation(marginCategory, { + strict: false, + includeOpenOrders, + enteringHighLeverage, + }); + const isolatedMarginCalculation = + marginCalculation.isolatedMarginCalculations.get(marketIndex); + const { totalCollateral, marginRequirement } = isolatedMarginCalculation; + + let freeCollateral = BN.max( + ZERO, + totalCollateral.sub(marginRequirement) + ).add(offsetCollateral); + + let freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp( + market, + currentPerpPosition, + positionBaseSizeChange, + oraclePrice, + marginCategory, + includeOpenOrders, + enteringHighLeverage + ); + + if (freeCollateralDelta.eq(ZERO)) { + return new BN(-1); + } + + const liqPriceDelta = freeCollateral + .mul(QUOTE_PRECISION) + .div(freeCollateralDelta); + + const liqPrice = oraclePrice.sub(liqPriceDelta); + + if (liqPrice.lt(ZERO)) { + return new BN(-1); + } + + return liqPrice; + } + const totalCollateral = this.getTotalCollateral( marginCategory, false, @@ -2029,15 +2520,6 @@ export class User { totalCollateral.sub(marginRequirement) ).add(offsetCollateral); - const oracle = - this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle; - - const oraclePrice = - this.driftClient.getOracleDataForPerpMarket(marketIndex).price; - - const market = this.driftClient.getPerpMarketAccount(marketIndex); - const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); - positionBaseSizeChange = standardizeBaseAssetAmount( positionBaseSizeChange, market.amm.orderStepSize From db4a374e02d0321bfb6a0c6ea2cd71b8754a4797 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 25 Aug 2025 16:46:35 -0600 Subject: [PATCH 132/247] feat: margin calc unit tests --- sdk/tests/dlob/helpers.ts | 4 + sdk/tests/user/getMarginCalculation.test.ts | 213 ++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 sdk/tests/user/getMarginCalculation.test.ts diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index d1b68abe8c..88d203f875 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -27,6 +27,8 @@ import { DataAndSlot, } from '../../src'; import { EventEmitter } from 'events'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { UserEvents } from '../../src/accounts/types'; export const mockPerpPosition: PerpPosition = { baseAssetAmount: new BN(0), @@ -660,6 +662,7 @@ export class MockUserMap implements UserMapInterface { private userMap = new Map(); private userAccountToAuthority = new Map(); private driftClient: DriftClient; + eventEmitter: StrictEventEmitter; constructor() { this.userMap = new Map(); @@ -669,6 +672,7 @@ export class MockUserMap implements UserMapInterface { wallet: new Wallet(new Keypair()), programID: PublicKey.default, }); + this.eventEmitter = new EventEmitter(); } public async subscribe(): Promise {} diff --git a/sdk/tests/user/getMarginCalculation.test.ts b/sdk/tests/user/getMarginCalculation.test.ts new file mode 100644 index 0000000000..36ce90ecb0 --- /dev/null +++ b/sdk/tests/user/getMarginCalculation.test.ts @@ -0,0 +1,213 @@ +import { + BN, + ZERO, + User, + UserAccount, + PublicKey, + PerpMarketAccount, + SpotMarketAccount, + PRICE_PRECISION, + OraclePriceData, + BASE_PRECISION, + QUOTE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + MARGIN_PRECISION, + OPEN_ORDER_MARGIN_REQUIREMENT, +} from '../../src'; +import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; +import { assert } from '../../src/assert/assert'; +import { mockUserAccount as baseMockUserAccount } from './helpers'; +import * as _ from 'lodash'; + +async function makeMockUser( + myMockPerpMarkets: Array, + myMockSpotMarkets: Array, + myMockUserAccount: UserAccount, + perpOraclePriceList: number[], + spotOraclePriceList: number[] +): Promise { + const umap = new MockUserMap(); + const mockUser: User = await umap.mustGet('1'); + mockUser._isSubscribed = true; + mockUser.driftClient._isSubscribed = true; + mockUser.driftClient.accountSubscriber.isSubscribed = true; + + const oraclePriceMap: Record = {}; + for (let i = 0; i < myMockPerpMarkets.length; i++) { + oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] = + perpOraclePriceList[i] ?? 1; + } + for (let i = 0; i < myMockSpotMarkets.length; i++) { + oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = + spotOraclePriceList[i] ?? 1; + } + + function getMockUserAccount(): UserAccount { + return myMockUserAccount; + } + function getMockPerpMarket(marketIndex: number): PerpMarketAccount { + return myMockPerpMarkets[marketIndex]; + } + function getMockSpotMarket(marketIndex: number): SpotMarketAccount { + return myMockSpotMarkets[marketIndex]; + } + function getMockOracle(oracleKey: PublicKey) { + const data: OraclePriceData = { + price: new BN( + (oraclePriceMap[oracleKey.toString()] ?? 1) * + PRICE_PRECISION.toNumber() + ), + slot: new BN(0), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + return { data, slot: 0 }; + } + function getOracleDataForPerpMarket(marketIndex: number) { + const oracle = getMockPerpMarket(marketIndex).amm.oracle; + return getMockOracle(oracle).data; + } + function getOracleDataForSpotMarket(marketIndex: number) { + const oracle = getMockSpotMarket(marketIndex).oracle; + return getMockOracle(oracle).data; + } + + mockUser.getUserAccount = getMockUserAccount; + mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any; + mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any; + mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any; + mockUser.driftClient.getOracleDataForPerpMarket = getOracleDataForPerpMarket as any; + mockUser.driftClient.getOracleDataForSpotMarket = getOracleDataForSpotMarket as any; + return mockUser; +} + +describe('getMarginCalculation snapshot', () => { + it('empty account returns zeroed snapshot', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(ZERO)); + assert(calc.numSpotLiabilities === 0); + assert(calc.numPerpLiabilities === 0); + }); + + it('quote deposit increases totalCollateral, no requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expected = new BN('10000000000'); // $10k + assert(calc.totalCollateral.eq(expected)); + assert(calc.marginRequirement.eq(ZERO)); + }); + + it('quote borrow increases requirement and buffer applies', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Borrow 100 quote + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.BORROW; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 100 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const tenPercent = MARGIN_PRECISION.divn(10); + const calc = user.getMarginCalculation('Initial', { + liquidationBuffer: tenPercent, + }); + const liability = new BN(100).mul(QUOTE_PRECISION); // $100 + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(liability)); + assert( + calc.marginRequirementPlusBuffer.eq( + liability.mul(tenPercent).div(MARGIN_PRECISION) + ) + ); + assert(calc.numSpotLiabilities === 1); + }); + + it('non-quote spot open orders add IM', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Market 1 (e.g., SOL) with 2 open orders + myMockUserAccount.spotPositions[1].marketIndex = 1; + myMockUserAccount.spotPositions[1].openOrders = 2; + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expectedIM = new BN(2).mul(OPEN_ORDER_MARGIN_REQUIREMENT); + assert(calc.marginRequirement.eq(expectedIM)); + }); + + it('perp long liability reflects maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // 20 base long, -$10 quote (liability) + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-10).mul( + QUOTE_PRECISION + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + // From existing liquidation test expectations: 2_000_000 + assert(calc.marginRequirement.eq(new BN('2000000'))); + }); +}); + + From 950b3c3e9098831cab628ee052d9b8cfbdd75b9b Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 26 Aug 2025 15:14:17 -0600 Subject: [PATCH 133/247] temp --- sdk/tests/amm/test.ts | 2 +- sdk/tests/user/getMarginCalculation.test.ts | 213 ---------- sdk/tests/user/getMarginCalculation.ts | 430 ++++++++++++++++++++ 3 files changed, 431 insertions(+), 214 deletions(-) delete mode 100644 sdk/tests/user/getMarginCalculation.test.ts create mode 100644 sdk/tests/user/getMarginCalculation.ts diff --git a/sdk/tests/amm/test.ts b/sdk/tests/amm/test.ts index ab849c57d6..b4377f4066 100644 --- a/sdk/tests/amm/test.ts +++ b/sdk/tests/amm/test.ts @@ -279,7 +279,7 @@ describe('AMM Tests', () => { longIntensity, shortIntensity, volume24H, - 0 + 0, ); const l1 = spreads[0]; const s1 = spreads[1]; diff --git a/sdk/tests/user/getMarginCalculation.test.ts b/sdk/tests/user/getMarginCalculation.test.ts deleted file mode 100644 index 36ce90ecb0..0000000000 --- a/sdk/tests/user/getMarginCalculation.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { - BN, - ZERO, - User, - UserAccount, - PublicKey, - PerpMarketAccount, - SpotMarketAccount, - PRICE_PRECISION, - OraclePriceData, - BASE_PRECISION, - QUOTE_PRECISION, - SPOT_MARKET_BALANCE_PRECISION, - SpotBalanceType, - MARGIN_PRECISION, - OPEN_ORDER_MARGIN_REQUIREMENT, -} from '../../src'; -import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; -import { assert } from '../../src/assert/assert'; -import { mockUserAccount as baseMockUserAccount } from './helpers'; -import * as _ from 'lodash'; - -async function makeMockUser( - myMockPerpMarkets: Array, - myMockSpotMarkets: Array, - myMockUserAccount: UserAccount, - perpOraclePriceList: number[], - spotOraclePriceList: number[] -): Promise { - const umap = new MockUserMap(); - const mockUser: User = await umap.mustGet('1'); - mockUser._isSubscribed = true; - mockUser.driftClient._isSubscribed = true; - mockUser.driftClient.accountSubscriber.isSubscribed = true; - - const oraclePriceMap: Record = {}; - for (let i = 0; i < myMockPerpMarkets.length; i++) { - oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] = - perpOraclePriceList[i] ?? 1; - } - for (let i = 0; i < myMockSpotMarkets.length; i++) { - oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = - spotOraclePriceList[i] ?? 1; - } - - function getMockUserAccount(): UserAccount { - return myMockUserAccount; - } - function getMockPerpMarket(marketIndex: number): PerpMarketAccount { - return myMockPerpMarkets[marketIndex]; - } - function getMockSpotMarket(marketIndex: number): SpotMarketAccount { - return myMockSpotMarkets[marketIndex]; - } - function getMockOracle(oracleKey: PublicKey) { - const data: OraclePriceData = { - price: new BN( - (oraclePriceMap[oracleKey.toString()] ?? 1) * - PRICE_PRECISION.toNumber() - ), - slot: new BN(0), - confidence: new BN(1), - hasSufficientNumberOfDataPoints: true, - }; - return { data, slot: 0 }; - } - function getOracleDataForPerpMarket(marketIndex: number) { - const oracle = getMockPerpMarket(marketIndex).amm.oracle; - return getMockOracle(oracle).data; - } - function getOracleDataForSpotMarket(marketIndex: number) { - const oracle = getMockSpotMarket(marketIndex).oracle; - return getMockOracle(oracle).data; - } - - mockUser.getUserAccount = getMockUserAccount; - mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any; - mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any; - mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any; - mockUser.driftClient.getOracleDataForPerpMarket = getOracleDataForPerpMarket as any; - mockUser.driftClient.getOracleDataForSpotMarket = getOracleDataForSpotMarket as any; - return mockUser; -} - -describe('getMarginCalculation snapshot', () => { - it('empty account returns zeroed snapshot', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const calc = user.getMarginCalculation('Initial'); - assert(calc.totalCollateral.eq(ZERO)); - assert(calc.marginRequirement.eq(ZERO)); - assert(calc.numSpotLiabilities === 0); - assert(calc.numPerpLiabilities === 0); - }); - - it('quote deposit increases totalCollateral, no requirement', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; - myMockUserAccount.spotPositions[0].scaledBalance = new BN( - 10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber() - ); - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const calc = user.getMarginCalculation('Initial'); - const expected = new BN('10000000000'); // $10k - assert(calc.totalCollateral.eq(expected)); - assert(calc.marginRequirement.eq(ZERO)); - }); - - it('quote borrow increases requirement and buffer applies', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - // Borrow 100 quote - myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.BORROW; - myMockUserAccount.spotPositions[0].scaledBalance = new BN( - 100 * SPOT_MARKET_BALANCE_PRECISION.toNumber() - ); - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const tenPercent = MARGIN_PRECISION.divn(10); - const calc = user.getMarginCalculation('Initial', { - liquidationBuffer: tenPercent, - }); - const liability = new BN(100).mul(QUOTE_PRECISION); // $100 - assert(calc.totalCollateral.eq(ZERO)); - assert(calc.marginRequirement.eq(liability)); - assert( - calc.marginRequirementPlusBuffer.eq( - liability.mul(tenPercent).div(MARGIN_PRECISION) - ) - ); - assert(calc.numSpotLiabilities === 1); - }); - - it('non-quote spot open orders add IM', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - // Market 1 (e.g., SOL) with 2 open orders - myMockUserAccount.spotPositions[1].marketIndex = 1; - myMockUserAccount.spotPositions[1].openOrders = 2; - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const calc = user.getMarginCalculation('Initial'); - const expectedIM = new BN(2).mul(OPEN_ORDER_MARGIN_REQUIREMENT); - assert(calc.marginRequirement.eq(expectedIM)); - }); - - it('perp long liability reflects maintenance requirement', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - // 20 base long, -$10 quote (liability) - myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( - BASE_PRECISION - ); - myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-10).mul( - QUOTE_PRECISION - ); - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const calc = user.getMarginCalculation('Maintenance'); - // From existing liquidation test expectations: 2_000_000 - assert(calc.marginRequirement.eq(new BN('2000000'))); - }); -}); - - diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts new file mode 100644 index 0000000000..9ca9b5e4aa --- /dev/null +++ b/sdk/tests/user/getMarginCalculation.ts @@ -0,0 +1,430 @@ +import { + BN, + ZERO, + User, + UserAccount, + PublicKey, + PerpMarketAccount, + SpotMarketAccount, + PRICE_PRECISION, + OraclePriceData, + BASE_PRECISION, + QUOTE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + MARGIN_PRECISION, + OPEN_ORDER_MARGIN_REQUIREMENT, + SPOT_MARKET_WEIGHT_PRECISION, +} from '../../src'; +import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; +import { assert } from '../../src/assert/assert'; +import { mockUserAccount as baseMockUserAccount } from './helpers'; +import * as _ from 'lodash'; + +async function makeMockUser( + myMockPerpMarkets: Array, + myMockSpotMarkets: Array, + myMockUserAccount: UserAccount, + perpOraclePriceList: number[], + spotOraclePriceList: number[] +): Promise { + const umap = new MockUserMap(); + const mockUser: User = await umap.mustGet('1'); + mockUser._isSubscribed = true; + mockUser.driftClient._isSubscribed = true; + mockUser.driftClient.accountSubscriber.isSubscribed = true; + + const oraclePriceMap: Record = {}; + for (let i = 0; i < myMockPerpMarkets.length; i++) { + oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] = + perpOraclePriceList[i] ?? 1; + } + for (let i = 0; i < myMockSpotMarkets.length; i++) { + oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = + spotOraclePriceList[i] ?? 1; + } + + function getMockUserAccount(): UserAccount { + return myMockUserAccount; + } + function getMockPerpMarket(marketIndex: number): PerpMarketAccount { + return myMockPerpMarkets[marketIndex]; + } + function getMockSpotMarket(marketIndex: number): SpotMarketAccount { + return myMockSpotMarkets[marketIndex]; + } + function getMockOracle(oracleKey: PublicKey) { + const data: OraclePriceData = { + price: new BN( + (oraclePriceMap[oracleKey.toString()] ?? 1) * + PRICE_PRECISION.toNumber() + ), + slot: new BN(0), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + return { data, slot: 0 }; + } + function getOracleDataForPerpMarket(marketIndex: number) { + const oracle = getMockPerpMarket(marketIndex).amm.oracle; + return getMockOracle(oracle).data; + } + function getOracleDataForSpotMarket(marketIndex: number) { + const oracle = getMockSpotMarket(marketIndex).oracle; + return getMockOracle(oracle).data; + } + + mockUser.getUserAccount = getMockUserAccount; + mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any; + mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any; + mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any; + mockUser.driftClient.getOracleDataForPerpMarket = getOracleDataForPerpMarket as any; + mockUser.driftClient.getOracleDataForSpotMarket = getOracleDataForSpotMarket as any; + return mockUser; +} + +describe('getMarginCalculation snapshot', () => { + it('empty account returns zeroed snapshot', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(ZERO)); + assert(calc.numSpotLiabilities === 0); + assert(calc.numPerpLiabilities === 0); + }); + + it('quote deposit increases totalCollateral, no requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expected = new BN('10000000000'); // $10k + assert(calc.totalCollateral.eq(expected)); + assert(calc.marginRequirement.eq(ZERO)); + }); + + it('quote borrow increases requirement and buffer applies', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Borrow 100 quote + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.BORROW; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 100 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const tenPercent = MARGIN_PRECISION.divn(10); + const calc = user.getMarginCalculation('Initial', { + liquidationBuffer: tenPercent, + }); + const liability = new BN(100).mul(QUOTE_PRECISION); // $100 + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(liability)); + assert( + calc.marginRequirementPlusBuffer.eq( + liability.mul(tenPercent).div(MARGIN_PRECISION) + ) + ); + assert(calc.numSpotLiabilities === 1); + }); + + it('non-quote spot open orders add IM', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Market 1 (e.g., SOL) with 2 open orders + myMockUserAccount.spotPositions[1].marketIndex = 1; + myMockUserAccount.spotPositions[1].openOrders = 2; + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expectedIM = new BN(2).mul(OPEN_ORDER_MARGIN_REQUIREMENT); + assert(calc.marginRequirement.eq(expectedIM)); + }); + + it('perp long liability reflects maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // 20 base long, -$10 quote (liability) + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-10).mul( + QUOTE_PRECISION + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + // From existing liquidation test expectations: 2_000_000 + assert(calc.marginRequirement.eq(new BN('2000000'))); + }); + + it.skip('maker position reducing: collateral equals maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Perp exposure: 20 base notional at oracle price 1 → maintenance MR = 10% of $20 = $2 + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + // Set entry/breakeven at $1 so unrealized PnL = $0 + myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + // Provide exactly $2 in quote collateral + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + myMockUserAccount.spotPositions[0].scaledBalance = new BN(2).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + console.log('calc.marginRequirement', calc.marginRequirement.toString()); + console.log('calc.totalCollateral', calc.totalCollateral.toString()); + assert(calc.marginRequirement.eq(calc.totalCollateral)); + }); + + it('maker reducing after simulated fill: collateral equals maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + + // Build maker and taker accounts + const makerAccount = _.cloneDeep(baseMockUserAccount); + const takerAccount = _.cloneDeep(baseMockUserAccount); + + // Oracle price = 1 for perp and spot + const perpOracles = [1, 1, 1, 1, 1, 1, 1, 1]; + const spotOracles = [1, 1, 1, 1, 1, 1, 1, 1]; + + // Pre-fill: maker has 21 base long at entry 1 ($21 notional), taker flat + makerAccount.perpPositions[0].baseAssetAmount = new BN(21).mul(BASE_PRECISION); + makerAccount.perpPositions[0].quoteEntryAmount = new BN(-21).mul(QUOTE_PRECISION); + makerAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-21).mul(QUOTE_PRECISION); + // Provide exactly $2 in quote collateral to equal 10% maintenance of 20 notional post-fill + makerAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + makerAccount.spotPositions[0].scaledBalance = new BN(2).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + + // Simulate fill: maker sells 1 base to taker at price = oracle = 1 + // Post-fill maker position: 20 base long with zero unrealized PnL + const maker: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + makerAccount, + perpOracles, + spotOracles + ); + const taker: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + takerAccount, + perpOracles, + spotOracles + ); + + // Apply synthetic trade deltas to both user accounts + // Maker: base 21 -> 20; taker: base 0 -> 1. Use quote deltas consistent with price 1, fee 0 + maker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + maker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + maker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + // Align quoteAssetAmount with base value so unrealized PnL = 0 at price 1 + maker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + + taker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(1).mul( + BASE_PRECISION + ); + taker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + taker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + // Also set taker's quoteAssetAmount consistently + taker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + + const makerCalc = maker.getMarginCalculation('Maintenance'); + assert(makerCalc.marginRequirement.eq(makerCalc.totalCollateral)); + assert(makerCalc.marginRequirement.gt(ZERO)); + }); + + it('isolated position margin requirement (SDK parity)', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + + // Configure perp market 0 ratios to match on-chain test + myMockPerpMarkets[0].marginRatioInitial = 1000; // 10% + myMockPerpMarkets[0].marginRatioMaintenance = 500; // 5% + + // Configure spot market 1 (e.g., SOL) weights to match on-chain test + myMockSpotMarkets[1].initialAssetWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 8) / 10; // 0.8 + myMockSpotMarkets[1].maintenanceAssetWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 9) / 10; // 0.9 + myMockSpotMarkets[1].initialLiabilityWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 12) / 10; // 1.2 + myMockSpotMarkets[1].maintenanceLiabilityWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 11) / 10; // 1.1 + + // ---------- Cross margin only (spot positions) ---------- + const crossAccount = _.cloneDeep(baseMockUserAccount); + // USDC deposit: $20,000 + crossAccount.spotPositions[0].marketIndex = 0; + crossAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + crossAccount.spotPositions[0].scaledBalance = new BN(20000).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + // SOL borrow: 100 units + crossAccount.spotPositions[1].marketIndex = 1; + crossAccount.spotPositions[1].balanceType = SpotBalanceType.BORROW; + crossAccount.spotPositions[1].scaledBalance = new BN(10000).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + // No perp exposure in cross calc + crossAccount.perpPositions[0].baseAssetAmount = ZERO; + crossAccount.perpPositions[0].quoteAssetAmount = ZERO; + + const userCross: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + crossAccount, + [100, 1, 1, 1, 1, 1, 1, 1], // perp oracle for market 0 = 100 + [1, 100, 1, 1, 1, 1, 1, 1] // spot oracle: usdc=1, sol=100 + ); + + const crossCalc = userCross.getMarginCalculation('Initial'); + // Expect: cross MR from SOL borrow: 100 * $100 = $10,000 * 1.2 = $12,000 + assert(crossCalc.marginRequirement.eq(new BN('12000000000'))); + // Expect: cross total collateral from USDC deposit only = $20,000 + assert(crossCalc.totalCollateral.eq(new BN('20000000000'))); + // Meets cross margin requirement + assert(crossCalc.marginRequirement.lte(crossCalc.totalCollateral)); + + // With 10% buffer + const tenPct = new BN(1000); + const crossCalcBuf = userCross.getMarginCalculation('Initial', { + liquidationBuffer: tenPct, + }); + console.log('crossCalcBuf.marginRequirementPlusBuffer', crossCalcBuf.marginRequirementPlusBuffer.toString()); + console.log('crossCalcBuf.totalCollateralBuffer', crossCalcBuf.totalCollateralBuffer.toString()); + assert(crossCalcBuf.marginRequirementPlusBuffer.eq(new BN('13000000000'))); + const crossTotalPlusBuffer = crossCalcBuf.totalCollateral.add( + crossCalcBuf.totalCollateralBuffer + ); + assert(crossTotalPlusBuffer.eq(new BN('20000000000'))); + + // ---------- Isolated perp position (simulate isolated by separate user) ---------- + const isolatedAccount = _.cloneDeep(baseMockUserAccount); + // Perp: 100 base long, quote -11,000 => PnL = 10k - 11k = -$1,000 + isolatedAccount.perpPositions[0].baseAssetAmount = new BN(100).mul( + BASE_PRECISION + ); + isolatedAccount.perpPositions[0].quoteAssetAmount = new BN(-11000).mul( + QUOTE_PRECISION + ); + // Simulate isolated balance: $100 quote deposit on this user + isolatedAccount.spotPositions[0].marketIndex = 0; + isolatedAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + isolatedAccount.spotPositions[0].scaledBalance = new BN(100).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + + const userIsolated: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + isolatedAccount, + [100, 1, 1, 1, 1, 1, 1, 1], + [1, 100, 1, 1, 1, 1, 1, 1] + ); + + const isoCalc = userIsolated.getMarginCalculation('Initial'); + // Expect: perp initial MR = 10% * $10,000 = $1,000 + assert(isoCalc.marginRequirement.eq(new BN('1000000000'))); + // Expect: total collateral = $100 (deposit) + (-$1,000) (PnL) = -$900 + assert(isoCalc.totalCollateral.eq(new BN('-900000000'))); + assert(isoCalc.marginRequirement.gt(isoCalc.totalCollateral)); + + const isoCalcBuf = userIsolated.getMarginCalculation('Initial', { + liquidationBuffer: tenPct, + }); + assert(isoCalcBuf.marginRequirementPlusBuffer.eq(new BN('2000000000'))); + const isoTotalPlusBuffer = isoCalcBuf.totalCollateral.add( + isoCalcBuf.totalCollateralBuffer + ); + assert(isoTotalPlusBuffer.eq(new BN('-1000000000'))); + }); +}); + + From b24334c52dc0a55e39081e655ba63d790439d326 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 27 Aug 2025 23:45:41 -0600 Subject: [PATCH 134/247] feat: finally - parity with on-chain cargo test --- sdk/src/decode/user.ts | 5 +- sdk/src/marginCalculation.ts | 351 +++++++++++++++++++++++++ sdk/src/types.ts | 6 + sdk/src/user.ts | 216 ++++++++------- sdk/tests/dlob/helpers.ts | 1 + sdk/tests/user/getMarginCalculation.ts | 64 ++--- sdk/tests/user/test.ts | 7 - 7 files changed, 484 insertions(+), 166 deletions(-) create mode 100644 sdk/src/marginCalculation.ts diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index e0d852f6e8..c3022d06aa 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -117,7 +117,9 @@ export function decodeUser(buffer: Buffer): UserAccount { offset += 3; const perLpBase = buffer.readUInt8(offset); offset += 1; - + // TODO: verify this works + const positionFlag = buffer.readUInt8(offset); + offset += 1; perpPositions.push({ lastCumulativeFundingRate, baseAssetAmount, @@ -135,6 +137,7 @@ export function decodeUser(buffer: Buffer): UserAccount { openOrders, perLpBase, maxMarginRatio, + positionFlag, }); } diff --git a/sdk/src/marginCalculation.ts b/sdk/src/marginCalculation.ts new file mode 100644 index 0000000000..2cb1dbe839 --- /dev/null +++ b/sdk/src/marginCalculation.ts @@ -0,0 +1,351 @@ +import { BN } from './'; +import { MARGIN_PRECISION } from './constants/numericConstants'; +import { MarketType } from './types'; + +export type MarginCategory = 'Initial' | 'Maintenance' | 'Fill'; + +export type MarginCalculationMode = + | { type: 'Standard' } + | { type: 'Liquidation'; marketToTrackMarginRequirement?: MarketIdentifier }; + +export class MarketIdentifier { + marketType: MarketType; + marketIndex: number; + + private constructor(marketType: MarketType, marketIndex: number) { + this.marketType = marketType; + this.marketIndex = marketIndex; + } + + static spot(marketIndex: number): MarketIdentifier { + return new MarketIdentifier(MarketType.SPOT, marketIndex); + } + + static perp(marketIndex: number): MarketIdentifier { + return new MarketIdentifier(MarketType.PERP, marketIndex); + } + + equals(other: MarketIdentifier | undefined): boolean { + return ( + !!other && + this.marketType === other.marketType && + this.marketIndex === other.marketIndex + ); + } +} + +export class MarginContext { + marginType: MarginCategory; + mode: MarginCalculationMode; + strict: boolean; + ignoreInvalidDepositOracles: boolean; + marginBuffer: BN; // scaled by MARGIN_PRECISION + fuelBonusNumerator: BN; // seconds since last update + fuelBonus: BN; // not used in calculation aggregation here + fuelPerpDelta?: { marketIndex: number; delta: BN }; + fuelSpotDeltas: Array<{ marketIndex: number; delta: BN }>; // up to 2 in rust + marginRatioOverride?: number; + + private constructor(marginType: MarginCategory) { + this.marginType = marginType; + this.mode = { type: 'Standard' }; + this.strict = false; + this.ignoreInvalidDepositOracles = false; + this.marginBuffer = new BN(0); + this.fuelBonusNumerator = new BN(0); + this.fuelBonus = new BN(0); + this.fuelSpotDeltas = []; + } + + static standard(marginType: MarginCategory): MarginContext { + return new MarginContext(marginType); + } + + static liquidation(marginBuffer: BN): MarginContext { + const ctx = new MarginContext('Maintenance'); + ctx.mode = { type: 'Liquidation' }; + ctx.marginBuffer = marginBuffer ?? new BN(0); + return ctx; + } + + strictMode(strict: boolean): this { + this.strict = strict; + return this; + } + + ignoreInvalidDeposits(ignore: boolean): this { + this.ignoreInvalidDepositOracles = ignore; + return this; + } + + setMarginBuffer(buffer?: BN): this { + this.marginBuffer = buffer ?? new BN(0); + return this; + } + + setFuelPerpDelta(marketIndex: number, delta: BN): this { + this.fuelPerpDelta = { marketIndex, delta }; + return this; + } + + setFuelSpotDelta(marketIndex: number, delta: BN): this { + this.fuelSpotDeltas = [{ marketIndex, delta }]; + return this; + } + + setFuelSpotDeltas(deltas: Array<{ marketIndex: number; delta: BN }>): this { + this.fuelSpotDeltas = deltas; + return this; + } + + setFuelNumerator(numerator: BN): this { + this.fuelBonusNumerator = numerator ?? new BN(0); + return this; + } + + setMarginRatioOverride(ratio: number): this { + this.marginRatioOverride = ratio; + return this; + } + + trackMarketMarginRequirement(marketIdentifier: MarketIdentifier): this { + if (this.mode.type !== 'Liquidation') { + throw new Error( + 'InvalidMarginCalculation: Cant track market outside of liquidation mode' + ); + } + this.mode.marketToTrackMarginRequirement = marketIdentifier; + return this; + } +} + +export class IsolatedMarginCalculation { + marginRequirement: BN; + totalCollateral: BN; // deposit + pnl + totalCollateralBuffer: BN; + marginRequirementPlusBuffer: BN; + + constructor() { + this.marginRequirement = new BN(0); + this.totalCollateral = new BN(0); + this.totalCollateralBuffer = new BN(0); + this.marginRequirementPlusBuffer = new BN(0); + } + + getTotalCollateralPlusBuffer(): BN { + return this.totalCollateral.add(this.totalCollateralBuffer); + } + + meetsMarginRequirement(): boolean { + return this.totalCollateral.gte(this.marginRequirement); + } + + meetsMarginRequirementWithBuffer(): boolean { + return this.getTotalCollateralPlusBuffer().gte( + this.marginRequirementPlusBuffer + ); + } + + marginShortage(): BN { + const shortage = this.marginRequirementPlusBuffer.sub( + this.getTotalCollateralPlusBuffer() + ); + return shortage.isNeg() ? new BN(0) : shortage; + } +} + +export class MarginCalculation { + context: MarginContext; + totalCollateral: BN; + totalCollateralBuffer: BN; + marginRequirement: BN; + marginRequirementPlusBuffer: BN; + isolatedMarginCalculations: Map; + numSpotLiabilities: number; + numPerpLiabilities: number; + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; + trackedMarketMarginRequirement: BN; + fuelDeposits: number; + fuelBorrows: number; + fuelPositions: number; + + constructor(context: MarginContext) { + this.context = context; + this.totalCollateral = new BN(0); + this.totalCollateralBuffer = new BN(0); + this.marginRequirement = new BN(0); + this.marginRequirementPlusBuffer = new BN(0); + this.isolatedMarginCalculations = new Map(); + this.numSpotLiabilities = 0; + this.numPerpLiabilities = 0; + this.allDepositOraclesValid = true; + this.allLiabilityOraclesValid = true; + this.withPerpIsolatedLiability = false; + this.withSpotIsolatedLiability = false; + this.trackedMarketMarginRequirement = new BN(0); + this.fuelDeposits = 0; + this.fuelBorrows = 0; + this.fuelPositions = 0; + } + + addIsolatedTotalCollateral(delta: BN): void { + this.totalCollateral = this.totalCollateral.add(delta); + if (this.context.marginBuffer.gt(new BN(0)) && delta.isNeg()) { + this.totalCollateralBuffer = this.totalCollateralBuffer.add( + delta.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ); + } + } + + addIsolatedMarginRequirement( + marginRequirement: BN, + liabilityValue: BN, + marketIdentifier: MarketIdentifier + ): void { + this.marginRequirement = this.marginRequirement.add(marginRequirement); + if (this.context.marginBuffer.gt(new BN(0))) { + this.marginRequirementPlusBuffer = this.marginRequirementPlusBuffer.add( + marginRequirement.add( + liabilityValue.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ) + ); + } + const tracked = this.marketToTrackMarginRequirement(); + if (tracked && tracked.equals(marketIdentifier)) { + this.trackedMarketMarginRequirement = + this.trackedMarketMarginRequirement.add(marginRequirement); + } + } + + addIsolatedMarginCalculation( + marketIndex: number, + depositValue: BN, + pnl: BN, + liabilityValue: BN, + marginRequirement: BN + ): void { + const totalCollateral = depositValue.add(pnl); + const totalCollateralBuffer = + this.context.marginBuffer.gt(new BN(0)) && pnl.isNeg() + ? pnl.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + : new BN(0); + + const marginRequirementPlusBuffer = this.context.marginBuffer.gt(new BN(0)) + ? marginRequirement.add( + liabilityValue.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ) + : new BN(0); + + const iso = new IsolatedMarginCalculation(); + iso.marginRequirement = marginRequirement; + iso.totalCollateral = totalCollateral; + iso.totalCollateralBuffer = totalCollateralBuffer; + iso.marginRequirementPlusBuffer = marginRequirementPlusBuffer; + this.isolatedMarginCalculations.set(marketIndex, iso); + + const tracked = this.marketToTrackMarginRequirement(); + if (tracked && tracked.equals(MarketIdentifier.perp(marketIndex))) { + this.trackedMarketMarginRequirement = + this.trackedMarketMarginRequirement.add(marginRequirement); + } + } + + addSpotLiability(): void { + this.numSpotLiabilities += 1; + } + + addPerpLiability(): void { + this.numPerpLiabilities += 1; + } + + updateAllDepositOraclesValid(valid: boolean): void { + this.allDepositOraclesValid = this.allDepositOraclesValid && valid; + } + + updateAllLiabilityOraclesValid(valid: boolean): void { + this.allLiabilityOraclesValid = this.allLiabilityOraclesValid && valid; + } + + updateWithSpotIsolatedLiability(isolated: boolean): void { + this.withSpotIsolatedLiability = this.withSpotIsolatedLiability || isolated; + } + + updateWithPerpIsolatedLiability(isolated: boolean): void { + this.withPerpIsolatedLiability = this.withPerpIsolatedLiability || isolated; + } + + validateNumSpotLiabilities(): void { + if (this.numSpotLiabilities > 0 && this.marginRequirement.eq(new BN(0))) { + throw new Error( + 'InvalidMarginRatio: num_spot_liabilities>0 but margin_requirement=0' + ); + } + } + + getNumOfLiabilities(): number { + return this.numSpotLiabilities + this.numPerpLiabilities; + } + + getCrossTotalCollateralPlusBuffer(): BN { + return this.totalCollateral.add(this.totalCollateralBuffer); + } + + meetsCrossMarginRequirement(): boolean { + return this.totalCollateral.gte(this.marginRequirement); + } + + meetsCrossMarginRequirementWithBuffer(): boolean { + return this.getCrossTotalCollateralPlusBuffer().gte( + this.marginRequirementPlusBuffer + ); + } + + meetsMarginRequirement(): boolean { + if (!this.meetsCrossMarginRequirement()) return false; + for (const [, iso] of this.isolatedMarginCalculations) { + if (!iso.meetsMarginRequirement()) return false; + } + return true; + } + + meetsMarginRequirementWithBuffer(): boolean { + if (!this.meetsCrossMarginRequirementWithBuffer()) return false; + for (const [, iso] of this.isolatedMarginCalculations) { + if (!iso.meetsMarginRequirementWithBuffer()) return false; + } + return true; + } + + getCrossFreeCollateral(): BN { + const free = this.totalCollateral.sub(this.marginRequirement); + return free.isNeg() ? new BN(0) : free; + } + + getIsolatedFreeCollateral(marketIndex: number): BN { + const iso = this.isolatedMarginCalculations.get(marketIndex); + if (!iso) + throw new Error('InvalidMarginCalculation: missing isolated calc'); + const free = iso.totalCollateral.sub(iso.marginRequirement); + return free.isNeg() ? new BN(0) : free; + } + + getIsolatedMarginCalculation( + marketIndex: number + ): IsolatedMarginCalculation | undefined { + return this.isolatedMarginCalculations.get(marketIndex); + } + + hasIsolatedMarginCalculation(marketIndex: number): boolean { + return this.isolatedMarginCalculations.has(marketIndex); + } + + private marketToTrackMarginRequirement(): MarketIdentifier | undefined { + if (this.context.mode.type === 'Liquidation') { + return this.context.mode.marketToTrackMarginRequirement; + } + return undefined; + } +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index f263e2b9cc..c161e724a5 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1264,6 +1264,12 @@ export class OrderParamsBitFlag { static readonly UpdateHighLeverageMode = 2; } +export class PositionFlag { + static readonly IsolatedPosition = 1; + static readonly BeingLiquidated = 2; + static readonly Bankruptcy = 3; +} + export type NecessaryOrderParams = { orderType: OrderType; marketIndex: number; diff --git a/sdk/src/user.ts b/sdk/src/user.ts index cbcc2ccf47..8aa3098539 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -68,6 +68,7 @@ import { getUser30dRollingVolumeEstimate } from './math/trade'; import { MarketType, PositionDirection, + PositionFlag, SpotBalanceType, SpotMarketAccount, } from './types'; @@ -106,15 +107,12 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice'; import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber'; - -// type for account-level margin calculation results. -// Mirrors key fields from on-chain MarginCalculation; can be extended as needed. -export type IsolatedMarginCalculation = { - marginRequirement: BN; - totalCollateral: BN; - totalCollateralBuffer: BN; - marginRequirementPlusBuffer: BN; -}; +import { + MarginCalculation as JsMarginCalculation, + MarginContext, + MarketIdentifier, + IsolatedMarginCalculation, +} from './marginCalculation'; export type UserMarginCalculation = { context: { marginType: MarginCategory; strict: boolean; marginBuffer?: BN }; @@ -148,7 +146,6 @@ export class User { * Compute a consolidated margin snapshot once, without caching. * Consumers can use this to avoid duplicating work across separate calls. */ - // TODO: verify this truly matches on-chain logic well public getMarginCalculation( marginCategory: MarginCategory = 'Initial', opts?: { @@ -160,8 +157,8 @@ export class User { } ): UserMarginCalculation { const strict = opts?.strict ?? false; - const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? const enteringHighLeverage = opts?.enteringHighLeverage ?? false; + const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? const marginBuffer = opts?.liquidationBuffer; // treat as MARGIN_BUFFER ratio if provided const marginRatioOverride = opts?.marginRatioOverride; @@ -175,19 +172,12 @@ export class User { ); } - // Initialize calculation (mirrors MarginCalculation::new) - let totalCollateral = ZERO; - let totalCollateralBuffer = ZERO; - let marginRequirement = ZERO; - let marginRequirementPlusBuffer = ZERO; - const isolatedMarginCalculations: Map = - new Map(); - let numSpotLiabilities = 0; - let numPerpLiabilities = 0; - let allDepositOraclesValid = true; - let allLiabilityOraclesValid = true; - let withPerpIsolatedLiability = false; - let withSpotIsolatedLiability = false; + // Initialize calc via JS mirror of Rust MarginCalculation + const ctx = MarginContext.standard(marginCategory) + .strictMode(strict) + .setMarginBuffer(marginBuffer) + .setMarginRatioOverride(userCustomMarginRatio); + const calc = new JsMarginCalculation(ctx); // SPOT POSITIONS for (const spotPosition of this.getUserAccount().spotPositions) { @@ -225,8 +215,7 @@ export class User { spotMarket.decimals, strictOracle ); - totalCollateral = totalCollateral.add(tokenValue); - // deposit oracle validity only affects flags; keep it true by default + calc.addIsolatedTotalCollateral(tokenValue); } else { // borrow on quote contributes to margin requirement const tokenValueAbs = getStrictTokenValue( @@ -234,13 +223,12 @@ export class User { spotMarket.decimals, strictOracle ).abs(); - marginRequirement = marginRequirement.add(tokenValueAbs); - if (marginBuffer) { - marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( - tokenValueAbs.mul(marginBuffer).div(MARGIN_PRECISION) - ); - } - numSpotLiabilities += 1; + calc.addIsolatedMarginRequirement( + tokenValueAbs, + tokenValueAbs, + MarketIdentifier.spot(0) + ); + calc.addSpotLiability(); } continue; } @@ -260,40 +248,40 @@ export class User { ); // open order IM - marginRequirement = marginRequirement.add( - new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) - ); + if (includeOpenOrders) { + calc.addIsolatedMarginRequirement( + new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), + ZERO, + MarketIdentifier.spot(spotPosition.marketIndex) + ); + } if (worstCaseTokenAmount.gt(ZERO)) { // asset side increases total collateral (weighted) - totalCollateral = totalCollateral.add(worstCaseWeightedTokenValue); + calc.addIsolatedTotalCollateral(worstCaseWeightedTokenValue); } else if (worstCaseTokenAmount.lt(ZERO)) { // liability side increases margin requirement (weighted >= abs(token_value)) const liabilityWeighted = worstCaseWeightedTokenValue.abs(); - const liabilityBase = worstCaseTokenValue.abs(); - marginRequirement = marginRequirement.add(liabilityWeighted); - if (marginBuffer) { - marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( - liabilityBase.mul(marginBuffer).div(MARGIN_PRECISION) - ); - } - numSpotLiabilities += 1; - // flag isolated tier if applicable (approx: isolated asset tier → not available here) + calc.addIsolatedMarginRequirement( + liabilityWeighted, + worstCaseTokenValue.abs(), + MarketIdentifier.spot(spotPosition.marketIndex) + ); + calc.addSpotLiability(); } else if (spotPosition.openOrders !== 0) { - numSpotLiabilities += 1; + calc.addSpotLiability(); } // orders value contributes to collateral or requirement if (worstCaseOrdersValue.gt(ZERO)) { - totalCollateral = totalCollateral.add(worstCaseOrdersValue); + calc.addIsolatedTotalCollateral(worstCaseOrdersValue); } else if (worstCaseOrdersValue.lt(ZERO)) { const absVal = worstCaseOrdersValue.abs(); - marginRequirement = marginRequirement.add(absVal); - if (marginBuffer) { - marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( - absVal.mul(marginBuffer).div(MARGIN_PRECISION) - ); - } + calc.addIsolatedMarginRequirement( + absVal, + absVal, + MarketIdentifier.spot(0) + ); } } @@ -377,56 +365,56 @@ export class User { .div(PRICE_PRECISION); // Add perp contribution: isolated vs cross - const isIsolated = false; // TODO: wire to marketPosition.is_isolated when available in TS types + const isIsolated = this.isPerpPositionIsolated(marketPosition); if (isIsolated) { - const existing = isolatedMarginCalculations.get(market.marketIndex) || { - marginRequirement: ZERO, - totalCollateral: ZERO, - totalCollateralBuffer: ZERO, - marginRequirementPlusBuffer: ZERO, - }; - existing.marginRequirement = existing.marginRequirement.add( - perpMarginRequirement - ); - existing.totalCollateral = existing.totalCollateral.add( - positionUnrealizedPnl - ); - if (marginBuffer) { - existing.totalCollateralBuffer = existing.totalCollateralBuffer.add( - positionUnrealizedPnl.isNeg() - ? positionUnrealizedPnl.mul(marginBuffer).div(MARGIN_PRECISION) - : ZERO + // derive isolated quote deposit value, mirroring on-chain logic + let depositValue = ZERO; + if (marketPosition.isolatedPositionScaledBalance) { + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const strictQuote = new StrictOraclePrice( + quoteOraclePriceData.price, + strict + ? quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + : undefined + ); + const quoteTokenAmount = getTokenAmount( + marketPosition.isolatedPositionScaledBalance, + quoteSpotMarket, + SpotBalanceType.DEPOSIT + ); + depositValue = getStrictTokenValue( + quoteTokenAmount, + quoteSpotMarket.decimals, + strictQuote ); - existing.marginRequirementPlusBuffer = - existing.marginRequirementPlusBuffer.add( - perpMarginRequirement.add( - worstCaseLiabilityValue.mul(marginBuffer).div(MARGIN_PRECISION) - ) - ); } - isolatedMarginCalculations.set(market.marketIndex, existing); - numPerpLiabilities += 1; - withPerpIsolatedLiability = withPerpIsolatedLiability || false; // TODO: derive from market tier + calc.addIsolatedMarginCalculation( + market.marketIndex, + depositValue, + positionUnrealizedPnl, + worstCaseLiabilityValue, + perpMarginRequirement + ); + calc.addPerpLiability(); } else { // cross: add to global requirement and collateral - marginRequirement = marginRequirement.add(perpMarginRequirement); - totalCollateral = totalCollateral.add(positionUnrealizedPnl); - numPerpLiabilities += - marketPosition.baseAssetAmount.eq(ZERO) && - marketPosition.openOrders === 0 - ? 0 - : 1; - if (marginBuffer) { - marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( - perpMarginRequirement.add( - worstCaseLiabilityValue.mul(marginBuffer).div(MARGIN_PRECISION) - ) - ); - if (positionUnrealizedPnl.isNeg()) { - totalCollateralBuffer = totalCollateralBuffer.add( - positionUnrealizedPnl.mul(marginBuffer).div(MARGIN_PRECISION) - ); - } + calc.addIsolatedMarginRequirement( + perpMarginRequirement, + worstCaseLiabilityValue, + MarketIdentifier.perp(market.marketIndex) + ); + calc.addIsolatedTotalCollateral(positionUnrealizedPnl); + const hasPerpLiability = + !marketPosition.baseAssetAmount.eq(ZERO) || + marketPosition.quoteAssetAmount.lt(ZERO) || + marketPosition.openOrders !== 0; + if (hasPerpLiability) { + calc.addPerpLiability(); } } } @@ -437,17 +425,17 @@ export class User { strict, marginBuffer: marginBuffer, }, - totalCollateral, - totalCollateralBuffer, - marginRequirement, - marginRequirementPlusBuffer, - isolatedMarginCalculations, - numSpotLiabilities, - numPerpLiabilities, - allDepositOraclesValid, - allLiabilityOraclesValid, - withPerpIsolatedLiability, - withSpotIsolatedLiability, + totalCollateral: calc.totalCollateral, + totalCollateralBuffer: calc.totalCollateralBuffer, + marginRequirement: calc.marginRequirement, + marginRequirementPlusBuffer: calc.marginRequirementPlusBuffer, + isolatedMarginCalculations: calc.isolatedMarginCalculations, + numSpotLiabilities: calc.numSpotLiabilities, + numPerpLiabilities: calc.numPerpLiabilities, + allDepositOraclesValid: calc.allDepositOraclesValid, + allLiabilityOraclesValid: calc.allLiabilityOraclesValid, + withPerpIsolatedLiability: calc.withPerpIsolatedLiability, + withSpotIsolatedLiability: calc.withSpotIsolatedLiability, }; } @@ -664,6 +652,7 @@ export class User { lastQuoteAssetAmountPerLp: ZERO, perLpBase: 0, maxMarginRatio: 0, + positionFlag: 0, }; } @@ -2471,12 +2460,12 @@ export class User { marginCalculation.isolatedMarginCalculations.get(marketIndex); const { totalCollateral, marginRequirement } = isolatedMarginCalculation; - let freeCollateral = BN.max( + const freeCollateral = BN.max( ZERO, totalCollateral.sub(marginRequirement) ).add(offsetCollateral); - let freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp( + const freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp( market, currentPerpPosition, positionBaseSizeChange, @@ -4371,4 +4360,7 @@ export class User { activeSpotPositions: activeSpotMarkets, }; } + private isPerpPositionIsolated(perpPosition: PerpPosition): boolean { + return (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0; + } } diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index 88d203f875..90dfac1df8 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -46,6 +46,7 @@ export const mockPerpPosition: PerpPosition = { lastBaseAssetAmountPerLp: new BN(0), lastQuoteAssetAmountPerLp: new BN(0), perLpBase: 0, + positionFlag: 0, }; export const mockAMM: AMM = { diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts index 9ca9b5e4aa..880276ebe6 100644 --- a/sdk/tests/user/getMarginCalculation.ts +++ b/sdk/tests/user/getMarginCalculation.ts @@ -15,6 +15,7 @@ import { MARGIN_PRECISION, OPEN_ORDER_MARGIN_REQUIREMENT, SPOT_MARKET_WEIGHT_PRECISION, + PositionFlag, } from '../../src'; import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; import { assert } from '../../src/assert/assert'; @@ -320,9 +321,12 @@ describe('getMarginCalculation snapshot', () => { assert(makerCalc.marginRequirement.gt(ZERO)); }); - it('isolated position margin requirement (SDK parity)', async () => { + it.only('isolated position margin requirement (SDK parity)', async () => { const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + myMockSpotMarkets[0].oracle = new PublicKey(2); + myMockSpotMarkets[1].oracle = new PublicKey(5); + myMockPerpMarkets[0].amm.oracle = new PublicKey(5); // Configure perp market 0 ratios to match on-chain test myMockPerpMarkets[0].marginRatioInitial = 1000; // 10% @@ -349,12 +353,16 @@ describe('getMarginCalculation snapshot', () => { // SOL borrow: 100 units crossAccount.spotPositions[1].marketIndex = 1; crossAccount.spotPositions[1].balanceType = SpotBalanceType.BORROW; - crossAccount.spotPositions[1].scaledBalance = new BN(10000).mul( + crossAccount.spotPositions[1].scaledBalance = new BN(100).mul( SPOT_MARKET_BALANCE_PRECISION ); // No perp exposure in cross calc - crossAccount.perpPositions[0].baseAssetAmount = ZERO; - crossAccount.perpPositions[0].quoteAssetAmount = ZERO; + crossAccount.perpPositions[0].baseAssetAmount = new BN(100 * BASE_PRECISION.toNumber()); + crossAccount.perpPositions[0].quoteAssetAmount = new BN(-11000 * QUOTE_PRECISION.toNumber()); + crossAccount.perpPositions[0].positionFlag = PositionFlag.IsolatedPosition; + crossAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(100).mul( + SPOT_MARKET_BALANCE_PRECISION + ); const userCross: User = await makeMockUser( myMockPerpMarkets, @@ -365,6 +373,8 @@ describe('getMarginCalculation snapshot', () => { ); const crossCalc = userCross.getMarginCalculation('Initial'); + // console.log('crossCalc.marginRequirement.toString()', crossCalc.marginRequirement.toString()); + // console.log('crossCalc.totalCollateral.toString()', crossCalc.totalCollateral.toString()); // Expect: cross MR from SOL borrow: 100 * $100 = $10,000 * 1.2 = $12,000 assert(crossCalc.marginRequirement.eq(new BN('12000000000'))); // Expect: cross total collateral from USDC deposit only = $20,000 @@ -377,53 +387,15 @@ describe('getMarginCalculation snapshot', () => { const crossCalcBuf = userCross.getMarginCalculation('Initial', { liquidationBuffer: tenPct, }); - console.log('crossCalcBuf.marginRequirementPlusBuffer', crossCalcBuf.marginRequirementPlusBuffer.toString()); - console.log('crossCalcBuf.totalCollateralBuffer', crossCalcBuf.totalCollateralBuffer.toString()); - assert(crossCalcBuf.marginRequirementPlusBuffer.eq(new BN('13000000000'))); + assert(crossCalcBuf.marginRequirementPlusBuffer.eq(new BN('13000000000'))); // replicate 10% buffer const crossTotalPlusBuffer = crossCalcBuf.totalCollateral.add( crossCalcBuf.totalCollateralBuffer ); assert(crossTotalPlusBuffer.eq(new BN('20000000000'))); - // ---------- Isolated perp position (simulate isolated by separate user) ---------- - const isolatedAccount = _.cloneDeep(baseMockUserAccount); - // Perp: 100 base long, quote -11,000 => PnL = 10k - 11k = -$1,000 - isolatedAccount.perpPositions[0].baseAssetAmount = new BN(100).mul( - BASE_PRECISION - ); - isolatedAccount.perpPositions[0].quoteAssetAmount = new BN(-11000).mul( - QUOTE_PRECISION - ); - // Simulate isolated balance: $100 quote deposit on this user - isolatedAccount.spotPositions[0].marketIndex = 0; - isolatedAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; - isolatedAccount.spotPositions[0].scaledBalance = new BN(100).mul( - SPOT_MARKET_BALANCE_PRECISION - ); - - const userIsolated: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - isolatedAccount, - [100, 1, 1, 1, 1, 1, 1, 1], - [1, 100, 1, 1, 1, 1, 1, 1] - ); - - const isoCalc = userIsolated.getMarginCalculation('Initial'); - // Expect: perp initial MR = 10% * $10,000 = $1,000 - assert(isoCalc.marginRequirement.eq(new BN('1000000000'))); - // Expect: total collateral = $100 (deposit) + (-$1,000) (PnL) = -$900 - assert(isoCalc.totalCollateral.eq(new BN('-900000000'))); - assert(isoCalc.marginRequirement.gt(isoCalc.totalCollateral)); - - const isoCalcBuf = userIsolated.getMarginCalculation('Initial', { - liquidationBuffer: tenPct, - }); - assert(isoCalcBuf.marginRequirementPlusBuffer.eq(new BN('2000000000'))); - const isoTotalPlusBuffer = isoCalcBuf.totalCollateral.add( - isoCalcBuf.totalCollateralBuffer - ); - assert(isoTotalPlusBuffer.eq(new BN('-1000000000'))); + const isoPosition = crossCalcBuf.isolatedMarginCalculations.get(0); + assert(isoPosition?.marginRequirementPlusBuffer.eq(new BN('2000000000'))); + assert(isoPosition?.totalCollateralBuffer.add(isoPosition?.totalCollateral).eq(new BN('-1000000000'))); }); }); diff --git a/sdk/tests/user/test.ts b/sdk/tests/user/test.ts index 6c431b7226..990abc473e 100644 --- a/sdk/tests/user/test.ts +++ b/sdk/tests/user/test.ts @@ -49,7 +49,6 @@ async function makeMockUser( oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = spotOraclePriceList[i]; } - // console.log(oraclePriceMap); function getMockUserAccount(): UserAccount { return myMockUserAccount; @@ -61,12 +60,6 @@ async function makeMockUser( return myMockSpotMarkets[marketIndex]; } function getMockOracle(oracleKey: PublicKey) { - // console.log('oracleKey.toString():', oracleKey.toString()); - // console.log( - // 'oraclePriceMap[oracleKey.toString()]:', - // oraclePriceMap[oracleKey.toString()] - // ); - const QUOTE_ORACLE_PRICE_DATA: OraclePriceData = { price: new BN( oraclePriceMap[oracleKey.toString()] * PRICE_PRECISION.toNumber() From 3606bb431e8ab3e3c2612aa3b2478054cc8850c3 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Thu, 28 Aug 2025 13:57:55 -0600 Subject: [PATCH 135/247] fix: PR feedback and cleanup + decoding position flag wrong --- sdk/src/decode/user.ts | 4 +- sdk/src/marginCalculation.ts | 55 ++------------------------ sdk/src/types.ts | 5 +++ sdk/src/user.ts | 26 +++++------- sdk/tests/user/getMarginCalculation.ts | 25 +++++------- 5 files changed, 30 insertions(+), 85 deletions(-) diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index c3022d06aa..4c35b4f6ed 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -84,6 +84,7 @@ export function decodeUser(buffer: Buffer): UserAccount { const quoteAssetAmount = readSignedBigInt64LE(buffer, offset + 16); const lpShares = readUnsignedBigInt64LE(buffer, offset + 64); const openOrders = buffer.readUInt8(offset + 94); + const positionFlag = buffer.readUInt8(offset + 95); if ( baseAssetAmount.eq(ZERO) && @@ -117,9 +118,6 @@ export function decodeUser(buffer: Buffer): UserAccount { offset += 3; const perLpBase = buffer.readUInt8(offset); offset += 1; - // TODO: verify this works - const positionFlag = buffer.readUInt8(offset); - offset += 1; perpPositions.push({ lastCumulativeFundingRate, baseAssetAmount, diff --git a/sdk/src/marginCalculation.ts b/sdk/src/marginCalculation.ts index 2cb1dbe839..d5507d9eef 100644 --- a/sdk/src/marginCalculation.ts +++ b/sdk/src/marginCalculation.ts @@ -1,4 +1,4 @@ -import { BN } from './'; +import { BN } from '@coral-xyz/anchor'; import { MARGIN_PRECISION } from './constants/numericConstants'; import { MarketType } from './types'; @@ -6,7 +6,7 @@ export type MarginCategory = 'Initial' | 'Maintenance' | 'Fill'; export type MarginCalculationMode = | { type: 'Standard' } - | { type: 'Liquidation'; marketToTrackMarginRequirement?: MarketIdentifier }; + | { type: 'Liquidation' }; export class MarketIdentifier { marketType: MarketType; @@ -40,10 +40,6 @@ export class MarginContext { strict: boolean; ignoreInvalidDepositOracles: boolean; marginBuffer: BN; // scaled by MARGIN_PRECISION - fuelBonusNumerator: BN; // seconds since last update - fuelBonus: BN; // not used in calculation aggregation here - fuelPerpDelta?: { marketIndex: number; delta: BN }; - fuelSpotDeltas: Array<{ marketIndex: number; delta: BN }>; // up to 2 in rust marginRatioOverride?: number; private constructor(marginType: MarginCategory) { @@ -52,9 +48,6 @@ export class MarginContext { this.strict = false; this.ignoreInvalidDepositOracles = false; this.marginBuffer = new BN(0); - this.fuelBonusNumerator = new BN(0); - this.fuelBonus = new BN(0); - this.fuelSpotDeltas = []; } static standard(marginType: MarginCategory): MarginContext { @@ -83,26 +76,6 @@ export class MarginContext { return this; } - setFuelPerpDelta(marketIndex: number, delta: BN): this { - this.fuelPerpDelta = { marketIndex, delta }; - return this; - } - - setFuelSpotDelta(marketIndex: number, delta: BN): this { - this.fuelSpotDeltas = [{ marketIndex, delta }]; - return this; - } - - setFuelSpotDeltas(deltas: Array<{ marketIndex: number; delta: BN }>): this { - this.fuelSpotDeltas = deltas; - return this; - } - - setFuelNumerator(numerator: BN): this { - this.fuelBonusNumerator = numerator ?? new BN(0); - return this; - } - setMarginRatioOverride(ratio: number): this { this.marginRatioOverride = ratio; return this; @@ -114,7 +87,6 @@ export class MarginContext { 'InvalidMarginCalculation: Cant track market outside of liquidation mode' ); } - this.mode.marketToTrackMarginRequirement = marketIdentifier; return this; } } @@ -191,7 +163,7 @@ export class MarginCalculation { this.fuelPositions = 0; } - addIsolatedTotalCollateral(delta: BN): void { + addCrossMarginTotalCollateral(delta: BN): void { this.totalCollateral = this.totalCollateral.add(delta); if (this.context.marginBuffer.gt(new BN(0)) && delta.isNeg()) { this.totalCollateralBuffer = this.totalCollateralBuffer.add( @@ -200,10 +172,9 @@ export class MarginCalculation { } } - addIsolatedMarginRequirement( + addCrossMarginRequirement( marginRequirement: BN, liabilityValue: BN, - marketIdentifier: MarketIdentifier ): void { this.marginRequirement = this.marginRequirement.add(marginRequirement); if (this.context.marginBuffer.gt(new BN(0))) { @@ -213,11 +184,6 @@ export class MarginCalculation { ) ); } - const tracked = this.marketToTrackMarginRequirement(); - if (tracked && tracked.equals(marketIdentifier)) { - this.trackedMarketMarginRequirement = - this.trackedMarketMarginRequirement.add(marginRequirement); - } } addIsolatedMarginCalculation( @@ -245,12 +211,6 @@ export class MarginCalculation { iso.totalCollateralBuffer = totalCollateralBuffer; iso.marginRequirementPlusBuffer = marginRequirementPlusBuffer; this.isolatedMarginCalculations.set(marketIndex, iso); - - const tracked = this.marketToTrackMarginRequirement(); - if (tracked && tracked.equals(MarketIdentifier.perp(marketIndex))) { - this.trackedMarketMarginRequirement = - this.trackedMarketMarginRequirement.add(marginRequirement); - } } addSpotLiability(): void { @@ -341,11 +301,4 @@ export class MarginCalculation { hasIsolatedMarginCalculation(marketIndex: number): boolean { return this.isolatedMarginCalculations.has(marketIndex); } - - private marketToTrackMarginRequirement(): MarketIdentifier | undefined { - if (this.context.mode.type === 'Liquidation') { - return this.context.mode.marketToTrackMarginRequirement; - } - return undefined; - } } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index c161e724a5..cce95639db 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -504,6 +504,7 @@ export type LiquidationRecord = { liquidatePerpPnlForDeposit: LiquidatePerpPnlForDepositRecord; perpBankruptcy: PerpBankruptcyRecord; spotBankruptcy: SpotBankruptcyRecord; + bitFlags: number; }; export class LiquidationType { @@ -582,6 +583,10 @@ export type SpotBankruptcyRecord = { ifPayment: BN; }; +export class LiquidationBitFlag { + static readonly IsolatedPosition = 1; +} + export type SettlePnlRecord = { ts: BN; user: PublicKey; diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 8aa3098539..7451d09122 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -110,7 +110,6 @@ import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber' import { MarginCalculation as JsMarginCalculation, MarginContext, - MarketIdentifier, IsolatedMarginCalculation, } from './marginCalculation'; @@ -215,7 +214,7 @@ export class User { spotMarket.decimals, strictOracle ); - calc.addIsolatedTotalCollateral(tokenValue); + calc.addCrossMarginTotalCollateral(tokenValue); } else { // borrow on quote contributes to margin requirement const tokenValueAbs = getStrictTokenValue( @@ -223,10 +222,9 @@ export class User { spotMarket.decimals, strictOracle ).abs(); - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( tokenValueAbs, tokenValueAbs, - MarketIdentifier.spot(0) ); calc.addSpotLiability(); } @@ -249,23 +247,21 @@ export class User { // open order IM if (includeOpenOrders) { - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), ZERO, - MarketIdentifier.spot(spotPosition.marketIndex) ); } if (worstCaseTokenAmount.gt(ZERO)) { // asset side increases total collateral (weighted) - calc.addIsolatedTotalCollateral(worstCaseWeightedTokenValue); + calc.addCrossMarginTotalCollateral(worstCaseWeightedTokenValue); } else if (worstCaseTokenAmount.lt(ZERO)) { // liability side increases margin requirement (weighted >= abs(token_value)) const liabilityWeighted = worstCaseWeightedTokenValue.abs(); - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( liabilityWeighted, worstCaseTokenValue.abs(), - MarketIdentifier.spot(spotPosition.marketIndex) ); calc.addSpotLiability(); } else if (spotPosition.openOrders !== 0) { @@ -274,13 +270,12 @@ export class User { // orders value contributes to collateral or requirement if (worstCaseOrdersValue.gt(ZERO)) { - calc.addIsolatedTotalCollateral(worstCaseOrdersValue); + calc.addCrossMarginTotalCollateral(worstCaseOrdersValue); } else if (worstCaseOrdersValue.lt(ZERO)) { const absVal = worstCaseOrdersValue.abs(); - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( absVal, absVal, - MarketIdentifier.spot(0) ); } } @@ -403,12 +398,11 @@ export class User { calc.addPerpLiability(); } else { // cross: add to global requirement and collateral - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( perpMarginRequirement, worstCaseLiabilityValue, - MarketIdentifier.perp(market.marketIndex) ); - calc.addIsolatedTotalCollateral(positionUnrealizedPnl); + calc.addCrossMarginTotalCollateral(positionUnrealizedPnl); const hasPerpLiability = !marketPosition.baseAssetAmount.eq(ZERO) || marketPosition.quoteAssetAmount.lt(ZERO) || @@ -1951,7 +1945,7 @@ export class User { return { perpLiabilityValue: perpLiability, perpPnl: positionUnrealizedPnl, - spotAssetValue: ZERO, + spotAssetValue: perpPosition.isolatedPositionScaledBalance, spotLiabilityValue: ZERO, }; } diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts index 880276ebe6..5e66fa0887 100644 --- a/sdk/tests/user/getMarginCalculation.ts +++ b/sdk/tests/user/getMarginCalculation.ts @@ -148,7 +148,7 @@ describe('getMarginCalculation snapshot', () => { [1, 1, 1, 1, 1, 1, 1, 1] ); - const tenPercent = MARGIN_PRECISION.divn(10); + const tenPercent = new BN(1000); const calc = user.getMarginCalculation('Initial', { liquidationBuffer: tenPercent, }); @@ -157,7 +157,7 @@ describe('getMarginCalculation snapshot', () => { assert(calc.marginRequirement.eq(liability)); assert( calc.marginRequirementPlusBuffer.eq( - liability.mul(tenPercent).div(MARGIN_PRECISION) + liability.div(new BN(10)).add(calc.marginRequirement) // 10% of liability + margin requirement ) ); assert(calc.numSpotLiabilities === 1); @@ -211,27 +211,21 @@ describe('getMarginCalculation snapshot', () => { assert(calc.marginRequirement.eq(new BN('2000000'))); }); - it.skip('maker position reducing: collateral equals maintenance requirement', async () => { + it.only('maker position reducing: collateral equals maintenance requirement', async () => { const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); const myMockUserAccount = _.cloneDeep(baseMockUserAccount); // Perp exposure: 20 base notional at oracle price 1 → maintenance MR = 10% of $20 = $2 - myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(2).mul( BASE_PRECISION ); - // Set entry/breakeven at $1 so unrealized PnL = $0 - myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(-20).mul( + myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(-20000000000).mul( QUOTE_PRECISION ); - myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-20).mul( + myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-20000000000).mul( QUOTE_PRECISION ); - // Provide exactly $2 in quote collateral - myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; - myMockUserAccount.spotPositions[0].scaledBalance = new BN(2).mul( - SPOT_MARKET_BALANCE_PRECISION - ); const user: User = await makeMockUser( myMockPerpMarkets, @@ -321,7 +315,7 @@ describe('getMarginCalculation snapshot', () => { assert(makerCalc.marginRequirement.gt(ZERO)); }); - it.only('isolated position margin requirement (SDK parity)', async () => { + it('isolated position margin requirement (SDK parity)', async () => { const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); myMockSpotMarkets[0].oracle = new PublicKey(2); @@ -373,8 +367,7 @@ describe('getMarginCalculation snapshot', () => { ); const crossCalc = userCross.getMarginCalculation('Initial'); - // console.log('crossCalc.marginRequirement.toString()', crossCalc.marginRequirement.toString()); - // console.log('crossCalc.totalCollateral.toString()', crossCalc.totalCollateral.toString()); + const isolatedMarginCalc = crossCalc.isolatedMarginCalculations.get(0); // Expect: cross MR from SOL borrow: 100 * $100 = $10,000 * 1.2 = $12,000 assert(crossCalc.marginRequirement.eq(new BN('12000000000'))); // Expect: cross total collateral from USDC deposit only = $20,000 @@ -382,6 +375,8 @@ describe('getMarginCalculation snapshot', () => { // Meets cross margin requirement assert(crossCalc.marginRequirement.lte(crossCalc.totalCollateral)); + assert(isolatedMarginCalc?.marginRequirement.eq(new BN('1000000000'))); + assert(isolatedMarginCalc?.totalCollateral.eq(new BN('-900000000'))); // With 10% buffer const tenPct = new BN(1000); const crossCalcBuf = userCross.getMarginCalculation('Initial', { From 99c826895cd8c80166708612b016a4b6fbb8e9d6 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Fri, 29 Aug 2025 23:25:47 -0600 Subject: [PATCH 136/247] feat: deposit into iso position ixs --- sdk/src/driftClient.ts | 177 +++++++++++++++++++++---- sdk/src/math/position.ts | 1 - sdk/src/types.ts | 4 + sdk/tests/user/getMarginCalculation.ts | 8 +- 4 files changed, 156 insertions(+), 34 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index abff8433df..b7ed95003c 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -57,7 +57,6 @@ import { StateAccount, SwapReduceOnly, SignedMsgOrderParamsMessage, - TakerInfo, TxParams, UserAccount, UserStatsAccount, @@ -191,6 +190,8 @@ import nacl from 'tweetnacl'; import { Slothash } from './slot/SlothashSubscriber'; import { getOracleId } from './oracles/oracleId'; import { SignedMsgOrderParams } from './types'; +import { TakerInfo } from './types'; +// BN is already imported globally in this file via other imports import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; import { Commitment } from 'gill'; @@ -263,6 +264,69 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } + public async getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex, + amount, + spotMarketIndex, + subAccountId, + userTokenAccount, + }: { + perpMarketIndex: number; + amount: BN; + spotMarketIndex?: number; // defaults to perp.quoteSpotMarketIndex + subAccountId?: number; + userTokenAccount?: PublicKey; // defaults ATA for spot market mint + }): Promise { + const user = await this.getUserAccountPublicKey(subAccountId); + const userStats = this.getUserStatsAccountPublicKey(); + const statePk = await this.getStatePublicKey(); + const perp = this.getPerpMarketAccount(perpMarketIndex); + const spotIndex = spotMarketIndex ?? perp.quoteSpotMarketIndex; + const spot = this.getSpotMarketAccount(spotIndex); + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(subAccountId)], + readablePerpMarketIndex: perpMarketIndex, + writableSpotMarketIndexes: [spotIndex], + }); + + // token program and transfer hook mints need to be present for deposit + this.addTokenMintToRemainingAccounts(spot, remainingAccounts); + if (this.isTransferHook(spot)) { + await this.addExtraAccountMetasToRemainingAccounts( + spot.mint, + remainingAccounts + ); + } + + const tokenProgram = this.getTokenProgramForSpotMarket(spot); + const ata = + userTokenAccount ?? + (await this.getAssociatedTokenAccount( + spotIndex, + false, + tokenProgram + )); + + return await this.program.instruction.depositIntoIsolatedPerpPosition( + spotIndex, + perpMarketIndex, + amount, + { + accounts: { + state: statePk, + user, + userStats, + authority: this.wallet.publicKey, + spotMarketVault: spot.vault, + userTokenAccount: ata, + tokenProgram, + }, + remainingAccounts, + } + ); + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -733,7 +797,6 @@ export class DriftClient { return lookupTableAccount; } - public async fetchAllLookupTableAccounts(): Promise< AddressLookupTableAccount[] > { @@ -1511,7 +1574,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getUpdateUserCustomMarginRatioIx( marginRatio: number, subAccountId = 0 @@ -2290,7 +2352,6 @@ export class DriftClient { this.mustIncludeSpotMarketIndexes.add(spotMarketIndex); }); } - getRemainingAccounts(params: RemainingAccountParams): AccountMeta[] { const { oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap } = this.getRemainingAccountMapsForUsers(params.userAccounts); @@ -3124,7 +3185,6 @@ export class DriftClient { userAccountPublicKey, }; } - public async createInitializeUserAccountAndDepositCollateral( amount: BN, userTokenAccount: PublicKey, @@ -4109,7 +4169,6 @@ export class DriftClient { } ); } - public async getRemovePerpLpSharesIx( marketIndex: number, sharesToBurn?: BN, @@ -4265,7 +4324,8 @@ export class DriftClient { bracketOrdersParams = new Array(), referrerInfo?: ReferrerInfo, cancelExistingOrders?: boolean, - settlePnl?: boolean + settlePnl?: boolean, + isolatedPositionDepositAmount?: BN ): Promise<{ cancelExistingOrdersTx?: Transaction | VersionedTransaction; settlePnlTx?: Transaction | VersionedTransaction; @@ -4290,10 +4350,32 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - ixPromisesForTxs.marketOrderTx = this.getPlaceOrdersIx( - [orderParams, ...bracketOrdersParams], - userAccount.subAccountId - ); + + const preIxs: TransactionInstruction[] = []; + if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + preIxs.push( + await this.getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex: orderParams.marketIndex, + amount: isolatedPositionDepositAmount as BN, + subAccountId: userAccount.subAccountId, + }) + ); + } + + + ixPromisesForTxs.marketOrderTx = (async () => { + const placeOrdersIx = await this.getPlaceOrdersIx( + [orderParams, ...bracketOrdersParams], + userAccount.subAccountId + ); + if (preIxs.length) { + return [ + ...preIxs, + placeOrdersIx, + ] as unknown as TransactionInstruction; + } + return placeOrdersIx; + })(); /* Cancel open orders in market if requested */ if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { @@ -4408,12 +4490,29 @@ export class DriftClient { public async placePerpOrder( orderParams: OptionalOrderParams, txParams?: TxParams, - subAccountId?: number + subAccountId?: number, + isolatedPositionDepositAmount?: BN ): Promise { + const preIxs: TransactionInstruction[] = []; + if (isolatedPositionDepositAmount?.gt?.(ZERO)) { + preIxs.push( + await this.getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex: orderParams.marketIndex, + amount: isolatedPositionDepositAmount as BN, + subAccountId, + }) + ); + } + const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlacePerpOrderIx(orderParams, subAccountId), - txParams + txParams, + undefined, + undefined, + undefined, + undefined, + preIxs ), [], this.opts @@ -4785,6 +4884,7 @@ export class DriftClient { useMarketLastSlotCache: true, }); + return await this.program.instruction.cancelOrders( marketType ?? null, marketIndex ?? null, @@ -4828,7 +4928,8 @@ export class DriftClient { params: OrderParams[], txParams?: TxParams, subAccountId?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ): Promise { const { txSig } = await this.sendTransaction( ( @@ -4850,10 +4951,25 @@ export class DriftClient { params: OrderParams[], txParams?: TxParams, subAccountId?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ) { const lookupTableAccounts = await this.fetchAllLookupTableAccounts(); + const preIxs: TransactionInstruction[] = []; + if (params?.length === 1) { + const p = params[0]; + if (isVariant(p.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + preIxs.push( + await this.getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex: p.marketIndex, + amount: isolatedPositionDepositAmount as BN, + subAccountId, + }) + ); + } + } + const tx = await this.buildTransaction( await this.getPlaceOrdersIx(params, subAccountId), txParams, @@ -4861,14 +4977,13 @@ export class DriftClient { lookupTableAccounts, undefined, undefined, - optionalIxs + [...preIxs, ...(optionalIxs ?? [])] ); return { placeOrdersTx: tx, }; } - public async getPlaceOrdersIx( params: OptionalOrderParams[], subAccountId?: number @@ -5606,7 +5721,6 @@ export class DriftClient { return txSig; } - public async getJupiterSwapIxV6({ jupiterClient, outMarketIndex, @@ -6276,7 +6390,6 @@ export class DriftClient { this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); return txSig; } - public async preparePlaceAndTakePerpOrderWithAdditionalOrders( orderParams: OptionalOrderParams, makerInfo?: MakerInfo | MakerInfo[], @@ -6288,7 +6401,8 @@ export class DriftClient { settlePnl?: boolean, exitEarlyIfSimFails?: boolean, auctionDurationPercentage?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ): Promise<{ placeAndTakeTx: Transaction | VersionedTransaction; cancelExistingOrdersTx: Transaction | VersionedTransaction; @@ -6322,6 +6436,16 @@ export class DriftClient { subAccountId ); + if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + placeAndTakeIxs.push( + await this.getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex: orderParams.marketIndex, + amount: isolatedPositionDepositAmount as BN, + subAccountId, + }) + ); + } + placeAndTakeIxs.push(placeAndTakeIx); if (bracketOrdersParams.length > 0) { @@ -6332,6 +6456,11 @@ export class DriftClient { placeAndTakeIxs.push(bracketOrdersIx); } + // Optional extra ixs can be appended at the front + if (optionalIxs?.length) { + placeAndTakeIxs.unshift(...optionalIxs); + } + const shouldUseSimulationComputeUnits = txParams?.useSimulatedComputeUnits; const shouldExitIfSimulationFails = exitEarlyIfSimFails; @@ -7064,7 +7193,6 @@ export class DriftClient { this.spotMarketLastSlotCache.set(QUOTE_SPOT_MARKET_INDEX, slot); return txSig; } - public async getPlaceAndTakeSpotOrderIx( orderParams: OptionalOrderParams, fulfillmentConfig?: SerumV3FulfillmentConfigAccount, @@ -7517,7 +7645,6 @@ export class DriftClient { bitFlags?: number; policy?: ModifyOrderPolicy; maxTs?: BN; - txParams?: TxParams; }, subAccountId?: number ): Promise { @@ -7859,7 +7986,6 @@ export class DriftClient { this.perpMarketLastSlotCache.set(marketIndex, slot); return txSig; } - public async getLiquidatePerpIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, @@ -8650,7 +8776,6 @@ export class DriftClient { } ); } - public async resolveSpotBankruptcy( userAccountPublicKey: PublicKey, userAccount: UserAccount, @@ -9482,7 +9607,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getSettleRevenueToInsuranceFundIx( spotMarketIndex: number ): Promise { @@ -10277,7 +10401,6 @@ export class DriftClient { ); return config as ProtectedMakerModeConfig; } - public async updateUserProtectedMakerOrders( subAccountId: number, protectedOrders: boolean, @@ -10601,4 +10724,4 @@ export class DriftClient { forceVersionedTransaction, }); } -} +} \ No newline at end of file diff --git a/sdk/src/math/position.ts b/sdk/src/math/position.ts index 3db5007a20..0cae66ab05 100644 --- a/sdk/src/math/position.ts +++ b/sdk/src/math/position.ts @@ -127,7 +127,6 @@ export function calculatePositionPNL( if (withFunding) { const fundingRatePnL = calculateUnsettledFundingPnl(market, perpPosition); - pnl = pnl.add(fundingRatePnL); } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index cce95639db..9d935099a9 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1286,6 +1286,10 @@ export type OptionalOrderParams = { [Property in keyof OrderParams]?: OrderParams[Property]; } & NecessaryOrderParams; +export type PerpOrderIsolatedExtras = { + isolatedPositionDepositAmount?: BN; +}; + export type ModifyOrderParams = { [Property in keyof OrderParams]?: OrderParams[Property] | null; } & { policy?: ModifyOrderPolicy }; diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts index 5e66fa0887..afc2111996 100644 --- a/sdk/tests/user/getMarginCalculation.ts +++ b/sdk/tests/user/getMarginCalculation.ts @@ -216,14 +216,10 @@ describe('getMarginCalculation snapshot', () => { const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - // Perp exposure: 20 base notional at oracle price 1 → maintenance MR = 10% of $20 = $2 - myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(2).mul( + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(200000000).mul( BASE_PRECISION ); - myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(-20000000000).mul( - QUOTE_PRECISION - ); - myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-20000000000).mul( + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-180000000).mul( QUOTE_PRECISION ); From 88d7b76917bfc0a0164d7aff91b378abf7a31903 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 11:27:54 -0600 Subject: [PATCH 137/247] temp: pr feedback nother round --- sdk/src/decode/user.ts | 6 +- sdk/src/marginCalculation.ts | 19 ++++- sdk/src/math/margin.ts | 14 +++- sdk/src/math/spotPosition.ts | 5 +- sdk/src/types.ts | 2 +- sdk/src/user.ts | 151 +++++++++++++++-------------------- 6 files changed, 103 insertions(+), 94 deletions(-) diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index 4c35b4f6ed..b3b2e2fca3 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -85,6 +85,8 @@ export function decodeUser(buffer: Buffer): UserAccount { const lpShares = readUnsignedBigInt64LE(buffer, offset + 64); const openOrders = buffer.readUInt8(offset + 94); const positionFlag = buffer.readUInt8(offset + 95); + const isolatedPositionScaledBalance = readUnsignedBigInt64LE(buffer, offset + 96); + const customMarginRatio = buffer.readUInt32LE(offset + 97); if ( baseAssetAmount.eq(ZERO) && @@ -136,7 +138,9 @@ export function decodeUser(buffer: Buffer): UserAccount { perLpBase, maxMarginRatio, positionFlag, - }); + isolatedPositionScaledBalance, + customMarginRatio, + }); } const orders: Order[] = []; diff --git a/sdk/src/marginCalculation.ts b/sdk/src/marginCalculation.ts index d5507d9eef..fe9fa8d7eb 100644 --- a/sdk/src/marginCalculation.ts +++ b/sdk/src/marginCalculation.ts @@ -139,6 +139,8 @@ export class MarginCalculation { allLiabilityOraclesValid: boolean; withPerpIsolatedLiability: boolean; withSpotIsolatedLiability: boolean; + totalSpotLiabilityValue: BN; + totalPerpLiabilityValue: BN; trackedMarketMarginRequirement: BN; fuelDeposits: number; fuelBorrows: number; @@ -157,6 +159,8 @@ export class MarginCalculation { this.allLiabilityOraclesValid = true; this.withPerpIsolatedLiability = false; this.withSpotIsolatedLiability = false; + this.totalSpotLiabilityValue = new BN(0); + this.totalPerpLiabilityValue = new BN(0); this.trackedMarketMarginRequirement = new BN(0); this.fuelDeposits = 0; this.fuelBorrows = 0; @@ -172,10 +176,7 @@ export class MarginCalculation { } } - addCrossMarginRequirement( - marginRequirement: BN, - liabilityValue: BN, - ): void { + addCrossMarginRequirement(marginRequirement: BN, liabilityValue: BN): void { this.marginRequirement = this.marginRequirement.add(marginRequirement); if (this.context.marginBuffer.gt(new BN(0))) { this.marginRequirementPlusBuffer = this.marginRequirementPlusBuffer.add( @@ -221,6 +222,16 @@ export class MarginCalculation { this.numPerpLiabilities += 1; } + addSpotLiabilityValue(spotLiabilityValue: BN): void { + this.totalSpotLiabilityValue = + this.totalSpotLiabilityValue.add(spotLiabilityValue); + } + + addPerpLiabilityValue(perpLiabilityValue: BN): void { + this.totalPerpLiabilityValue = + this.totalPerpLiabilityValue.add(perpLiabilityValue); + } + updateAllDepositOraclesValid(valid: boolean): void { this.allDepositOraclesValid = this.allDepositOraclesValid && valid; } diff --git a/sdk/src/math/margin.ts b/sdk/src/math/margin.ts index 63fada436b..d67a5e2b67 100644 --- a/sdk/src/math/margin.ts +++ b/sdk/src/math/margin.ts @@ -160,8 +160,20 @@ export function calculateWorstCaseBaseAssetAmount( export function calculateWorstCasePerpLiabilityValue( perpPosition: PerpPosition, perpMarket: PerpMarketAccount, - oraclePrice: BN + oraclePrice: BN, + includeOpenOrders: boolean = true ): { worstCaseBaseAssetAmount: BN; worstCaseLiabilityValue: BN } { + // return early if no open orders required + if (!includeOpenOrders) { + return { + worstCaseBaseAssetAmount: perpPosition.baseAssetAmount, + worstCaseLiabilityValue: calculatePerpLiabilityValue( + perpPosition.baseAssetAmount, + oraclePrice, + isVariant(perpMarket.contractType, 'prediction') + ), + }; + } const allBids = perpPosition.baseAssetAmount.add(perpPosition.openBids); const allAsks = perpPosition.baseAssetAmount.add(perpPosition.openAsks); diff --git a/sdk/src/math/spotPosition.ts b/sdk/src/math/spotPosition.ts index 61eae83ec1..ac5c6d7611 100644 --- a/sdk/src/math/spotPosition.ts +++ b/sdk/src/math/spotPosition.ts @@ -33,7 +33,8 @@ export function getWorstCaseTokenAmounts( spotMarketAccount: SpotMarketAccount, strictOraclePrice: StrictOraclePrice, marginCategory: MarginCategory, - customMarginRatio?: number + customMarginRatio?: number, + includeOpenOrders: boolean = true ): OrderFillSimulation { const tokenAmount = getSignedTokenAmount( getTokenAmount( @@ -50,7 +51,7 @@ export function getWorstCaseTokenAmounts( strictOraclePrice ); - if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO)) { + if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO) || !includeOpenOrders) { const { weight, weightedTokenValue } = calculateWeightedTokenValue( tokenAmount, tokenValue, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 9d935099a9..df9596f9fd 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1115,8 +1115,8 @@ export type PerpPosition = { lastBaseAssetAmountPerLp: BN; lastQuoteAssetAmountPerLp: BN; perLpBase: number; - isolatedPositionScaledBalance: BN; positionFlag: number; + isolatedPositionScaledBalance: BN; }; export type UserStatsAccount = { diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 7451d09122..d0223a3c91 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -110,23 +110,10 @@ import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber' import { MarginCalculation as JsMarginCalculation, MarginContext, - IsolatedMarginCalculation, } from './marginCalculation'; -export type UserMarginCalculation = { - context: { marginType: MarginCategory; strict: boolean; marginBuffer?: BN }; - totalCollateral: BN; - totalCollateralBuffer: BN; - marginRequirement: BN; - marginRequirementPlusBuffer: BN; - isolatedMarginCalculations: Map; - numSpotLiabilities: number; - numPerpLiabilities: number; - allDepositOraclesValid: boolean; - allLiabilityOraclesValid: boolean; - withPerpIsolatedLiability: boolean; - withSpotIsolatedLiability: boolean; -}; +// Backwards compatibility: alias SDK MarginCalculation shape +export type UserMarginCalculation = JsMarginCalculation; export type MarginType = 'Cross' | 'Isolated'; @@ -145,6 +132,8 @@ export class User { * Compute a consolidated margin snapshot once, without caching. * Consumers can use this to avoid duplicating work across separate calls. */ + // TODO: need another param to tell it give it back leverage compnents + // TODO: change get leverage functions need to pull the right values from public getMarginCalculation( marginCategory: MarginCategory = 'Initial', opts?: { @@ -154,7 +143,7 @@ export class User { liquidationBuffer?: BN; // margin_buffer analog for buffer mode marginRatioOverride?: number; // mirrors context.margin_ratio_override } - ): UserMarginCalculation { + ): JsMarginCalculation { const strict = opts?.strict ?? false; const enteringHighLeverage = opts?.enteringHighLeverage ?? false; const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? @@ -179,6 +168,7 @@ export class User { const calc = new JsMarginCalculation(ctx); // SPOT POSITIONS + // TODO: include open orders in the worst-case simulation in the same way on both spot and perp positions for (const spotPosition of this.getUserAccount().spotPositions) { if (isSpotPositionAvailable(spotPosition)) continue; @@ -222,10 +212,7 @@ export class User { spotMarket.decimals, strictOracle ).abs(); - calc.addCrossMarginRequirement( - tokenValueAbs, - tokenValueAbs, - ); + calc.addCrossMarginRequirement(tokenValueAbs, tokenValueAbs); calc.addSpotLiability(); } continue; @@ -242,16 +229,15 @@ export class User { spotMarket, strictOracle, marginCategory, - userCustomMarginRatio + userCustomMarginRatio, + includeOpenOrders ); // open order IM - if (includeOpenOrders) { - calc.addCrossMarginRequirement( - new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), - ZERO, - ); - } + calc.addCrossMarginRequirement( + new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), + ZERO + ); if (worstCaseTokenAmount.gt(ZERO)) { // asset side increases total collateral (weighted) @@ -261,11 +247,13 @@ export class User { const liabilityWeighted = worstCaseWeightedTokenValue.abs(); calc.addCrossMarginRequirement( liabilityWeighted, - worstCaseTokenValue.abs(), + worstCaseTokenValue.abs() ); calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); } else if (spotPosition.openOrders !== 0) { calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); } // orders value contributes to collateral or requirement @@ -273,10 +261,7 @@ export class User { calc.addCrossMarginTotalCollateral(worstCaseOrdersValue); } else if (worstCaseOrdersValue.lt(ZERO)) { const absVal = worstCaseOrdersValue.abs(); - calc.addCrossMarginRequirement( - absVal, - absVal, - ); + calc.addCrossMarginRequirement(absVal, absVal); } } @@ -300,7 +285,8 @@ export class User { calculateWorstCasePerpLiabilityValue( marketPosition, market, - oraclePriceData.price + oraclePriceData.price, + includeOpenOrders ); // margin ratio for this perp @@ -364,7 +350,7 @@ export class User { if (isIsolated) { // derive isolated quote deposit value, mirroring on-chain logic let depositValue = ZERO; - if (marketPosition.isolatedPositionScaledBalance) { + if (marketPosition.isolatedPositionScaledBalance?.gt(ZERO)) { const quoteSpotMarket = this.driftClient.getSpotMarketAccount( market.quoteSpotMarketIndex ); @@ -396,11 +382,12 @@ export class User { perpMarginRequirement ); calc.addPerpLiability(); + calc.addPerpLiabilityValue(worstCaseLiabilityValue); } else { // cross: add to global requirement and collateral calc.addCrossMarginRequirement( perpMarginRequirement, - worstCaseLiabilityValue, + worstCaseLiabilityValue ); calc.addCrossMarginTotalCollateral(positionUnrealizedPnl); const hasPerpLiability = @@ -413,24 +400,7 @@ export class User { } } - return { - context: { - marginType: marginCategory, - strict, - marginBuffer: marginBuffer, - }, - totalCollateral: calc.totalCollateral, - totalCollateralBuffer: calc.totalCollateralBuffer, - marginRequirement: calc.marginRequirement, - marginRequirementPlusBuffer: calc.marginRequirementPlusBuffer, - isolatedMarginCalculations: calc.isolatedMarginCalculations, - numSpotLiabilities: calc.numSpotLiabilities, - numPerpLiabilities: calc.numPerpLiabilities, - allDepositOraclesValid: calc.allDepositOraclesValid, - allLiabilityOraclesValid: calc.allLiabilityOraclesValid, - withPerpIsolatedLiability: calc.withPerpIsolatedLiability, - withSpotIsolatedLiability: calc.withSpotIsolatedLiability, - }; + return calc; } public set isSubscribed(val: boolean) { @@ -647,6 +617,8 @@ export class User { perLpBase: 0, maxMarginRatio: 0, positionFlag: 0, + isolatedPositionScaledBalance: ZERO, + customMarginRatio: 0, }; } @@ -840,18 +812,15 @@ export class User { enterHighLeverageMode = false, perpMarketIndex?: number ): BN { - const totalCollateral = this.getTotalCollateral(marginCategory, true); - const marginRequirement = this.getMarginRequirement( - marginCategory, - undefined, - true, - true, // includeOpenOrders default - enterHighLeverageMode, - perpMarketIndex ? 'Isolated' : undefined, - perpMarketIndex - ); - const freeCollateral = totalCollateral.sub(marginRequirement); - return freeCollateral.gte(ZERO) ? freeCollateral : ZERO; + const marginCalc = this.getMarginCalculation(marginCategory, { + enteringHighLeverage: enterHighLeverageMode, + }); + + if (perpMarketIndex !== undefined) { + return marginCalc.getIsolatedFreeCollateral(perpMarketIndex); + } else { + return marginCalc.getCrossFreeCollateral(); + } } /** @@ -873,7 +842,6 @@ export class User { * @param strict - Optional flag to enforce strict margin calculations. * @param includeOpenOrders - Optional flag to include open orders in the margin calculation. * @param enteringHighLeverage - Optional flag indicating if the user is entering high leverage mode. - * @param marginType - Optional type of margin ('Cross' or 'Isolated'). If 'Isolated', perpMarketIndex must be provided. * @param perpMarketIndex - Optional index of the perpetual market. Required if marginType is 'Isolated'. * * @returns The calculated margin requirement as a BN (BigNumber). @@ -884,7 +852,6 @@ export class User { strict?: boolean, includeOpenOrders?: boolean, enteringHighLeverage?: boolean, - marginType?: MarginType, perpMarketIndex?: number ): BN; @@ -894,7 +861,6 @@ export class User { strict?: boolean, includeOpenOrders?: boolean, enteringHighLeverage?: boolean, - marginType?: MarginType, perpMarketIndex?: number ): BN { const marginCalc = this.getMarginCalculation(marginCategory, { @@ -904,13 +870,8 @@ export class User { liquidationBuffer, }); - // If marginType is provided and is Isolated, compute only for that market index - if (marginType === 'Isolated') { - if (perpMarketIndex === undefined) { - throw new Error( - 'perpMarketIndex is required when marginType = Isolated' - ); - } + // If perpMarketIndex is provided, compute only for that market index + if (perpMarketIndex !== undefined) { const isolatedMarginCalculation = marginCalc.isolatedMarginCalculations.get(perpMarketIndex); const { marginRequirement } = isolatedMarginCalculation; @@ -928,16 +889,14 @@ export class User { */ public getInitialMarginRequirement( enterHighLeverageMode = false, - marginType?: MarginType, perpMarketIndex?: number ): BN { return this.getMarginRequirement( 'Initial', undefined, - true, + false, undefined, enterHighLeverageMode, - marginType, perpMarketIndex ); } @@ -947,7 +906,6 @@ export class User { */ public getMaintenanceMarginRequirement( liquidationBuffer?: BN, - marginType?: MarginType, perpMarketIndex?: number ): BN { return this.getMarginRequirement( @@ -956,7 +914,6 @@ export class User { true, // strict default true, // includeOpenOrders default false, // enteringHighLeverage default - marginType, perpMarketIndex ); } @@ -1542,13 +1499,21 @@ export class User { marginCategory: MarginCategory = 'Initial', strict = false, includeOpenOrders = true, - liquidationBuffer?: BN + liquidationBuffer?: BN, + perpMarketIndex?: number ): BN { - return this.getMarginCalculation(marginCategory, { + const marginCalc = this.getMarginCalculation(marginCategory, { strict, includeOpenOrders, liquidationBuffer, - }).totalCollateral; + }); + + if (perpMarketIndex !== undefined) { + return marginCalc.isolatedMarginCalculations.get(perpMarketIndex) + .totalCollateral; + } + + return marginCalc.totalCollateral; } public getLiquidationBuffer(): BN | undefined { @@ -1935,6 +1900,16 @@ export class User { const oraclePriceData = this.getOracleDataForPerpMarket( perpPosition.marketIndex ); + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + perpMarket.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + perpMarket.quoteSpotMarketIndex + ); + const strictOracle = new StrictOraclePrice( + quoteOraclePriceData.price, + quoteOraclePriceData.twap + ); const positionUnrealizedPnl = calculatePositionPNL( perpMarket, @@ -1942,10 +1917,17 @@ export class User { true, oraclePriceData ); + + const spotAssetValue = getStrictTokenValue( + perpPosition.isolatedPositionScaledBalance, + quoteSpotMarket.decimals, + strictOracle + ); + return { perpLiabilityValue: perpLiability, perpPnl: positionUnrealizedPnl, - spotAssetValue: perpPosition.isolatedPositionScaledBalance, + spotAssetValue, spotLiabilityValue: ZERO, }; } @@ -2255,7 +2237,6 @@ export class User { const marginRequirement = this.getMaintenanceMarginRequirement( liquidationBuffer, - perpMarketIndex ? 'Isolated' : 'Cross', perpMarketIndex ); const canBeLiquidated = totalCollateral.lt(marginRequirement); From d956c23e43155dd23a70bc5f5a3eb07310e5eff2 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 14:06:11 -0600 Subject: [PATCH 138/247] feat: per perp pos max margin ratio --- sdk/src/user.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index d0223a3c91..636eac164b 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -290,12 +290,13 @@ export class User { ); // margin ratio for this perp + const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.customMarginRatio); let marginRatio = new BN( calculateMarketMarginRatio( market, worstCaseBaseAssetAmount.abs(), marginCategory, - this.getUserAccount().maxMarginRatio, + customMarginRatio, this.isHighLeverageMode() || enteringHighLeverage ) ); @@ -350,7 +351,7 @@ export class User { if (isIsolated) { // derive isolated quote deposit value, mirroring on-chain logic let depositValue = ZERO; - if (marketPosition.isolatedPositionScaledBalance?.gt(ZERO)) { + if (marketPosition.isolatedPositionScaledBalance.gt(ZERO)) { const quoteSpotMarket = this.driftClient.getSpotMarketAccount( market.quoteSpotMarketIndex ); From 826c962dc14249d5b9ad8cc9aed132f2ea33aabb Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 14:56:55 -0600 Subject: [PATCH 139/247] feat: additional ixs for transfer into iso + update perp margin ratio --- sdk/src/driftClient.ts | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index b7ed95003c..c74f3cd042 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -327,6 +327,40 @@ export class DriftClient { ); } + public async getTransferIsolatedPerpPositionDepositIx({ + perpMarketIndex, + amount, + spotMarketIndex, + subAccountId, + }: { + perpMarketIndex: number; + amount: BN; + spotMarketIndex?: number; // defaults to perp.quoteSpotMarketIndex + subAccountId?: number; + }): Promise { + const user = await this.getUserAccountPublicKey(subAccountId); + const userStats = this.getUserStatsAccountPublicKey(); + const statePk = await this.getStatePublicKey(); + const perp = this.getPerpMarketAccount(perpMarketIndex); + const spotIndex = spotMarketIndex ?? perp.quoteSpotMarketIndex; + const spot = this.getSpotMarketAccount(spotIndex); + + return await this.program.instruction.transferIsolatedPerpPositionDeposit( + spotIndex, + perpMarketIndex, + amount, + { + accounts: { + user, + userStats, + authority: this.wallet.publicKey, + state: statePk, + spotMarketVault: spot.vault, + }, + } + ); + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -4354,7 +4388,7 @@ export class DriftClient { const preIxs: TransactionInstruction[] = []; if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { preIxs.push( - await this.getDepositIntoIsolatedPerpPositionIx({ + await this.getTransferIsolatedPerpPositionDepositIx({ perpMarketIndex: orderParams.marketIndex, amount: isolatedPositionDepositAmount as BN, subAccountId: userAccount.subAccountId, @@ -4496,7 +4530,7 @@ export class DriftClient { const preIxs: TransactionInstruction[] = []; if (isolatedPositionDepositAmount?.gt?.(ZERO)) { preIxs.push( - await this.getDepositIntoIsolatedPerpPositionIx({ + await this.getTransferIsolatedPerpPositionDepositIx({ perpMarketIndex: orderParams.marketIndex, amount: isolatedPositionDepositAmount as BN, subAccountId, @@ -4961,7 +4995,7 @@ export class DriftClient { const p = params[0]; if (isVariant(p.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { preIxs.push( - await this.getDepositIntoIsolatedPerpPositionIx({ + await this.getTransferIsolatedPerpPositionDepositIx({ perpMarketIndex: p.marketIndex, amount: isolatedPositionDepositAmount as BN, subAccountId, @@ -6438,7 +6472,7 @@ export class DriftClient { if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { placeAndTakeIxs.push( - await this.getDepositIntoIsolatedPerpPositionIx({ + await this.getTransferIsolatedPerpPositionDepositIx({ perpMarketIndex: orderParams.marketIndex, amount: isolatedPositionDepositAmount as BN, subAccountId, From 46cc352fad5ac3213a179ece9df84131c7d388c7 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 20:20:53 -0600 Subject: [PATCH 140/247] feat: revamp liquidation checker functions for cross vs iso margin --- sdk/src/user.ts | 102 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 28 deletions(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 636eac164b..89a7dceca6 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -108,13 +108,10 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice'; import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber'; import { - MarginCalculation as JsMarginCalculation, + MarginCalculation, MarginContext, } from './marginCalculation'; -// Backwards compatibility: alias SDK MarginCalculation shape -export type UserMarginCalculation = JsMarginCalculation; - export type MarginType = 'Cross' | 'Isolated'; export class User { @@ -143,7 +140,7 @@ export class User { liquidationBuffer?: BN; // margin_buffer analog for buffer mode marginRatioOverride?: number; // mirrors context.margin_ratio_override } - ): JsMarginCalculation { + ): MarginCalculation { const strict = opts?.strict ?? false; const enteringHighLeverage = opts?.enteringHighLeverage ?? false; const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? @@ -165,7 +162,7 @@ export class User { .strictMode(strict) .setMarginBuffer(marginBuffer) .setMarginRatioOverride(userCustomMarginRatio); - const calc = new JsMarginCalculation(ctx); + const calc = new MarginCalculation(ctx); // SPOT POSITIONS // TODO: include open orders in the worst-case simulation in the same way on both spot and perp positions @@ -2222,41 +2219,90 @@ export class User { return netAssetValue.mul(TEN_THOUSAND).div(totalLiabilityValue); } - public canBeLiquidated(perpMarketIndex?: number): { + public canBeLiquidated(): { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN; } { - const liquidationBuffer = this.getLiquidationBuffer(); + // Deprecated signature retained for backward compatibility in type only + // but implementation now delegates to the new Map-based API and returns cross margin status. + const map = this.getLiquidationStatuses(); + const cross = map.get('cross'); + return cross ?? { canBeLiquidated: false, marginRequirement: ZERO, totalCollateral: ZERO }; + } - const totalCollateral = this.getTotalCollateral( - 'Maintenance', - undefined, - undefined, - liquidationBuffer - ); + /** + * New API: Returns liquidation status for cross and each isolated perp position. + * Map keys: + * - 'cross' for cross margin + * - marketIndex (number) for each isolated perp position + */ + public getLiquidationStatuses(marginCalc?: MarginCalculation): Map<'cross' | number, { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }> { + // If not provided, use buffer-aware calc for canBeLiquidated checks + if (!marginCalc) { + const liquidationBuffer = this.getLiquidationBuffer(); + marginCalc = this.getMarginCalculation('Maintenance', { liquidationBuffer }); + } - const marginRequirement = this.getMaintenanceMarginRequirement( - liquidationBuffer, - perpMarketIndex - ); - const canBeLiquidated = totalCollateral.lt(marginRequirement); + const result = new Map<'cross' | number, { + canBeLiquidated: boolean; + marginRequirement: BN; + totalCollateral: BN; + }>(); + + // Cross margin status + const crossTotalCollateral = marginCalc.totalCollateral; + const crossMarginRequirement = marginCalc.marginRequirement; + result.set('cross', { + canBeLiquidated: crossTotalCollateral.lt(crossMarginRequirement), + marginRequirement: crossMarginRequirement, + totalCollateral: crossTotalCollateral, + }); - return { - canBeLiquidated, - marginRequirement, - totalCollateral, - }; + // Isolated positions status + for (const [marketIndex, isoCalc] of marginCalc.isolatedMarginCalculations) { + const isoTotalCollateral = isoCalc.totalCollateral; + const isoMarginRequirement = isoCalc.marginRequirement; + result.set(marketIndex, { + canBeLiquidated: isoTotalCollateral.lt(isoMarginRequirement), + marginRequirement: isoMarginRequirement, + totalCollateral: isoTotalCollateral, + }); + } + + return result; } - public isBeingLiquidated(): boolean { - return ( + public isBeingLiquidated(marginCalc?: MarginCalculation): boolean { + // Consider on-chain flags OR computed margin status (cross or any isolated) + const hasOnChainFlag = (this.getUserAccount().status & - (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > - 0 + (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > 0; + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + return ( + hasOnChainFlag || + this.isCrossMarginBeingLiquidated(calc) || + this.isIsolatedMarginBeingLiquidated(calc) ); } + /** Returns true if cross margin is currently below maintenance requirement (no buffer). */ + public isCrossMarginBeingLiquidated(marginCalc?: MarginCalculation): boolean { + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + return calc.totalCollateral.lt(calc.marginRequirement); + } + + /** Returns true if any isolated perp position is currently below its maintenance requirement (no buffer). */ + public isIsolatedMarginBeingLiquidated(marginCalc?: MarginCalculation): boolean { + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + for (const [, isoCalc] of calc.isolatedMarginCalculations) { + if (isoCalc.totalCollateral.lt(isoCalc.marginRequirement)) { + return true; + } + } + return false; + } + public hasStatus(status: UserStatus): boolean { return (this.getUserAccount().status & status) > 0; } From 9c6b4b08e696239638b237a48452314e52889f02 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 20:23:40 -0600 Subject: [PATCH 141/247] fix: adjust health getter for user --- sdk/src/user.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 89a7dceca6..b0cfb591e5 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -1530,11 +1530,11 @@ export class User { * @returns : number (value from [0, 100]) */ public getHealth(perpMarketIndex?: number): number { - if (this.isBeingLiquidated() && !perpMarketIndex) { + const marginCalc = this.getMarginCalculation('Maintenance'); + if (this.isCrossMarginBeingLiquidated(marginCalc) && !perpMarketIndex) { return 0; } - const marginCalc = this.getMarginCalculation('Maintenance'); let totalCollateral: BN; let maintenanceMarginReq: BN; From 929434094f333769d6dee9257186b57484eab135 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 20:29:14 -0600 Subject: [PATCH 142/247] fix: liq statuses add to return signature --- sdk/src/user.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index b0cfb591e5..68e8879bef 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -2223,12 +2223,13 @@ export class User { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN; + liquidationStatuses: Map<'cross' | number, { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }>; } { // Deprecated signature retained for backward compatibility in type only // but implementation now delegates to the new Map-based API and returns cross margin status. const map = this.getLiquidationStatuses(); const cross = map.get('cross'); - return cross ?? { canBeLiquidated: false, marginRequirement: ZERO, totalCollateral: ZERO }; + return cross ? { ...cross, liquidationStatuses: map } : { canBeLiquidated: false, marginRequirement: ZERO, totalCollateral: ZERO, liquidationStatuses: map }; } /** From c45be38b45ca7144f7cd3877b64228a06316abff Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 14 Oct 2025 13:01:05 -0600 Subject: [PATCH 143/247] chore: post rebase cleaner upper --- sdk/src/decode/user.ts | 2 - sdk/src/driftClient.ts | 162 ++++++++------------------------------ sdk/src/user.ts | 5 +- sdk/tests/dlob/helpers.ts | 3 +- 4 files changed, 38 insertions(+), 134 deletions(-) diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index b3b2e2fca3..706a0698ba 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -86,7 +86,6 @@ export function decodeUser(buffer: Buffer): UserAccount { const openOrders = buffer.readUInt8(offset + 94); const positionFlag = buffer.readUInt8(offset + 95); const isolatedPositionScaledBalance = readUnsignedBigInt64LE(buffer, offset + 96); - const customMarginRatio = buffer.readUInt32LE(offset + 97); if ( baseAssetAmount.eq(ZERO) && @@ -139,7 +138,6 @@ export function decodeUser(buffer: Buffer): UserAccount { maxMarginRatio, positionFlag, isolatedPositionScaledBalance, - customMarginRatio, }); } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index c74f3cd042..96d917e9e1 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -264,103 +264,6 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } - public async getDepositIntoIsolatedPerpPositionIx({ - perpMarketIndex, - amount, - spotMarketIndex, - subAccountId, - userTokenAccount, - }: { - perpMarketIndex: number; - amount: BN; - spotMarketIndex?: number; // defaults to perp.quoteSpotMarketIndex - subAccountId?: number; - userTokenAccount?: PublicKey; // defaults ATA for spot market mint - }): Promise { - const user = await this.getUserAccountPublicKey(subAccountId); - const userStats = this.getUserStatsAccountPublicKey(); - const statePk = await this.getStatePublicKey(); - const perp = this.getPerpMarketAccount(perpMarketIndex); - const spotIndex = spotMarketIndex ?? perp.quoteSpotMarketIndex; - const spot = this.getSpotMarketAccount(spotIndex); - - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [this.getUserAccount(subAccountId)], - readablePerpMarketIndex: perpMarketIndex, - writableSpotMarketIndexes: [spotIndex], - }); - - // token program and transfer hook mints need to be present for deposit - this.addTokenMintToRemainingAccounts(spot, remainingAccounts); - if (this.isTransferHook(spot)) { - await this.addExtraAccountMetasToRemainingAccounts( - spot.mint, - remainingAccounts - ); - } - - const tokenProgram = this.getTokenProgramForSpotMarket(spot); - const ata = - userTokenAccount ?? - (await this.getAssociatedTokenAccount( - spotIndex, - false, - tokenProgram - )); - - return await this.program.instruction.depositIntoIsolatedPerpPosition( - spotIndex, - perpMarketIndex, - amount, - { - accounts: { - state: statePk, - user, - userStats, - authority: this.wallet.publicKey, - spotMarketVault: spot.vault, - userTokenAccount: ata, - tokenProgram, - }, - remainingAccounts, - } - ); - } - - public async getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex, - amount, - spotMarketIndex, - subAccountId, - }: { - perpMarketIndex: number; - amount: BN; - spotMarketIndex?: number; // defaults to perp.quoteSpotMarketIndex - subAccountId?: number; - }): Promise { - const user = await this.getUserAccountPublicKey(subAccountId); - const userStats = this.getUserStatsAccountPublicKey(); - const statePk = await this.getStatePublicKey(); - const perp = this.getPerpMarketAccount(perpMarketIndex); - const spotIndex = spotMarketIndex ?? perp.quoteSpotMarketIndex; - const spot = this.getSpotMarketAccount(spotIndex); - - return await this.program.instruction.transferIsolatedPerpPositionDeposit( - spotIndex, - perpMarketIndex, - amount, - { - accounts: { - user, - userStats, - authority: this.wallet.publicKey, - state: statePk, - spotMarketVault: spot.vault, - }, - } - ); - } - public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -4384,29 +4287,27 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - const preIxs: TransactionInstruction[] = []; - if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + if ( + isVariant(orderParams.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) + ) { preIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex: orderParams.marketIndex, - amount: isolatedPositionDepositAmount as BN, - subAccountId: userAccount.subAccountId, - }) + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + userAccount.subAccountId + ) ); } - ixPromisesForTxs.marketOrderTx = (async () => { const placeOrdersIx = await this.getPlaceOrdersIx( [orderParams, ...bracketOrdersParams], userAccount.subAccountId ); if (preIxs.length) { - return [ - ...preIxs, - placeOrdersIx, - ] as unknown as TransactionInstruction; + return [...preIxs, placeOrdersIx] as unknown as TransactionInstruction; } return placeOrdersIx; })(); @@ -4530,11 +4431,11 @@ export class DriftClient { const preIxs: TransactionInstruction[] = []; if (isolatedPositionDepositAmount?.gt?.(ZERO)) { preIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex: orderParams.marketIndex, - amount: isolatedPositionDepositAmount as BN, - subAccountId, - }) + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + subAccountId + ) ); } @@ -4918,7 +4819,6 @@ export class DriftClient { useMarketLastSlotCache: true, }); - return await this.program.instruction.cancelOrders( marketType ?? null, marketIndex ?? null, @@ -4993,13 +4893,16 @@ export class DriftClient { const preIxs: TransactionInstruction[] = []; if (params?.length === 1) { const p = params[0]; - if (isVariant(p.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + if ( + isVariant(p.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) + ) { preIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex: p.marketIndex, - amount: isolatedPositionDepositAmount as BN, - subAccountId, - }) + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + p.marketIndex, + subAccountId + ) ); } } @@ -6470,13 +6373,16 @@ export class DriftClient { subAccountId ); - if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + if ( + isVariant(orderParams.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) + ) { placeAndTakeIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex: orderParams.marketIndex, - amount: isolatedPositionDepositAmount as BN, - subAccountId, - }) + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + subAccountId + ) ); } @@ -10758,4 +10664,4 @@ export class DriftClient { forceVersionedTransaction, }); } -} \ No newline at end of file +} diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 68e8879bef..48b4a8306d 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -287,14 +287,14 @@ export class User { ); // margin ratio for this perp - const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.customMarginRatio); + const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.maxMarginRatio); let marginRatio = new BN( calculateMarketMarginRatio( market, worstCaseBaseAssetAmount.abs(), marginCategory, customMarginRatio, - this.isHighLeverageMode() || enteringHighLeverage + this.isHighLeverageMode(marginCategory) || enteringHighLeverage ) ); if (isVariant(market.status, 'settlement')) { @@ -616,7 +616,6 @@ export class User { maxMarginRatio: 0, positionFlag: 0, isolatedPositionScaledBalance: ZERO, - customMarginRatio: 0, }; } diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index 90dfac1df8..b36effd2d7 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -47,6 +47,8 @@ export const mockPerpPosition: PerpPosition = { lastQuoteAssetAmountPerLp: new BN(0), perLpBase: 0, positionFlag: 0, + isolatedPositionScaledBalance: new BN(0), + maxMarginRatio: 0, }; export const mockAMM: AMM = { @@ -663,7 +665,6 @@ export class MockUserMap implements UserMapInterface { private userMap = new Map(); private userAccountToAuthority = new Map(); private driftClient: DriftClient; - eventEmitter: StrictEventEmitter; constructor() { this.userMap = new Map(); From d73ebd71cfe828ed0a67ac3a8932e4ac5985f5a6 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Tue, 14 Oct 2025 13:04:59 -0600 Subject: [PATCH 144/247] feat: grpc v2 delisted markets handling better optimized (#1960) * feat: grpc v2 delisted markets handling better optimized * fix: prettier formatting --- .../grpcDriftClientAccountSubscriberV2.ts | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 6008806ddd..9abfc93c21 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -691,19 +691,28 @@ export class grpcDriftClientAccountSubscriberV2 Array.from(this.spotMarketsSubscriber?.getAccountDataMap().values() || []) ); - for (const perpMarketIndex of perpMarketIndexes) { - await this.perpMarketsSubscriber.removeAccounts([ - new PublicKey( - this.perpMarketIndexToAccountPubkeyMap.get(perpMarketIndex) || '' - ), - ]); - if (this.delistedMarketSetting === DelistedMarketSetting.Discard) { - this.perpMarketIndexToAccountPubkeyMap.delete(perpMarketIndex); - } + // Build array of perp market pubkeys to remove + const perpMarketPubkeysToRemove = perpMarketIndexes + .map((marketIndex) => { + const pubkeyString = + this.perpMarketIndexToAccountPubkeyMap.get(marketIndex); + return pubkeyString ? new PublicKey(pubkeyString) : null; + }) + .filter((pubkey) => pubkey !== null) as PublicKey[]; + + // Build array of oracle pubkeys to remove + const oraclePubkeysToRemove = oracles.map((oracle) => oracle.publicKey); + + // Remove accounts in batches - perp markets + if (perpMarketPubkeysToRemove.length > 0) { + await this.perpMarketsSubscriber.removeAccounts( + perpMarketPubkeysToRemove + ); } - for (const oracle of oracles) { - await this.oracleMultiSubscriber.removeAccounts([oracle.publicKey]); + // Remove accounts in batches - oracles + if (oraclePubkeysToRemove.length > 0) { + await this.oracleMultiSubscriber.removeAccounts(oraclePubkeysToRemove); } } From ec2731a0462a86251a4af667309c78a35bf25298 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 19:09:57 +0000 Subject: [PATCH 145/247] sdk: release v2.142.0-beta.26 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index f0145dcf5b..84d536deff 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.25 \ No newline at end of file +2.142.0-beta.26 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index e66287af39..3e6fc727b6 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.25", + "version": "2.142.0-beta.26", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From b4fa440df02c14691a0d2d901aa8a01d4a55792a Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:56:41 -0700 Subject: [PATCH 146/247] ref price offset enhance (#1958) * logic change * logic change + some tests * wrap tests * revert logic back * tests pass * add admin function + move field to amm + sdk math * remove msg * add additional tests and address comments * prettify --- programs/drift/src/controller/amm.rs | 27 ++++++-- .../drift/src/controller/position/tests.rs | 53 +++++++-------- programs/drift/src/instructions/admin.rs | 46 ++++++++++++- programs/drift/src/lib.rs | 10 +++ programs/drift/src/math/amm_spread.rs | 35 +++++++++- programs/drift/src/math/amm_spread/tests.rs | 68 +++++++++++++++++++ programs/drift/src/state/perp_market.rs | 10 ++- sdk/src/adminClient.ts | 40 +++++++++++ sdk/src/idl/drift.json | 37 ++++++++-- sdk/src/math/amm.ts | 60 +++++++++++++--- sdk/src/types.ts | 1 + tests/admin.ts | 13 ++++ 12 files changed, 346 insertions(+), 54 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 0012b4804f..7162e9d2dd 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -185,20 +185,33 @@ pub fn update_spreads( let max_ref_offset = market.amm.get_max_reference_price_offset()?; let reference_price_offset = if max_ref_offset > 0 { - let liquidity_ratio = amm_spread::calculate_inventory_liquidity_ratio( - market.amm.base_asset_amount_with_amm, - market.amm.base_asset_reserve, - market.amm.min_base_asset_reserve, - market.amm.max_base_asset_reserve, - )?; + let liquidity_ratio = + amm_spread::calculate_inventory_liquidity_ratio_for_reference_price_offset( + market.amm.base_asset_amount_with_amm, + market.amm.base_asset_reserve, + market.amm.min_base_asset_reserve, + market.amm.max_base_asset_reserve, + )?; let signed_liquidity_ratio = liquidity_ratio.safe_mul(market.amm.get_protocol_owned_position()?.signum().cast()?)?; + let deadband_pct = market.amm.get_reference_price_offset_deadband_pct()?; + let liquidity_fraction_after_deadband = + if signed_liquidity_ratio.unsigned_abs() <= deadband_pct { + 0 + } else { + signed_liquidity_ratio.safe_sub( + deadband_pct + .cast::()? + .safe_mul(signed_liquidity_ratio.signum())?, + )? + }; + amm_spread::calculate_reference_price_offset( reserve_price, market.amm.last_24h_avg_funding_rate, - signed_liquidity_ratio, + liquidity_fraction_after_deadband, market.amm.min_order_size, market .amm diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index ab6cf1034c..89f8305340 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -843,7 +843,7 @@ fn amm_ref_price_offset_decay_logic() { .unwrap(); assert_eq!(perp_market.amm.last_update_slot, clock_slot); assert_eq!(perp_market.amm.last_oracle_valid, true); - assert_eq!(perp_market.amm.reference_price_offset, 7350); + assert_eq!(perp_market.amm.reference_price_offset, 4458); perp_market.amm.last_mark_price_twap_5min = (perp_market .amm @@ -893,28 +893,28 @@ fn amm_ref_price_offset_decay_logic() { assert_eq!( offsets, [ - 7140, 6930, 6720, 6510, 6300, 6090, 6070, 6050, 6030, 6010, 5800, 5590, 5380, 5170, - 4960, 4750, 4540, 4330, 4120, 3910, 3700, 3490, 3280, 3070, 2860, 2650, 2440, 2230, - 2020, 1810, 1620, 1449, 1296, 1158, 1034, 922, 821, 730, 648, 575, 509, 450, 396, 348, - 305, 266, 231, 199, 171, 145, 122, 101, 81, 61, 41, 21, 1, 0, 0, 0 + 4248, 4038, 3828, 3618, 3408, 3198, 3178, 3158, 3138, 3118, 2908, 2698, 2488, 2278, + 2068, 1858, 1664, 1489, 1332, 1190, 1062, 947, 844, 751, 667, 592, 524, 463, 408, 359, + 315, 275, 239, 207, 178, 152, 128, 107, 87, 67, 47, 27, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 ] ); assert_eq!( lspreads, [ - 726, 726, 726, 726, 726, 726, 536, 536, 536, 536, 726, 726, 726, 726, 726, 726, 726, - 726, 726, 726, 726, 726, 726, 726, 726, 726, 726, 726, 726, 726, 706, 687, 669, 654, - 640, 628, 617, 607, 598, 589, 582, 575, 570, 564, 559, 555, 551, 548, 544, 542, 539, - 537, 536, 536, 536, 536, 536, 526, 526, 526 + 726, 726, 726, 726, 726, 726, 536, 536, 536, 536, 726, 726, 726, 726, 726, 726, 710, + 691, 673, 658, 644, 631, 619, 609, 600, 591, 584, 577, 571, 565, 560, 556, 552, 548, + 545, 542, 540, 537, 536, 536, 536, 536, 536, 526, 526, 526, 526, 526, 526, 526, 526, + 526, 526, 526, 526, 526, 526, 526, 526, 526 ] ); assert_eq!( sspreads, [ - 7150, 6940, 6730, 6520, 6310, 6100, 6080, 6060, 6040, 6020, 5810, 5600, 5390, 5180, - 4970, 4760, 4550, 4340, 4130, 3920, 3710, 3500, 3290, 3080, 2870, 2660, 2450, 2240, - 2030, 1820, 1630, 1459, 1306, 1168, 1044, 932, 831, 740, 658, 585, 519, 460, 406, 358, - 315, 276, 241, 209, 181, 155, 132, 111, 91, 71, 51, 31, 11, 10, 10, 10 + 4258, 4048, 3838, 3628, 3418, 3208, 3188, 3168, 3148, 3128, 2918, 2708, 2498, 2288, + 2078, 1868, 1674, 1499, 1342, 1200, 1072, 957, 854, 761, 677, 602, 534, 473, 418, 369, + 325, 285, 249, 217, 188, 162, 138, 117, 97, 77, 57, 37, 17, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 ] ); } @@ -1017,7 +1017,7 @@ fn amm_negative_ref_price_offset_decay_logic() { .unwrap(); assert_eq!(perp_market.amm.last_update_slot, clock_slot); assert_eq!(perp_market.amm.last_oracle_valid, true); - assert_eq!(perp_market.amm.reference_price_offset, 7350); + assert_eq!(perp_market.amm.reference_price_offset, 4458); perp_market.amm.last_mark_price_twap_5min = (perp_market .amm @@ -1068,34 +1068,31 @@ fn amm_negative_ref_price_offset_decay_logic() { assert_eq!( offsets, [ - -7140, -6930, -6720, -6510, -6300, -6090, -6070, -6050, -6030, -6010, -5800, -5590, - -5380, -5170, -4960, -4750, -4540, -4330, -4120, -3910, -3700, -3490, -3280, -3070, - -2860, -2650, -2440, -2230, -2020, -1810, -1600, -1390, -1180, -970, -760, -550, -340, - -130, 0, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, - 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, - 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, - 10000, 10000, 10000, 10000, 10000, 10000 + -4248, -4038, -3828, -3618, -3408, -3198, -3178, -3158, -3138, -3118, -2908, -2698, + -2488, -2278, -2068, -1858, -1648, -1438, -1228, -1018, -808, -598, -388, -178, 0, + 7654, 7652, 7651, 7649, 7648, 7646, 7645, 7643, 7641, 7640, 7638, 7637, 7635, 7634, + 7632, 7631, 7629, 7628, 7626, 7625, 7623, 7622, 7620, 7619, 7618, 7616, 7615, 7613, + 7612, 7610, 7609, 7607, 7606, 7605, 7603, 7602, 7600, 7599, 7597, 7596, 7595, 7593, + 7592, 7591, 7589, 7588, 7586, 7585, 7584, 7582, 7581, 7580, 7578, 7577, 7576 ] ); assert_eq!( sspreads, [ 210, 210, 210, 210, 210, 210, 20, 20, 20, 20, 210, 210, 210, 210, 210, 210, 210, 210, - 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, - 210, 210, 210, 130, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 210, 210, 210, 210, 210, 210, 178, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, - 10, 10 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 ] ); assert_eq!( lspreads, [ - 7666, 7456, 7246, 7036, 6826, 6616, 6596, 6576, 6556, 6536, 6326, 6116, 5906, 5696, - 5486, 5276, 5066, 4856, 4646, 4436, 4226, 4016, 3806, 3596, 3386, 3176, 2966, 2756, - 2546, 2336, 2126, 1916, 1706, 1496, 1286, 1076, 866, 656, 526, 526, 526, 526, 526, 526, + 4774, 4564, 4354, 4144, 3934, 3724, 3704, 3684, 3664, 3644, 3434, 3224, 3014, 2804, + 2594, 2384, 2174, 1964, 1754, 1544, 1334, 1124, 914, 704, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, - 526, 526 + 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526 ] ); } diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 94fe90f15f..dd4a0d72fa 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1079,7 +1079,8 @@ pub fn handle_initialize_perp_market( quote_asset_amount_with_unsettled_lp: 0, reference_price_offset: 0, amm_inventory_spread_adjustment: 0, - padding: [0; 3], + reference_price_offset_deadband_pct: 0, + padding: [0; 2], last_funding_oracle_twap: 0, }, }; @@ -3439,6 +3440,49 @@ pub fn handle_update_perp_market_curve_update_intensity( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_market_reference_price_offset_deadband_pct( + ctx: Context, + reference_price_offset_deadband_pct: u8, +) -> Result<()> { + validate!( + reference_price_offset_deadband_pct <= 100, + ErrorCode::DefaultError, + "invalid reference_price_offset_deadband_pct", + )?; + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + + msg!( + "perp_market.amm.reference_price_offset_deadband_pct: {} -> {}", + perp_market.amm.reference_price_offset_deadband_pct, + reference_price_offset_deadband_pct + ); + + let liquidity_ratio = + crate::math::amm_spread::calculate_inventory_liquidity_ratio_for_reference_price_offset( + perp_market.amm.base_asset_amount_with_amm, + perp_market.amm.base_asset_reserve, + perp_market.amm.min_base_asset_reserve, + perp_market.amm.max_base_asset_reserve, + )?; + + let signed_liquidity_ratio = liquidity_ratio.safe_mul( + perp_market + .amm + .get_protocol_owned_position()? + .signum() + .cast()?, + )?; + + msg!("current signed liquidity ratio: {}", signed_liquidity_ratio); + + perp_market.amm.reference_price_offset_deadband_pct = reference_price_offset_deadband_pct; + Ok(()) +} + pub fn handle_update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index e71b6de2a1..d91c7e80bc 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1387,6 +1387,16 @@ pub mod drift { handle_update_perp_market_curve_update_intensity(ctx, curve_update_intensity) } + pub fn update_perp_market_reference_price_offset_deadband_pct( + ctx: Context, + reference_price_offset_deadband_pct: u8, + ) -> Result<()> { + handle_update_perp_market_reference_price_offset_deadband_pct( + ctx, + reference_price_offset_deadband_pct, + ) + } + pub fn update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/math/amm_spread.rs b/programs/drift/src/math/amm_spread.rs index 80915b83b9..cb90937315 100644 --- a/programs/drift/src/math/amm_spread.rs +++ b/programs/drift/src/math/amm_spread.rs @@ -12,8 +12,8 @@ use crate::math::constants::{ BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, DEFAULT_LARGE_BID_ASK_FACTOR, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, FUNDING_RATE_BUFFER, MAX_BID_ASK_INVENTORY_SKEW_FACTOR, PEG_PRECISION, PERCENTAGE_PRECISION, - PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, - PRICE_PRECISION_I64, + PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, + PRICE_PRECISION_I128, PRICE_PRECISION_I64, }; use crate::math::safe_math::SafeMath; use crate::state::perp_market::{ContractType, PerpMarket, AMM}; @@ -193,6 +193,35 @@ pub fn calculate_inventory_liquidity_ratio( Ok(amm_inventory_pct) } +pub fn calculate_inventory_liquidity_ratio_for_reference_price_offset( + base_asset_amount_with_amm: i128, + base_asset_reserve: u128, + min_base_asset_reserve: u128, + max_base_asset_reserve: u128, +) -> DriftResult { + // inventory scale + let (max_bids, max_asks) = _calculate_market_open_bids_asks( + base_asset_reserve, + min_base_asset_reserve, + max_base_asset_reserve, + )?; + + let avg_liquidity = (max_bids.safe_add(max_asks.abs())?).safe_div(2)?; + + let amm_inventory_pct = if base_asset_amount_with_amm.abs() < avg_liquidity { + base_asset_amount_with_amm + .abs() + .safe_mul(PERCENTAGE_PRECISION_I128) + .unwrap_or(i128::MAX) + .safe_div(avg_liquidity.max(1))? + .min(PERCENTAGE_PRECISION_I128) + } else { + PERCENTAGE_PRECISION_I128 // 100% + }; + + Ok(amm_inventory_pct) +} + pub fn calculate_spread_inventory_scale( base_asset_amount_with_amm: i128, base_asset_reserve: u128, @@ -570,7 +599,7 @@ pub fn calculate_reference_price_offset( mark_twap_slow: u64, max_offset_pct: i64, ) -> DriftResult { - if last_24h_avg_funding_rate == 0 { + if last_24h_avg_funding_rate == 0 || liquidity_fraction == 0 { return Ok(0); } diff --git a/programs/drift/src/math/amm_spread/tests.rs b/programs/drift/src/math/amm_spread/tests.rs index 5953db1e04..ac57fb45df 100644 --- a/programs/drift/src/math/amm_spread/tests.rs +++ b/programs/drift/src/math/amm_spread/tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod test { + use crate::controller::amm::update_spreads; use crate::math::amm::calculate_price; use crate::math::amm_spread::*; use crate::math::constants::{ @@ -189,6 +190,73 @@ mod test { assert_eq!(res, 0); } + #[test] + fn calculate_reference_price_offset_deadband_tests() { + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: AMM_RESERVE_PRECISION * 11, + quote_asset_reserve: AMM_RESERVE_PRECISION * 10, + sqrt_k: AMM_RESERVE_PRECISION * 10, + peg_multiplier: 34_000_000, + min_base_asset_reserve: AMM_RESERVE_PRECISION * 7, + max_base_asset_reserve: AMM_RESERVE_PRECISION * 14, + base_spread: 1000, + max_spread: 20_000, + curve_update_intensity: 110, + last_24h_avg_funding_rate: 1, + last_mark_price_twap_5min: 4216 * 10000 + 2 * 10000, + last_mark_price_twap: 4216 * 10000 + 2 * 10000, + historical_oracle_data: { + let mut hod: crate::state::oracle::HistoricalOracleData = Default::default(); + hod.last_oracle_price_twap_5min = 4216 * 10000; + hod.last_oracle_price_twap = 4216 * 10000; + hod + }, + ..AMM::default() + }, + ..PerpMarket::default() + }; + + let reserve_price = 4216 * 10000; + + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 7 / 20) as i128; + let inventory_ratio = calculate_inventory_liquidity_ratio_for_reference_price_offset( + market.amm.base_asset_amount_with_amm, + market.amm.base_asset_reserve, + market.amm.min_base_asset_reserve, + market.amm.max_base_asset_reserve, + ) + .unwrap(); + assert_eq!(inventory_ratio, 100000); // 10% + + market.amm.reference_price_offset_deadband_pct = 10; // 10% + + // If inventory exceeds threshold positive ref price offset + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 8 / 20) as i128; + let (_l, _s) = update_spreads(&mut market, reserve_price as u64, None).unwrap(); + assert!(market.amm.reference_price_offset > 0); + + // If inventory is small, goes to 0 + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 6 / 20) as i128; + let (_l, _s) = update_spreads(&mut market, reserve_price as u64, None).unwrap(); + assert_eq!(market.amm.reference_price_offset, 0); + + // Same for short pos + // Make sure that the premium is also short + market.amm.last_24h_avg_funding_rate = -1; + market.amm.last_mark_price_twap_5min = 4216 * 10000 - 2 * 10000; + market.amm.last_mark_price_twap = 4216 * 10000 - 2 * 10000; + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 8 / 20) as i128 * -1; + let (_l, _s) = update_spreads(&mut market, reserve_price as u64, None).unwrap(); + println!("ref offset: {}", market.amm.reference_price_offset); + assert!(market.amm.reference_price_offset < 0); + + // Same for short pos + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 6 / 20) as i128 * -1; + let (_l, _s) = update_spreads(&mut market, reserve_price as u64, None).unwrap(); + assert_eq!(market.amm.reference_price_offset, 0); + } + #[test] fn calculate_spread_tests() { let base_spread = 1000; // .1% diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 815fa83fc2..69d2e80f38 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1167,7 +1167,8 @@ pub struct AMM { pub reference_price_offset: i32, /// signed scale amm_spread similar to fee_adjustment logic (-100 = 0, 100 = double) pub amm_inventory_spread_adjustment: i8, - pub padding: [u8; 3], + pub reference_price_offset_deadband_pct: u8, + pub padding: [u8; 2], pub last_funding_oracle_twap: i64, } @@ -1258,13 +1259,18 @@ impl Default for AMM { quote_asset_amount_with_unsettled_lp: 0, reference_price_offset: 0, amm_inventory_spread_adjustment: 0, - padding: [0; 3], + reference_price_offset_deadband_pct: 0, + padding: [0; 2], last_funding_oracle_twap: 0, } } } impl AMM { + pub fn get_reference_price_offset_deadband_pct(&self) -> DriftResult { + let pct = self.reference_price_offset_deadband_pct as u128; + Ok(PERCENTAGE_PRECISION.safe_mul(pct)?.safe_div(100_u128)?) + } pub fn get_fallback_price( self, direction: &PositionDirection, diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index b5b6244866..ad5307ad70 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -1232,6 +1232,46 @@ export class AdminClient extends DriftClient { ); } + public async updatePerpMarketReferencePriceOffsetDeadbandPct( + perpMarketIndex: number, + referencePriceOffsetDeadbandPct: number + ): Promise { + const updatePerpMarketReferencePriceOffsetDeadbandPctIx = + await this.getUpdatePerpMarketReferencePriceOffsetDeadbandPctIx( + perpMarketIndex, + referencePriceOffsetDeadbandPct + ); + + const tx = await this.buildTransaction( + updatePerpMarketReferencePriceOffsetDeadbandPctIx + ); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdatePerpMarketReferencePriceOffsetDeadbandPctIx( + perpMarketIndex: number, + referencePriceOffsetDeadbandPct: number + ): Promise { + return await this.program.instruction.updatePerpMarketReferencePriceOffsetDeadbandPct( + referencePriceOffsetDeadbandPct, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + }, + } + ); + } + public async updatePerpMarketTargetBaseAssetAmountPerLp( perpMarketIndex: number, targetBaseAssetAmountPerLP: number diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 10f90b9c8d..ed162a931f 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -5874,6 +5874,32 @@ } ] }, + { + "name": "updatePerpMarketReferencePriceOffsetDeadbandPct", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "referencePriceOffsetDeadbandPct", + "type": "u8" + } + ] + }, { "name": "updateLpCooldownTime", "accounts": [ @@ -11209,12 +11235,16 @@ ], "type": "i8" }, + { + "name": "referencePriceOffsetDeadbandPct", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 3 + 2 ] } }, @@ -16782,8 +16812,5 @@ "name": "UnableToLoadRevenueShareAccount", "msg": "Unable to load builder account" } - ], - "metadata": { - "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" - } + ] } \ No newline at end of file diff --git a/sdk/src/math/amm.ts b/sdk/src/math/amm.ts index 7b41b4a783..88a9ec9f5b 100644 --- a/sdk/src/math/amm.ts +++ b/sdk/src/math/amm.ts @@ -389,6 +389,31 @@ export function calculateInventoryLiquidityRatio( return inventoryScaleBN; } +export function calculateInventoryLiquidityRatioForReferencePriceOffset( + baseAssetAmountWithAmm: BN, + baseAssetReserve: BN, + minBaseAssetReserve: BN, + maxBaseAssetReserve: BN +): BN { + // inventory skew + const [openBids, openAsks] = calculateMarketOpenBidAsk( + baseAssetReserve, + minBaseAssetReserve, + maxBaseAssetReserve + ); + + const avgSideLiquidity = openBids.abs().add(openAsks.abs()).div(TWO); + + const inventoryScaleBN = BN.min( + baseAssetAmountWithAmm + .mul(PERCENTAGE_PRECISION) + .div(BN.max(avgSideLiquidity, ONE)) + .abs(), + PERCENTAGE_PRECISION + ); + return inventoryScaleBN; +} + export function calculateInventoryScale( baseAssetAmountWithAmm: BN, baseAssetReserve: BN, @@ -440,7 +465,7 @@ export function calculateReferencePriceOffset( markTwapSlow: BN, maxOffsetPct: number ): BN { - if (last24hAvgFundingRate.eq(ZERO)) { + if (last24hAvgFundingRate.eq(ZERO) || liquidityFraction.eq(ZERO)) { return ZERO; } @@ -1013,19 +1038,38 @@ export function calculateSpreadReserves( (amm.curveUpdateIntensity - 100) ); - const liquidityFraction = calculateInventoryLiquidityRatio( - amm.baseAssetAmountWithAmm, - amm.baseAssetReserve, - amm.minBaseAssetReserve, - amm.maxBaseAssetReserve - ); + const liquidityFraction = + calculateInventoryLiquidityRatioForReferencePriceOffset( + amm.baseAssetAmountWithAmm, + amm.baseAssetReserve, + amm.minBaseAssetReserve, + amm.maxBaseAssetReserve + ); const liquidityFractionSigned = liquidityFraction.mul( sigNum(amm.baseAssetAmountWithAmm.add(amm.baseAssetAmountWithUnsettledLp)) ); + + let liquidityFractionAfterDeadband = liquidityFractionSigned; + const deadbandPct = amm.referencePriceOffsetDeadbandPct + ? PERCENTAGE_PRECISION.mul( + new BN(amm.referencePriceOffsetDeadbandPct as number) + ).divn(100) + : ZERO; + if (!liquidityFractionAfterDeadband.eq(ZERO) && deadbandPct.gt(ZERO)) { + const abs = liquidityFractionAfterDeadband.abs(); + if (abs.lte(deadbandPct)) { + liquidityFractionAfterDeadband = ZERO; + } else { + liquidityFractionAfterDeadband = liquidityFractionAfterDeadband.sub( + deadbandPct.mul(sigNum(liquidityFractionAfterDeadband)) + ); + } + } + referencePriceOffset = calculateReferencePriceOffset( reservePrice, amm.last24HAvgFundingRate, - liquidityFractionSigned, + liquidityFractionAfterDeadband, amm.historicalOracleData.lastOraclePriceTwap5Min, amm.lastMarkPriceTwap5Min, amm.historicalOracleData.lastOraclePriceTwap, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index ae9fc45533..fd0cb165cd 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1112,6 +1112,7 @@ export type AMM = { ammInventorySpreadAdjustment: number; lastFundingOracleTwap: BN; + referencePriceOffsetDeadbandPct: number; }; // # User Account Types diff --git a/tests/admin.ts b/tests/admin.ts index 7eb87c396a..c397b3f350 100644 --- a/tests/admin.ts +++ b/tests/admin.ts @@ -472,6 +472,19 @@ describe('admin', () => { assert(perpMarket.amm.ammSpreadAdjustment == ammSpreadAdjustment); }); + it('update perp market reference offset deadband pct', async () => { + const referenceOffsetDeadbandPct = 5; + await driftClient.updatePerpMarketReferencePriceOffsetDeadbandPct( + 0, + referenceOffsetDeadbandPct + ); + const perpMarket = driftClient.getPerpMarketAccount(0); + assert( + perpMarket.amm.referencePriceOffsetDeadbandPct == + referenceOffsetDeadbandPct + ); + }); + it('update pnl pool', async () => { const quoteVault = driftClient.getSpotMarketAccount(0).vault; From 948b9f24408f9a69e2172bf5413bfd2899678475 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:01:34 +0000 Subject: [PATCH 147/247] sdk: release v2.142.0-beta.27 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 84d536deff..3d464037fe 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.26 \ No newline at end of file +2.142.0-beta.27 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 3e6fc727b6..f823f96f68 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.26", + "version": "2.142.0-beta.27", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 52490f752a72e5eae5cec9037a69d3cc12fd78b5 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 14 Oct 2025 17:36:58 -0400 Subject: [PATCH 148/247] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd53f4c01..82232c716b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features - program: add titan to whitelisted swap programs ([#1952](https://github.com/drift-labs/protocol-v2/pull/1952)) +- program: allow hot wallet to increase max spread and pause funding ([#1957](https://github.com/drift-labs/protocol-v2/pull/1957)) ### Fixes From ee0bf581d4b19dc904b7c8ff59b6870c201a1d3c Mon Sep 17 00:00:00 2001 From: lil perp Date: Tue, 14 Oct 2025 17:37:15 -0400 Subject: [PATCH 149/247] program: allow hot wallet to increase max spread and pause funding (#1957) * program: allow hot wallet to increase max spread * pause funding --- programs/drift/src/instructions/admin.rs | 12 ++++++++++-- programs/drift/src/lib.rs | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index dd4a0d72fa..198bcd6377 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -3262,12 +3262,20 @@ pub fn handle_update_perp_market_status( perp_market_valid(&ctx.accounts.perp_market) )] pub fn handle_update_perp_market_paused_operations( - ctx: Context, + ctx: Context, paused_operations: u8, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; msg!("perp market {}", perp_market.market_index); + if *ctx.accounts.admin.key != ctx.accounts.state.admin { + validate!( + paused_operations == PerpOperation::UpdateFunding as u8, + ErrorCode::DefaultError, + "signer must be admin", + )?; + } + perp_market.paused_operations = paused_operations; if perp_market.is_prediction_market() { @@ -3783,7 +3791,7 @@ pub fn handle_update_amm_jit_intensity( perp_market_valid(&ctx.accounts.perp_market) )] pub fn handle_update_perp_market_max_spread( - ctx: Context, + ctx: Context, max_spread: u32, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index d91c7e80bc..42c9e9f050 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1340,7 +1340,7 @@ pub mod drift { } pub fn update_perp_market_paused_operations( - ctx: Context, + ctx: Context, paused_operations: u8, ) -> Result<()> { handle_update_perp_market_paused_operations(ctx, paused_operations) @@ -1491,7 +1491,7 @@ pub mod drift { } pub fn update_perp_market_max_spread( - ctx: Context, + ctx: Context, max_spread: u32, ) -> Result<()> { handle_update_perp_market_max_spread(ctx, max_spread) From bfef3f4f2ec89350fe772b420b78b6e2a173cda7 Mon Sep 17 00:00:00 2001 From: lil perp Date: Tue, 14 Oct 2025 19:57:19 -0400 Subject: [PATCH 150/247] program: allow settling positive pnl expired pos during liquidation (#1959) * program: allow settling positive pnl expired pos during liquidation * fix --- programs/drift/src/controller/pnl.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index c08912beb5..11911e0356 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -353,8 +353,20 @@ pub fn settle_expired_position( ) -> DriftResult { validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + let position_index = match get_position_index(&user.perp_positions, perp_market_index) { + Ok(index) => index, + Err(_) => { + msg!("User has no position for market {}", perp_market_index); + return Ok(()); + } + }; + + let can_skip_margin_calc = user.perp_positions[position_index].base_asset_amount == 0 + && user.perp_positions[position_index].quote_asset_amount > 0; + // cannot settle pnl this way on a user who is in liquidation territory - if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?) + if !meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)? + && !can_skip_margin_calc { return Err(ErrorCode::InsufficientCollateralForSettlingPNL); } @@ -390,14 +402,6 @@ pub fn settle_expired_position( None, )?; - let position_index = match get_position_index(&user.perp_positions, perp_market_index) { - Ok(index) => index, - Err(_) => { - msg!("User has no position for market {}", perp_market_index); - return Ok(()); - } - }; - let quote_spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; let perp_market = &mut perp_market_map.get_ref_mut(&perp_market_index)?; validate!( From 7582b0b037593b7e0f3d312a2be2081b0d7a7a7e Mon Sep 17 00:00:00 2001 From: wphan Date: Tue, 14 Oct 2025 19:52:54 -0700 Subject: [PATCH 151/247] v2.142.0 --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82232c716b..e9cc3fb8aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +### Fixes + +### Breaking + +## [2.142.0] - 2025-10-14 + +### Features + - program: add titan to whitelisted swap programs ([#1952](https://github.com/drift-labs/protocol-v2/pull/1952)) - program: allow hot wallet to increase max spread and pause funding ([#1957](https://github.com/drift-labs/protocol-v2/pull/1957)) diff --git a/Cargo.lock b/Cargo.lock index 10989da811..4d0ca24568 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.141.0" +version = "2.142.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 7771b9c8e3..ba4ab87aeb 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.141.0" +version = "2.142.0" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index f823f96f68..c183e52a45 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0-beta.27", + "version": "2.142.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index ed162a931f..eed45293ca 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.141.0", + "version": "2.142.0", "name": "drift", "instructions": [ { From b4d4302d46c667bdaa947e9b3344fc9b9c76f6f1 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 03:01:29 +0000 Subject: [PATCH 152/247] sdk: release v2.143.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 3d464037fe..96ecb56281 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.142.0-beta.27 \ No newline at end of file +2.143.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c183e52a45..02280ffb4a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.142.0", + "version": "2.143.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 7c34e4283fcc30d1f9f524d2a3eaecf6ddbe7e7a Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Wed, 15 Oct 2025 20:29:06 +0800 Subject: [PATCH 153/247] rm println (#1962) --- programs/drift/src/state/perp_market.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 69d2e80f38..520af20258 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -739,9 +739,11 @@ impl PerpMarket { let oracle_plus_funding_basis = oracle_price.safe_add(last_funding_basis)?.cast::()?; let median_price = if last_fill_price > 0 { - println!( + msg!( "last_fill_price: {} oracle_plus_funding_basis: {} oracle_plus_basis_5min: {}", - last_fill_price, oracle_plus_funding_basis, oracle_plus_basis_5min + last_fill_price, + oracle_plus_funding_basis, + oracle_plus_basis_5min ); let mut prices = [ last_fill_price, From afd9feefbded0949b1e6eb27a86d81a65435fc57 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 15 Oct 2025 07:10:37 -0600 Subject: [PATCH 154/247] fix: missing params from per market lev --- sdk/src/driftClient.ts | 4 +++- sdk/src/types.ts | 2 ++ sdk/src/user.ts | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 96d917e9e1..21eb752908 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1569,7 +1569,8 @@ export class DriftClient { perpMarketIndex: number, marginRatio: number, subAccountId = 0, - txParams?: TxParams + txParams?: TxParams, + enteringHighLeverageMode?: boolean ): Promise { const ix = await this.getUpdateUserPerpPositionCustomMarginRatioIx( perpMarketIndex, @@ -4262,6 +4263,7 @@ export class DriftClient { referrerInfo?: ReferrerInfo, cancelExistingOrders?: boolean, settlePnl?: boolean, + positionMaxLev?: number, isolatedPositionDepositAmount?: BN ): Promise<{ cancelExistingOrdersTx?: Transaction | VersionedTransaction; diff --git a/sdk/src/types.ts b/sdk/src/types.ts index df9596f9fd..0a5d320d88 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1327,6 +1327,7 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + isolatedPositionDepositAmount?: BN | null; }; export type SignedMsgOrderParamsDelegateMessage = { @@ -1337,6 +1338,7 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + isolatedPositionDepositAmount?: BN | null; }; export type SignedMsgTriggerOrderParams = { diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 48b4a8306d..7a3b7440e7 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -984,6 +984,7 @@ export class User { const market = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); + if(!market) return unrealizedPnl; const oraclePriceData = this.getMMOracleDataForPerpMarket( market.marketIndex ); @@ -1584,6 +1585,8 @@ export class User { perpPosition.marketIndex ); + if(!market) return ZERO; + let valuationPrice = this.getOracleDataForPerpMarket( market.marketIndex ).price; @@ -2895,7 +2898,8 @@ export class User { targetMarketIndex: number, tradeSide: PositionDirection, isLp = false, - enterHighLeverageMode = undefined + enterHighLeverageMode = undefined, + maxMarginRatio = undefined ): { tradeSize: BN; oppositeSideTradeSize: BN } { let tradeSize = ZERO; let oppositeSideTradeSize = ZERO; From 04243e9220537cd6aa6daa8851c5fe8e4adf040e Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 15 Oct 2025 12:28:27 -0600 Subject: [PATCH 155/247] fix: cleanup potential mem leaks on grpc v2 (#1963) * fix: cleanup potential mem leaks on grpc v2 * fix: lint and prettier * fix: lint again * feat: higher load grpc client test * fix: lint --- ...test.ts => grpc-client-test-comparison.ts} | 0 sdk/scripts/single-grpc-client-test.ts | 226 ++++++++++++++++++ .../grpcDriftClientAccountSubscriberV2.ts | 47 +++- 3 files changed, 269 insertions(+), 4 deletions(-) rename sdk/scripts/{client-test.ts => grpc-client-test-comparison.ts} (100%) create mode 100644 sdk/scripts/single-grpc-client-test.ts diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/grpc-client-test-comparison.ts similarity index 100% rename from sdk/scripts/client-test.ts rename to sdk/scripts/grpc-client-test-comparison.ts diff --git a/sdk/scripts/single-grpc-client-test.ts b/sdk/scripts/single-grpc-client-test.ts new file mode 100644 index 0000000000..0aca8985a0 --- /dev/null +++ b/sdk/scripts/single-grpc-client-test.ts @@ -0,0 +1,226 @@ +import { DriftClient } from '../src/driftClient'; +import { grpcDriftClientAccountSubscriberV2 } from '../src/accounts/grpcDriftClientAccountSubscriberV2'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { DriftClientConfig } from '../src/driftClientConfig'; +import { + DRIFT_PROGRAM_ID, + PerpMarketAccount, + SpotMarketAccount, + Wallet, + OracleInfo, + decodeName, +} from '../src'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; +import dotenv from 'dotenv'; +import { + AnchorProvider, + Idl, + Program, + ProgramAccount, +} from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; + +const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; +const TOKEN = process.env.TOKEN; +const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + +async function initializeSingleGrpcClient() { + console.log('🚀 Initializing single gRPC Drift Client...'); + + const connection = new Connection(RPC_ENDPOINT); + const wallet = new Wallet(new Keypair()); + dotenv.config({ path: '../' }); + + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const provider = new AnchorProvider( + connection, + // @ts-ignore + wallet, + { + commitment: 'processed', + } + ); + + const program = new Program(driftIDL as Idl, programId, provider); + + // Get perp market accounts + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketProgramAccounts = allPerpMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].includes( + val.account.marketIndex + ) + ); + const perpMarketIndexes = perpMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + // Get spot market accounts + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketProgramAccounts = allSpotMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5].includes(val.account.marketIndex) + ); + const spotMarketIndexes = spotMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + // Get oracle infos + const seen = new Set(); + const oracleInfos: OracleInfo[] = []; + for (const acct of perpMarketProgramAccounts) { + const key = `${acct.account.amm.oracle.toBase58()}-${ + Object.keys(acct.account.amm.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.amm.oracle, + source: acct.account.amm.oracleSource, + }); + } + } + for (const acct of spotMarketProgramAccounts) { + const key = `${acct.account.oracle.toBase58()}-${ + Object.keys(acct.account.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.oracle, + source: acct.account.oracleSource, + }); + } + } + + console.log(`📊 Markets: ${perpMarketIndexes.length} perp, ${spotMarketIndexes.length} spot`); + console.log(`🔮 Oracles: ${oracleInfos.length}`); + + const baseAccountSubscription = { + type: 'grpc' as const, + grpcConfigs: { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: CommitmentLevel.PROCESSED, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }, + }; + + const config: DriftClientConfig = { + connection, + wallet, + programID: new PublicKey(DRIFT_PROGRAM_ID), + accountSubscription: { + ...baseAccountSubscription, + driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, + }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + }; + + const client = new DriftClient(config); + + // Set up event listeners + const eventCounts = { + stateAccountUpdate: 0, + perpMarketAccountUpdate: 0, + spotMarketAccountUpdate: 0, + oraclePriceUpdate: 0, + update: 0, + }; + + console.log('🎧 Setting up event listeners...'); + + client.eventEmitter.on('stateAccountUpdate', (_data) => { + eventCounts.stateAccountUpdate++; + }); + + client.eventEmitter.on('perpMarketAccountUpdate', (_data) => { + eventCounts.perpMarketAccountUpdate++; + }); + + client.eventEmitter.on('spotMarketAccountUpdate', (_data) => { + eventCounts.spotMarketAccountUpdate++; + }); + + client.eventEmitter.on('oraclePriceUpdate', (_publicKey, _source, _data) => { + eventCounts.oraclePriceUpdate++; + }); + + client.accountSubscriber.eventEmitter.on('update', () => { + eventCounts.update++; + }); + + // Subscribe + console.log('🔗 Subscribing to accounts...'); + await client.subscribe(); + + console.log('✅ Client subscribed successfully!'); + console.log('🚀 Starting high-load testing (50 reads/sec per perp market)...'); + + // High-frequency load testing - 50 reads per second per perp market + const loadTestInterval = setInterval(async () => { + try { + // Test getPerpMarketAccount for each perp market (50 times per second per market) + for (const marketIndex of perpMarketIndexes) { + const perpMarketAccount = client.getPerpMarketAccount(marketIndex); + console.log("perpMarketAccount name: ", decodeName(perpMarketAccount.name)); + console.log("perpMarketAccount data: ", JSON.stringify({ + marketIndex: perpMarketAccount.marketIndex, + name: decodeName(perpMarketAccount.name), + baseAssetReserve: perpMarketAccount.amm.baseAssetReserve.toString(), + quoteAssetReserve: perpMarketAccount.amm.quoteAssetReserve.toString() + })); + } + + // Test getMMOracleDataForPerpMarket for each perp market (50 times per second per market) + for (const marketIndex of perpMarketIndexes) { + try { + const oracleData = client.getMMOracleDataForPerpMarket(marketIndex); + console.log("oracleData price: ", oracleData.price.toString()); + console.log("oracleData: ", JSON.stringify({ + price: oracleData.price.toString(), + confidence: oracleData.confidence?.toString(), + slot: oracleData.slot?.toString() + })); + } catch (error) { + // Ignore errors for load testing + } + } + } catch (error) { + console.error('Load test error:', error); + } + }, 20); // 50 times per second = 1000ms / 50 = 20ms interval + + // Log periodic stats + const statsInterval = setInterval(() => { + console.log('\n📈 Event Counts:', eventCounts); + console.log(`⏱️ Client subscribed: ${client.isSubscribed}`); + console.log(`🔗 Account subscriber subscribed: ${client.accountSubscriber.isSubscribed}`); + console.log(`🔥 Load: ${perpMarketIndexes.length * 50 * 2} reads/sec (${perpMarketIndexes.length} markets × 50 getPerpMarketAccount + 50 getMMOracleDataForPerpMarket)`); + }, 5000); + + // Handle shutdown signals - just exit without cleanup since they never unsubscribe + process.on('SIGINT', () => { + console.log('\n🛑 Shutting down...'); + clearInterval(loadTestInterval); + clearInterval(statsInterval); + process.exit(0); + }); + + process.on('SIGTERM', () => { + console.log('\n🛑 Shutting down...'); + clearInterval(loadTestInterval); + clearInterval(statsInterval); + process.exit(0); + }); + + return client; +} + +initializeSingleGrpcClient().catch(console.error); diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 9abfc93c21..ccea364002 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -227,10 +227,11 @@ export class grpcDriftClientAccountSubscriberV2 o.source === oracleInfo.source && o.publicKey.equals(oracleInfo.publicKey) ); - if (!exists) { - this.oracleInfos = this.oracleInfos.concat(oracleInfo); + if (exists) { + return true; // Already exists, don't add duplicate } + this.oracleInfos = this.oracleInfos.concat(oracleInfo); this.oracleMultiSubscriber?.addAccounts([oracleInfo.publicKey]); return true; @@ -708,11 +709,37 @@ export class grpcDriftClientAccountSubscriberV2 await this.perpMarketsSubscriber.removeAccounts( perpMarketPubkeysToRemove ); + // Clean up the mapping for removed perp markets + for (const pubkey of perpMarketPubkeysToRemove) { + const pubkeyString = pubkey.toBase58(); + for (const [ + marketIndex, + accountPubkey, + ] of this.perpMarketIndexToAccountPubkeyMap.entries()) { + if (accountPubkey === pubkeyString) { + this.perpMarketIndexToAccountPubkeyMap.delete(marketIndex); + this.perpOracleMap.delete(marketIndex); + this.perpOracleStringMap.delete(marketIndex); + break; + } + } + } } // Remove accounts in batches - oracles if (oraclePubkeysToRemove.length > 0) { await this.oracleMultiSubscriber.removeAccounts(oraclePubkeysToRemove); + // Clean up oracle data for removed oracles by finding their sources + for (const pubkey of oraclePubkeysToRemove) { + // Find the oracle source by checking oracleInfos + const oracleInfo = this.oracleInfos.find((info) => + info.publicKey.equals(pubkey) + ); + if (oracleInfo) { + const oracleId = getOracleId(pubkey, oracleInfo.source); + this.oracleIdToOracleDataMap.delete(oracleId); + } + } } } @@ -731,13 +758,25 @@ export class grpcDriftClientAccountSubscriberV2 } async unsubscribe(): Promise { - if (this.isSubscribed) { + if (!this.isSubscribed) { return; } - await this.stateAccountSubscriber.unsubscribe(); + this.isSubscribed = false; + this.isSubscribing = false; + + await this.stateAccountSubscriber?.unsubscribe(); await this.unsubscribeFromOracles(); await this.perpMarketsSubscriber?.unsubscribe(); await this.spotMarketsSubscriber?.unsubscribe(); + + // Clean up all maps to prevent memory leaks + this.perpMarketIndexToAccountPubkeyMap.clear(); + this.spotMarketIndexToAccountPubkeyMap.clear(); + this.oracleIdToOracleDataMap.clear(); + this.perpOracleMap.clear(); + this.perpOracleStringMap.clear(); + this.spotOracleMap.clear(); + this.spotOracleStringMap.clear(); } } From 7d6375351d17dfbc9fd4a6ceb63d45183915f4b9 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:34:01 +0000 Subject: [PATCH 156/247] sdk: release v2.143.0-beta.1 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 96ecb56281..b807294192 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.0 \ No newline at end of file +2.143.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 02280ffb4a..9324ff0ae6 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.0", + "version": "2.143.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 788bdef624df22ad1b8a7d29f39b2748aa7e62f4 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 15 Oct 2025 14:27:29 -0600 Subject: [PATCH 157/247] feat: zero out account withdraw from perp position --- sdk/src/driftClient.ts | 55 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 21eb752908..04a61ce932 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -46,6 +46,7 @@ import { PhoenixV1FulfillmentConfigAccount, PlaceAndTakeOrderSuccessCondition, PositionDirection, + PositionFlag, ReferrerInfo, ReferrerNameAccount, SerumV3FulfillmentConfigAccount, @@ -264,6 +265,51 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } + private async getPostIxsForIsolatedWithdrawAfterMarketOrder( + orderParams: OptionalOrderParams, + userAccount: UserAccount + ): Promise { + const postIxs: TransactionInstruction[] = []; + const perpPosition = userAccount.perpPositions.find( + (p) => p.marketIndex === orderParams.marketIndex + ); + if (!perpPosition) return postIxs; + + const isIsolated = + (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0; + if (!isIsolated) return postIxs; + + const currentBase = perpPosition.baseAssetAmount; + if (currentBase.eq(ZERO)) return postIxs; + + const signedOrderBase = + orderParams.direction === PositionDirection.LONG + ? orderParams.baseAssetAmount + : (orderParams.baseAssetAmount as BN).neg(); + const postBase = currentBase.add(signedOrderBase as BN); + if (!postBase.eq(ZERO)) return postIxs; + + const withdrawAmount = this.getIsolatedPerpPositionTokenAmount( + orderParams.marketIndex, + userAccount.subAccountId + ); + if (withdrawAmount.lte(ZERO)) return postIxs; + + const userTokenAccount = await this.getAssociatedTokenAccount( + QUOTE_SPOT_MARKET_INDEX + ); + postIxs.push( + await this.getWithdrawFromIsolatedPerpPositionIx( + withdrawAmount, + orderParams.marketIndex, + userTokenAccount, + userAccount.subAccountId + ) + ); + + return postIxs; + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -4303,13 +4349,18 @@ export class DriftClient { ); } + // Build post-order instructions for perp (e.g., withdraw isolated margin on close) + const postIxs: TransactionInstruction[] = isVariant(orderParams.marketType, 'perp') + ? await this.getPostIxsForIsolatedWithdrawAfterMarketOrder(orderParams, userAccount) + : []; + ixPromisesForTxs.marketOrderTx = (async () => { const placeOrdersIx = await this.getPlaceOrdersIx( [orderParams, ...bracketOrdersParams], userAccount.subAccountId ); - if (preIxs.length) { - return [...preIxs, placeOrdersIx] as unknown as TransactionInstruction; + if (preIxs.length || postIxs.length) { + return [...preIxs, placeOrdersIx, ...postIxs] as unknown as TransactionInstruction; } return placeOrdersIx; })(); From 5f90487d6a094f28c695dd9e4b7e8b7419dbd221 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 15 Oct 2025 15:38:11 -0600 Subject: [PATCH 158/247] fix: available positions logic update for iso --- sdk/src/math/position.ts | 9 ++++++++- sdk/src/types.ts | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/src/math/position.ts b/sdk/src/math/position.ts index 0cae66ab05..6a5da647ec 100644 --- a/sdk/src/math/position.ts +++ b/sdk/src/math/position.ts @@ -14,6 +14,7 @@ import { PositionDirection, PerpPosition, SpotMarketAccount, + PositionFlag, } from '../types'; import { calculateUpdatedAMM, @@ -243,10 +244,16 @@ export function positionIsAvailable(position: PerpPosition): boolean { position.baseAssetAmount.eq(ZERO) && position.openOrders === 0 && position.quoteAssetAmount.eq(ZERO) && - position.lpShares.eq(ZERO) + position.lpShares.eq(ZERO) && + position.isolatedPositionScaledBalance.eq(ZERO) + && !positionIsBeingLiquidated(position) ); } +export function positionIsBeingLiquidated(position: PerpPosition): boolean { + return (position.positionFlag & (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) > 0; +} + /** * * @param userPosition diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 0a5d320d88..6acd8e0a7f 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -504,7 +504,6 @@ export type LiquidationRecord = { liquidatePerpPnlForDeposit: LiquidatePerpPnlForDepositRecord; perpBankruptcy: PerpBankruptcyRecord; spotBankruptcy: SpotBankruptcyRecord; - bitFlags: number; }; export class LiquidationType { From a8a4011c95539ed30e60dab3e8ae64795b38a065 Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:12:48 -0400 Subject: [PATCH 159/247] sdk: add-max-size-for-target-liability-weight (#1961) * sdk: add-max-size-for-target-liability-weight * cleanup * cleanup --------- Co-authored-by: Nick Caradonna --- sdk/src/math/orders.ts | 71 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index de5486694b..2dc9196ab8 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -9,7 +9,13 @@ import { ProtectedMakerParams, MarketTypeStr, } from '../types'; -import { ZERO, TWO, ONE } from '../constants/numericConstants'; +import { + ZERO, + TWO, + ONE, + SPOT_MARKET_IMF_PRECISION, + MARGIN_PRECISION, +} from '../constants/numericConstants'; import { BN } from '@coral-xyz/anchor'; import { MMOraclePriceData, OraclePriceData } from '../oracles/types'; import { @@ -22,6 +28,7 @@ import { calculateMaxBaseAssetAmountToTrade, calculateUpdatedAMM, } from './amm'; +import { calculateSizePremiumLiabilityWeight } from './margin'; export function isOrderRiskIncreasing(user: User, order: Order): boolean { if (!isVariant(order.status, 'open')) { @@ -411,3 +418,65 @@ export function calculateOrderBaseAssetAmount( return BN.min(BN.max(existingBaseAssetAmount, ZERO), order.baseAssetAmount); } } + +// ---------- inverse ---------- +/** + * Invert the size-premium liability weight: given a target margin ratio (liability weight), + * return the max `size` (AMM_RESERVE_PRECISION units) that still yields <= target. + * + * Returns: + * - BN size (>=0) if bounded + * - null if impossible (target < liabilityWeight) OR imfFactor == 0 (unbounded) + */ +export function maxSizeForTargetLiabilityWeightBN( + target: BN, + imfFactor: BN, + liabilityWeight: BN +): BN | null { + if (target.lt(liabilityWeight)) return null; + if (imfFactor.isZero()) return null; + + const base = liabilityWeight.muln(4).divn(5); + + const denom = new BN(100_000) + .mul(SPOT_MARKET_IMF_PRECISION) + .div(MARGIN_PRECISION); + if (denom.isZero()) + throw new Error('denom=0: bad precision/spotImfPrecision'); + + const allowedInc = target.gt(base) ? target.sub(base) : ZERO; + + const maxSqrt = allowedInc.mul(denom).div(imfFactor); + + if (maxSqrt.lte(ZERO)) { + const fitsZero = calculateSizePremiumLiabilityWeight( + ZERO, + imfFactor, + liabilityWeight, + MARGIN_PRECISION + ).lte(target); + return fitsZero ? ZERO : null; + } + + let hi = maxSqrt.mul(maxSqrt).sub(ONE).divn(10); + if (hi.isNeg()) hi = ZERO; + + let lo = ZERO; + while (lo.lt(hi)) { + const mid = lo.add(hi).add(ONE).divn(2); // upper mid to prevent infinite loop + if ( + calculateSizePremiumLiabilityWeight( + mid, + imfFactor, + liabilityWeight, + MARGIN_PRECISION + ).lte(target) + ) { + lo = mid; + } else { + hi = mid.sub(ONE); + } + } + + return lo; +} From c315267cf6c2ea2c0f221e75d42a9a4e636ec0da Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:18:32 +0000 Subject: [PATCH 160/247] sdk: release v2.143.0-beta.2 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index b807294192..851e0e4829 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.1 \ No newline at end of file +2.143.0-beta.2 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 9324ff0ae6..389bd06f0b 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.1", + "version": "2.143.0-beta.2", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From babdddfbb11540c8c78a74b677854b9fa236494f Mon Sep 17 00:00:00 2001 From: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:19:16 -0400 Subject: [PATCH 161/247] add includeOpenOrders option for perp position health (#1966) --- sdk/src/user.ts | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index a36820306d..92512243fd 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -3590,11 +3590,13 @@ export class User { perpPosition, oraclePriceData, quoteOraclePriceData, + includeOpenOrders = true, }: { marginCategory: MarginCategory; perpPosition: PerpPosition; oraclePriceData?: OraclePriceData; quoteOraclePriceData?: OraclePriceData; + includeOpenOrders?: boolean; }): HealthComponent { const perpMarket = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex @@ -3603,14 +3605,25 @@ export class User { oraclePriceData || this.driftClient.getOracleDataForPerpMarket(perpMarket.marketIndex); const oraclePrice = _oraclePriceData.price; - const { - worstCaseBaseAssetAmount: worstCaseBaseAmount, - worstCaseLiabilityValue, - } = calculateWorstCasePerpLiabilityValue( - perpPosition, - perpMarket, - oraclePrice - ); + + let worstCaseBaseAmount; + let worstCaseLiabilityValue; + if (includeOpenOrders) { + const worstCaseIncludeOrders = calculateWorstCasePerpLiabilityValue( + perpPosition, + perpMarket, + oraclePrice + ); + worstCaseBaseAmount = worstCaseIncludeOrders.worstCaseBaseAssetAmount; + worstCaseLiabilityValue = worstCaseIncludeOrders.worstCaseLiabilityValue; + } else { + worstCaseBaseAmount = perpPosition.baseAssetAmount; + worstCaseLiabilityValue = calculatePerpLiabilityValue( + perpPosition.baseAssetAmount, + oraclePrice, + isVariant(perpMarket.contractType, 'prediction') + ); + } const userCustomMargin = Math.max( perpPosition.maxMarginRatio, From ab911c24ebdd05e32e93e00e8093fda4bb7996b7 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 01:23:43 +0000 Subject: [PATCH 162/247] sdk: release v2.143.0-beta.3 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 851e0e4829..c10ef2415c 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.2 \ No newline at end of file +2.143.0-beta.3 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 389bd06f0b..9aba12a95e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.2", + "version": "2.143.0-beta.3", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From a8be60441e4e16484038d7ec5ebd355661cc3a11 Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Thu, 16 Oct 2025 16:33:35 +0800 Subject: [PATCH 163/247] refactor(sdk): add fetch revenue share accounts, overrides for revenue share actions --- sdk/src/accounts/fetch.ts | 51 ++++++++++++++++++++++++++++++++++++++- sdk/src/driftClient.ts | 25 +++++++++++++------ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/sdk/src/accounts/fetch.ts b/sdk/src/accounts/fetch.ts index cc92eb5e1d..63c7bfd7f0 100644 --- a/sdk/src/accounts/fetch.ts +++ b/sdk/src/accounts/fetch.ts @@ -1,6 +1,13 @@ import { Connection, PublicKey } from '@solana/web3.js'; -import { UserAccount, UserStatsAccount } from '../types'; import { + RevenueShareAccount, + RevenueShareEscrowAccount, + UserAccount, + UserStatsAccount, +} from '../types'; +import { + getRevenueShareAccountPublicKey, + getRevenueShareEscrowAccountPublicKey, getUserAccountPublicKey, getUserStatsAccountPublicKey, } from '../addresses/pda'; @@ -64,3 +71,45 @@ export async function fetchUserStatsAccount( ) as UserStatsAccount) : undefined; } + +export async function fetchRevenueShareAccount( + connection: Connection, + program: Program, + authority: PublicKey +): Promise { + const revenueShareAccountPublicKey = getRevenueShareAccountPublicKey( + program.programId, + authority + ); + const accountInfo = await connection.getAccountInfo( + revenueShareAccountPublicKey + ); + if (!accountInfo) return null; + return program.account.revenueShare.coder.accounts.decode( + 'RevenueShare', + accountInfo.data + ) as RevenueShareAccount; +} + +export async function fetchRevenueShareEscrowAccount( + connection: Connection, + program: Program, + authority: PublicKey +): Promise { + const revenueShareEscrowPubKey = getRevenueShareEscrowAccountPublicKey( + program.programId, + authority + ); + + const escrow = await connection.getAccountInfo(revenueShareEscrowPubKey); + + if (!escrow) return null; + + const escrowAccount = + program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + escrow.data + ) as RevenueShareEscrowAccount; + + return escrowAccount; +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 0a86382d5b..4c71a49abf 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1255,7 +1255,10 @@ export class DriftClient { } public async getInitializeRevenueShareIx( - authority: PublicKey + authority: PublicKey, + overrides?: { + payer?: PublicKey; + } ): Promise { const revenueShare = getRevenueShareAccountPublicKey( this.program.programId, @@ -1265,7 +1268,7 @@ export class DriftClient { accounts: { revenueShare, authority, - payer: this.wallet.publicKey, + payer: overrides?.payer ?? this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, @@ -1288,7 +1291,10 @@ export class DriftClient { public async getInitializeRevenueShareEscrowIx( authority: PublicKey, - numOrders: number + numOrders: number, + overrides?: { + payer?: PublicKey; + } ): Promise { const escrow = getRevenueShareEscrowAccountPublicKey( this.program.programId, @@ -1298,7 +1304,7 @@ export class DriftClient { accounts: { escrow, authority, - payer: this.wallet.publicKey, + payer: overrides?.payer ?? this.wallet.publicKey, userStats: getUserStatsAccountPublicKey( this.program.programId, authority @@ -1411,9 +1417,14 @@ export class DriftClient { public async getChangeApprovedBuilderIx( builder: PublicKey, maxFeeTenthBps: number, - add: boolean + add: boolean, + overrides?: { + authority?: PublicKey; + payer?: PublicKey; + } ): Promise { - const authority = this.wallet.publicKey; + const authority = overrides?.authority ?? this.wallet.publicKey; + const payer = overrides?.payer ?? this.wallet.publicKey; const escrow = getRevenueShareEscrowAccountPublicKey( this.program.programId, authority @@ -1426,7 +1437,7 @@ export class DriftClient { accounts: { escrow, authority, - payer: this.wallet.publicKey, + payer, systemProgram: anchor.web3.SystemProgram.programId, }, } From 151cd42483244825bf4ee8f20ca94e65bbf263ac Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:38:53 +0000 Subject: [PATCH 164/247] sdk: release v2.143.0-beta.4 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index c10ef2415c..839ff6864c 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.3 \ No newline at end of file +2.143.0-beta.4 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 9aba12a95e..c9a6dc9d4c 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.3", + "version": "2.143.0-beta.4", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From e11a2ec81974191410558dcdf047715134949ebe Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Sun, 19 Oct 2025 15:40:12 -0600 Subject: [PATCH 165/247] feat: iso position txs cleanup + ix ordering --- sdk/src/driftClient.ts | 163 +++++++++++++++++++++-------------------- sdk/src/types.ts | 8 +- 2 files changed, 86 insertions(+), 85 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 714221e11f..d39f70dcd9 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -274,49 +274,44 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } - private async getPostIxsForIsolatedWithdrawAfterMarketOrder( + private async getPrePlaceOrderIxs( orderParams: OptionalOrderParams, - userAccount: UserAccount + userAccount: UserAccount, + options?: { positionMaxLev?: number; isolatedPositionDepositAmount?: BN } ): Promise { - const postIxs: TransactionInstruction[] = []; - const perpPosition = userAccount.perpPositions.find( - (p) => p.marketIndex === orderParams.marketIndex - ); - if (!perpPosition) return postIxs; - - const isIsolated = - (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0; - if (!isIsolated) return postIxs; - - const currentBase = perpPosition.baseAssetAmount; - if (currentBase.eq(ZERO)) return postIxs; + const preIxs: TransactionInstruction[] = []; - const signedOrderBase = - orderParams.direction === PositionDirection.LONG - ? orderParams.baseAssetAmount - : (orderParams.baseAssetAmount as BN).neg(); - const postBase = currentBase.add(signedOrderBase as BN); - if (!postBase.eq(ZERO)) return postIxs; + if (isVariant(orderParams.marketType, 'perp')) { + const { positionMaxLev, isolatedPositionDepositAmount } = options ?? {}; - const withdrawAmount = this.getIsolatedPerpPositionTokenAmount( - orderParams.marketIndex, - userAccount.subAccountId - ); - if (withdrawAmount.lte(ZERO)) return postIxs; + if ( + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(orderParams, userAccount) + ) { + preIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + userAccount.subAccountId + ) + ); + } - const userTokenAccount = await this.getAssociatedTokenAccount( - QUOTE_SPOT_MARKET_INDEX - ); - postIxs.push( - await this.getWithdrawFromIsolatedPerpPositionIx( - withdrawAmount, - orderParams.marketIndex, - userTokenAccount, - userAccount.subAccountId - ) - ); + if (positionMaxLev) { + const marginRatio = Math.floor( + (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() + ); + preIxs.push( + await this.getUpdateUserPerpPositionCustomMarginRatioIx( + orderParams.marketIndex, + marginRatio, + userAccount.subAccountId + ) + ); + } + } - return postIxs; + return preIxs; } public set isSubscribed(val: boolean) { @@ -4206,16 +4201,26 @@ export class DriftClient { subAccountId?: number, txParams?: TxParams ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + const userAccount = this.getUserAccount(subAccountId); + const settleIx = await this.settleMultiplePNLsIx( + userAccountPublicKey, + userAccount, + [perpMarketIndex], + SettlePnlMode.TRY_SETTLE + ); + const withdrawIx = await this.getWithdrawFromIsolatedPerpPositionIx( + amount, + perpMarketIndex, + userTokenAccount, + subAccountId + ); const { txSig } = await this.sendTransaction( - await this.buildTransaction( - await this.getWithdrawFromIsolatedPerpPositionIx( - amount, - perpMarketIndex, - userTokenAccount, - subAccountId - ), - txParams - ) + await this.buildTransaction([settleIx, withdrawIx], txParams) ); return txSig; } @@ -4588,47 +4593,25 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - const preIxs: TransactionInstruction[] = []; - if ( - isVariant(orderParams.marketType, 'perp') && - isolatedPositionDepositAmount?.gt?.(ZERO) - ) { - preIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx( - isolatedPositionDepositAmount as BN, - orderParams.marketIndex, - userAccount.subAccountId - ) - ); - } - - // Build post-order instructions for perp (e.g., withdraw isolated margin on close) - const postIxs: TransactionInstruction[] = isVariant(orderParams.marketType, 'perp') - ? await this.getPostIxsForIsolatedWithdrawAfterMarketOrder(orderParams, userAccount) - : []; + const preIxs: TransactionInstruction[] = await this.getPrePlaceOrderIxs( + orderParams, + userAccount, + { + positionMaxLev, + isolatedPositionDepositAmount, + } + ); ixPromisesForTxs.marketOrderTx = (async () => { const placeOrdersIx = await this.getPlaceOrdersIx( [orderParams, ...bracketOrdersParams], userAccount.subAccountId ); - if (preIxs.length || postIxs.length) { - return [...preIxs, placeOrdersIx, ...postIxs] as unknown as TransactionInstruction; + if (preIxs.length) { + return [...preIxs, placeOrdersIx] as unknown as TransactionInstruction; } return placeOrdersIx; })(); - const marketOrderTxIxs = positionMaxLev - ? this.getPlaceOrdersAndSetPositionMaxLevIx( - [orderParams, ...bracketOrdersParams], - positionMaxLev, - userAccount.subAccountId - ) - : this.getPlaceOrdersIx( - [orderParams, ...bracketOrdersParams], - userAccount.subAccountId - ); - - ixPromisesForTxs.marketOrderTx = marketOrderTxIxs; /* Cancel open orders in market if requested */ if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { @@ -5205,7 +5188,8 @@ export class DriftClient { params, txParams, subAccountId, - optionalIxs + optionalIxs, + isolatedPositionDepositAmount ) ).placeOrdersTx, [], @@ -5363,8 +5347,7 @@ export class DriftClient { const marginRatio = Math.floor( (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() ); - - // TODO: Handle multiple markets? + // Keep existing behavior but note: prefer using getPostPlaceOrderIxs path const setPositionMaxLevIxs = await this.getUpdateUserPerpPositionCustomMarginRatioIx( readablePerpMarketIndex[0], @@ -11360,4 +11343,22 @@ export class DriftClient { forceVersionedTransaction, }); } + + isOrderIncreasingPosition( + orderParams: OptionalOrderParams, + userAccount: UserAccount + ): boolean { + const perpPosition = userAccount.perpPositions.find( + (p) => p.marketIndex === orderParams.marketIndex + ); + if (!perpPosition) return true; + + const currentBase = perpPosition.baseAssetAmount; + if (currentBase.eq(ZERO)) return true; + + return currentBase + .add(orderParams.baseAssetAmount) + .abs() + .gt(currentBase.abs()); + } } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index dc84698abd..dd4a9d8b8c 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -585,7 +585,7 @@ export type SpotBankruptcyRecord = { }; export class LiquidationBitFlag { - static readonly IsolatedPosition = 1; + static readonly IsolatedPosition = 1; } export type SettlePnlRecord = { @@ -1310,7 +1310,7 @@ export type OptionalOrderParams = { } & NecessaryOrderParams; export type PerpOrderIsolatedExtras = { - isolatedPositionDepositAmount?: BN; + isolatedPositionDepositAmount?: BN; }; export type ModifyOrderParams = { @@ -1350,7 +1350,7 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; - isolatedPositionDepositAmount?: BN | null; + isolatedPositionDeposit?: BN | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; }; @@ -1363,7 +1363,7 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; - isolatedPositionDepositAmount?: BN | null; + isolatedPositionDeposit?: BN | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; }; From 3d9f823aa7c3f0dc9b7d86d531df8e253964713c Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 20 Oct 2025 23:29:08 -0600 Subject: [PATCH 166/247] feat: onchain props for signed msg orders + idl update --- programs/drift/src/state/order_params.rs | 2 ++ sdk/src/idl/drift.json | 28 +++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 95ccf8424d..e2dd59d866 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -875,6 +875,7 @@ pub struct SignedMsgOrderParamsMessage { pub max_margin_ratio: Option, pub builder_idx: Option, pub builder_fee_tenth_bps: Option, + pub isolated_position_deposit: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] @@ -888,6 +889,7 @@ pub struct SignedMsgOrderParamsDelegateMessage { pub max_margin_ratio: Option, pub builder_idx: Option, pub builder_fee_tenth_bps: Option, + pub isolated_position_deposit: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 60441df3d2..7469e012bb 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -10491,6 +10491,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -10556,6 +10562,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -16962,42 +16974,42 @@ "msg": "Invalid Isolated Perp Market" }, { - "code": 6317, + "code": 6318, "name": "InvalidRevenueShareResize", "msg": "Invalid RevenueShare resize" }, { - "code": 6318, + "code": 6319, "name": "BuilderRevoked", "msg": "Builder has been revoked" }, { - "code": 6319, + "code": 6320, "name": "InvalidBuilderFee", "msg": "Builder fee is greater than max fee bps" }, { - "code": 6320, + "code": 6321, "name": "RevenueShareEscrowAuthorityMismatch", "msg": "RevenueShareEscrow authority mismatch" }, { - "code": 6321, + "code": 6322, "name": "RevenueShareEscrowOrdersAccountFull", "msg": "RevenueShareEscrow has too many active orders" }, { - "code": 6322, + "code": 6323, "name": "InvalidRevenueShareAccount", "msg": "Invalid RevenueShareAccount" }, { - "code": 6323, + "code": 6324, "name": "CannotRevokeBuilderWithOpenOrders", "msg": "Cannot revoke builder with open orders" }, { - "code": 6324, + "code": 6325, "name": "UnableToLoadRevenueShareAccount", "msg": "Unable to load builder account" } From ec328b18ec142e4594bf7333da6c7d1cfaa17859 Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:32:59 -0700 Subject: [PATCH 167/247] Mm oracle sdk patch (#1977) * patch mm oracle price data selection * patch the patch * prettify --- sdk/src/driftClient.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 4c71a49abf..bf80ca94f9 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -168,7 +168,11 @@ import { getMarinadeDepositIx, getMarinadeFinanceProgram } from './marinade'; import { getOrderParams, isUpdateHighLeverageMode } from './orderParams'; import { numberToSafeBN } from './math/utils'; import { TransactionParamProcessor } from './tx/txParamProcessor'; -import { isOracleValid, trimVaaSignatures } from './math/oracles'; +import { + isOracleTooDivergent, + isOracleValid, + trimVaaSignatures, +} from './math/oracles'; import { TxHandler } from './tx/txHandler'; import { DEFAULT_RECEIVER_PROGRAM_ID, @@ -9384,12 +9388,22 @@ export class DriftClient { isExchangeOracleMoreRecent = false; } + const conf = getOracleConfidenceFromMMOracleData( + perpMarket.amm.mmOraclePrice, + oracleData + ); + if ( - !isOracleValid( - perpMarket, - oracleData, + isOracleTooDivergent( + perpMarket.amm, + { + price: perpMarket.amm.mmOraclePrice, + slot: perpMarket.amm.mmOracleSlot, + confidence: conf, + hasSufficientNumberOfDataPoints: true, + }, stateAccountAndSlot.data.oracleGuardRails, - stateAccountAndSlot.slot + perpMarket.amm.mmOracleSlot ) || perpMarket.amm.mmOraclePrice.eq(ZERO) || isExchangeOracleMoreRecent || @@ -9397,10 +9411,6 @@ export class DriftClient { ) { return { ...oracleData, isMMOracleActive }; } else { - const conf = getOracleConfidenceFromMMOracleData( - perpMarket.amm.mmOraclePrice, - oracleData - ); return { price: perpMarket.amm.mmOraclePrice, slot: perpMarket.amm.mmOracleSlot, From 63ca710e1909f49f5f7f46f26feb8424ead8428f Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:38:15 +0000 Subject: [PATCH 168/247] sdk: release v2.143.0-beta.5 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 839ff6864c..65d412d7d6 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.4 \ No newline at end of file +2.143.0-beta.5 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c9a6dc9d4c..2c422df115 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.4", + "version": "2.143.0-beta.5", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From de06f466c0c22c7426648bc2d75915b93dbf898a Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:35:29 -0700 Subject: [PATCH 169/247] is oracle divergent sdk fix (#1978) * is oracle divergent sdk fix * remove console log * linter fix * patch --- sdk/src/driftClient.ts | 3 +-- sdk/src/math/oracles.ts | 27 +++++++-------------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index bf80ca94f9..2d1ecb13ae 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -9402,8 +9402,7 @@ export class DriftClient { confidence: conf, hasSufficientNumberOfDataPoints: true, }, - stateAccountAndSlot.data.oracleGuardRails, - perpMarket.amm.mmOracleSlot + stateAccountAndSlot.data.oracleGuardRails ) || perpMarket.amm.mmOraclePrice.eq(ZERO) || isExchangeOracleMoreRecent || diff --git a/sdk/src/math/oracles.ts b/sdk/src/math/oracles.ts index 8873c66150..6575ed7a1d 100644 --- a/sdk/src/math/oracles.ts +++ b/sdk/src/math/oracles.ts @@ -10,7 +10,6 @@ import { OraclePriceData } from '../oracles/types'; import { BID_ASK_SPREAD_PRECISION, MARGIN_PRECISION, - PRICE_PRECISION, ONE, ZERO, FIVE_MINUTE, @@ -97,29 +96,17 @@ export function isOracleValid( export function isOracleTooDivergent( amm: AMM, oraclePriceData: OraclePriceData, - oracleGuardRails: OracleGuardRails, - now: BN + oracleGuardRails: OracleGuardRails ): boolean { - const sinceLastUpdate = now.sub( - amm.historicalOracleData.lastOraclePriceTwapTs - ); - const sinceStart = BN.max(ZERO, FIVE_MINUTE.sub(sinceLastUpdate)); - const oracleTwap5min = amm.historicalOracleData.lastOraclePriceTwap5Min - .mul(sinceStart) - .add(oraclePriceData.price) - .mul(sinceLastUpdate) - .div(sinceStart.add(sinceLastUpdate)); - - const oracleSpread = oracleTwap5min.sub(oraclePriceData.price); - const oracleSpreadPct = oracleSpread.mul(PRICE_PRECISION).div(oracleTwap5min); - + const oracleSpreadPct = oraclePriceData.price + .sub(amm.historicalOracleData.lastOraclePriceTwap5Min) + .mul(PERCENTAGE_PRECISION) + .div(amm.historicalOracleData.lastOraclePriceTwap5Min); const maxDivergence = BN.max( - oracleGuardRails.priceDivergence.markOraclePercentDivergence, - PERCENTAGE_PRECISION.div(new BN(10)) + oracleGuardRails.priceDivergence.oracleTwap5MinPercentDivergence, + PERCENTAGE_PRECISION.div(new BN(2)) ); - const tooDivergent = oracleSpreadPct.abs().gte(maxDivergence); - return tooDivergent; } From e756ed11506622762be08dbd0218f7f003288f7c Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:41:56 +0000 Subject: [PATCH 170/247] sdk: release v2.143.0-beta.6 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 65d412d7d6..a95b88f007 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.5 \ No newline at end of file +2.143.0-beta.6 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 2c422df115..0bb7ccd494 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.5", + "version": "2.143.0-beta.6", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 9650603e66431fb3f8c0485b5760f5cca0711edf Mon Sep 17 00:00:00 2001 From: jordy25519 Date: Wed, 22 Oct 2025 08:52:49 +0800 Subject: [PATCH 171/247] use custom msg! macro for drift-rs (#1975) --- programs/drift/src/math/amm_spread.rs | 4 ++-- programs/drift/src/state/perp_market.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/math/amm_spread.rs b/programs/drift/src/math/amm_spread.rs index cb90937315..7007722932 100644 --- a/programs/drift/src/math/amm_spread.rs +++ b/programs/drift/src/math/amm_spread.rs @@ -12,8 +12,8 @@ use crate::math::constants::{ BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, DEFAULT_LARGE_BID_ASK_FACTOR, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, FUNDING_RATE_BUFFER, MAX_BID_ASK_INVENTORY_SKEW_FACTOR, PEG_PRECISION, PERCENTAGE_PRECISION, - PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, - PRICE_PRECISION_I128, PRICE_PRECISION_I64, + PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, + PRICE_PRECISION_I64, }; use crate::math::safe_math::SafeMath; use crate::state::perp_market::{ContractType, PerpMarket, AMM}; diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 520af20258..45e7cf11ce 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1,3 +1,4 @@ +use crate::msg; use crate::state::pyth_lazer_oracle::PythLazerOracle; use crate::state::user::MarketType; use anchor_lang::prelude::*; From 99245f795d46d73f7439cc032ca16891923b6885 Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 22 Oct 2025 08:56:43 -0700 Subject: [PATCH 172/247] sdk: update jlp oracle to pyth lazer (#1980) --- sdk/src/constants/spotMarkets.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 707387d0f5..a8b5e14e0f 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -459,14 +459,15 @@ export const MainnetSpotMarkets: SpotMarketConfig[] = [ symbol: 'JLP', marketIndex: 19, poolId: 0, - oracle: new PublicKey('5Mb11e5rt1Sp6A286B145E4TmgMzsM2UX9nCF2vas5bs'), - oracleSource: OracleSource.PYTH_PULL, + oracle: new PublicKey('4VMtKepA6iFwMTJ7bBbdcGxavNRKiDjxxRr1CaB2NnFJ'), + oracleSource: OracleSource.PYTH_LAZER, mint: new PublicKey('27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4'), precision: new BN(10).pow(SIX), precisionExp: SIX, launchTs: 1719415157000, pythFeedId: '0xc811abc82b4bad1f9bd711a2773ccaa935b03ecef974236942cec5e0eb845a3a', + pythLazerId: 459, }, { symbol: 'POPCAT', From 37f07816d7e4a3a75f2b02793b6c55b049ee1d96 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:03:41 +0000 Subject: [PATCH 173/247] sdk: release v2.143.0-beta.7 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index a95b88f007..31694b8593 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.6 \ No newline at end of file +2.143.0-beta.7 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 0bb7ccd494..ee843c7a2e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.6", + "version": "2.143.0-beta.7", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 9260e6852b5bd571156dd0604fe3c865fbfd4ef0 Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:02:11 -0400 Subject: [PATCH 174/247] program: initial-margin-hlm-imf change (#1969) * program: initial-margin-hlm-imf change * working loop multiple base amounts test * isolated hlm func * reorg func so its easier to read * rename cap to bound * add asserts to swift test * add more max perp size tests * apply comments/feedback * add sdk * fix test and cargo fmt * CHANGELOG --------- Co-authored-by: Chris Heaney --- CHANGELOG.md | 2 + programs/drift/src/math/margin.rs | 51 ++++- programs/drift/src/math/orders/tests.rs | 194 +++++++++++++++++- programs/drift/src/state/perp_market.rs | 71 +++++-- programs/drift/src/state/perp_market/tests.rs | 120 ++++++++++- programs/drift/src/state/spot_market.rs | 1 + sdk/src/math/margin.ts | 49 ++++- sdk/src/math/market.ts | 92 ++++++--- 8 files changed, 514 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9cc3fb8aa..9a2b470d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: make imf smoother between hlm and non hlm users ([#1969](https://github.com/drift-labs/protocol-v2/pull/1969)) + ### Fixes ### Breaking diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 9d227bfe8b..2285c23bc1 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -1,8 +1,8 @@ use crate::error::DriftResult; use crate::error::ErrorCode; use crate::math::constants::{ - MARGIN_PRECISION_U128, MAX_POSITIVE_UPNL_FOR_INITIAL_MARGIN, PRICE_PRECISION, - SPOT_IMF_PRECISION_U128, SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_U128, + MARGIN_PRECISION_U128, MAX_POSITIVE_UPNL_FOR_INITIAL_MARGIN, PERCENTAGE_PRECISION, + PRICE_PRECISION, SPOT_IMF_PRECISION_U128, SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_U128, }; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; @@ -14,9 +14,9 @@ use crate::math::casting::Cast; use crate::math::funding::calculate_funding_payment; use crate::math::oracle::{is_oracle_valid_for_action, DriftAction}; -use crate::math::spot_balance::{get_strict_token_value, get_token_value}; - +use crate::math::helpers::get_proportion_u128; use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::{get_strict_token_value, get_token_value}; use crate::msg; use crate::state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}; use crate::state::oracle::{OraclePriceData, StrictOraclePrice}; @@ -45,6 +45,7 @@ pub fn calculate_size_premium_liability_weight( imf_factor: u32, liability_weight: u32, precision: u128, + is_bounded: bool, ) -> DriftResult { if imf_factor == 0 { return Ok(liability_weight); @@ -66,8 +67,46 @@ pub fn calculate_size_premium_liability_weight( )? .cast::()?; - let max_liability_weight = max(liability_weight, size_premium_liability_weight); - Ok(max_liability_weight) + if is_bounded { + let max_liability_weight = max(liability_weight, size_premium_liability_weight); + return Ok(max_liability_weight); + } + + Ok(size_premium_liability_weight) +} + +pub fn calc_high_leverage_mode_initial_margin_ratio_from_size( + pre_size_adj_margin_ratio: u32, + size_adj_margin_ratio: u32, + default_margin_ratio: u32, +) -> DriftResult { + let result = if size_adj_margin_ratio < pre_size_adj_margin_ratio { + let size_pct_discount_factor = PERCENTAGE_PRECISION.saturating_sub( + (pre_size_adj_margin_ratio + .cast::()? + .safe_sub(size_adj_margin_ratio.cast::()?)? + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div((pre_size_adj_margin_ratio.safe_div(5)?).cast::()?)?), + ); + + let hlm_margin_delta = pre_size_adj_margin_ratio + .saturating_sub(default_margin_ratio) + .max(1); + + let hlm_margin_delta_proportion = get_proportion_u128( + hlm_margin_delta.cast()?, + size_pct_discount_factor, + PERCENTAGE_PRECISION, + )? + .cast::()?; + hlm_margin_delta_proportion.safe_add(default_margin_ratio)? + } else if size_adj_margin_ratio == pre_size_adj_margin_ratio { + default_margin_ratio + } else { + size_adj_margin_ratio + }; + + Ok(result) } pub fn calculate_size_discount_asset_weight( diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 52b0e012a8..9d01e4dbb1 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -1956,7 +1956,7 @@ mod calculate_max_perp_order_size { use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{Order, PerpPosition, SpotPosition, User}; + use crate::state::user::{MarginMode, Order, PerpPosition, SpotPosition, User, UserStatus}; use crate::test_utils::get_pyth_price; use crate::test_utils::*; use crate::{ @@ -3160,6 +3160,191 @@ mod calculate_max_perp_order_size { assert!(total_collateral.unsigned_abs() - margin_requirement < 100 * QUOTE_PRECISION); } + #[test] + pub fn sol_perp_hlm_with_imf() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + high_leverage_margin_ratio_initial: 100, + high_leverage_margin_ratio_maintenance: 66, + imf_factor: 50, + status: MarketStatus::Active, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + ..PerpPosition::default() + }), + spot_positions, + margin_mode: MarginMode::HighLeverage, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + assert_eq!(max_order_size, 4098356557000); // 4098 + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + ..PerpPosition::default() + }), + spot_positions, + margin_mode: MarginMode::HighLeverage, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + assert_eq!(max_order_size, 84737288000); // 84 + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + ..PerpPosition::default() + }), + spot_positions, + margin_mode: MarginMode::HighLeverage, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + assert_eq!(max_order_size, 9605769000); // 9.6 + + user.perp_positions[0].open_orders = 1; + user.perp_positions[0].open_asks = -(max_order_size as i64); + + let MarginCalculation { + margin_requirement, + total_collateral, + .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial).strict(true), + ) + .unwrap(); + + assert!(total_collateral.unsigned_abs() - margin_requirement < QUOTE_PRECISION); + } + #[test] pub fn swift_failure() { let clock_slot = 0_u64; @@ -3385,6 +3570,8 @@ mod calculate_max_perp_order_size { ) .unwrap(); + assert_eq!(max_order_size, 1600000); + user.perp_positions[0].open_orders += 1; user.perp_positions[0].open_bids += max_order_size as i64; @@ -3401,7 +3588,10 @@ mod calculate_max_perp_order_size { ) .unwrap(); - assert!(total_collateral.unsigned_abs() - margin_requirement < QUOTE_PRECISION); + assert_eq!(total_collateral.unsigned_abs(), 2199358529); // ~$2200 + assert_eq!(margin_requirement, 2186678676); + + assert!(total_collateral.unsigned_abs() - margin_requirement < 13 * QUOTE_PRECISION); } } diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 45e7cf11ce..ba0cd0ee10 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -23,9 +23,10 @@ use crate::math::constants::{ PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, }; +use crate::math::helpers::get_proportion_u128; use crate::math::margin::{ - calculate_size_discount_asset_weight, calculate_size_premium_liability_weight, - MarginRequirementType, + calc_high_leverage_mode_initial_margin_ratio_from_size, calculate_size_discount_asset_weight, + calculate_size_premium_liability_weight, MarginRequirementType, }; use crate::math::safe_math::SafeMath; use crate::math::stats; @@ -439,18 +440,19 @@ impl PerpMarket { user_high_leverage_mode: bool, ) -> DriftResult { if self.status == MarketStatus::Settlement { - return Ok(0); // no liability weight on size + return Ok(0); } - let (margin_ratio_initial, margin_ratio_maintenance) = - if user_high_leverage_mode && self.is_high_leverage_mode_enabled() { - ( - self.high_leverage_margin_ratio_initial.cast::()?, - self.high_leverage_margin_ratio_maintenance.cast::()?, - ) - } else { - (self.margin_ratio_initial, self.margin_ratio_maintenance) - }; + let is_high_leverage_user = user_high_leverage_mode && self.is_high_leverage_mode_enabled(); + + let (margin_ratio_initial, margin_ratio_maintenance) = if is_high_leverage_user { + ( + self.high_leverage_margin_ratio_initial.cast::()?, + self.high_leverage_margin_ratio_maintenance.cast::()?, + ) + } else { + (self.margin_ratio_initial, self.margin_ratio_maintenance) + }; let default_margin_ratio = match margin_type { MarginRequirementType::Initial => margin_ratio_initial, @@ -460,14 +462,43 @@ impl PerpMarket { MarginRequirementType::Maintenance => margin_ratio_maintenance, }; - let size_adj_margin_ratio = calculate_size_premium_liability_weight( - size, - self.imf_factor, - default_margin_ratio, - MARGIN_PRECISION_U128, - )?; - - let margin_ratio = default_margin_ratio.max(size_adj_margin_ratio); + let margin_ratio = + if is_high_leverage_user && margin_type != MarginRequirementType::Maintenance { + // use HLM maintenance margin but ordinary mode initial/fill margin for size adj calculation + let pre_size_adj_margin_ratio = match margin_type { + MarginRequirementType::Initial => self.margin_ratio_initial, + MarginRequirementType::Fill => { + self.margin_ratio_initial + .safe_add(self.margin_ratio_maintenance)? + / 2 + } + MarginRequirementType::Maintenance => margin_ratio_maintenance, + }; + + let size_adj_margin_ratio = calculate_size_premium_liability_weight( + size, + self.imf_factor, + pre_size_adj_margin_ratio, + MARGIN_PRECISION_U128, + false, + )?; + + calc_high_leverage_mode_initial_margin_ratio_from_size( + pre_size_adj_margin_ratio, + size_adj_margin_ratio, + default_margin_ratio, + )? + } else { + let size_adj_margin_ratio = calculate_size_premium_liability_weight( + size, + self.imf_factor, + default_margin_ratio, + MARGIN_PRECISION_U128, + true, + )?; + + default_margin_ratio.max(size_adj_margin_ratio) + }; Ok(margin_ratio) } diff --git a/programs/drift/src/state/perp_market/tests.rs b/programs/drift/src/state/perp_market/tests.rs index 46de2248b1..f98e7ef01e 100644 --- a/programs/drift/src/state/perp_market/tests.rs +++ b/programs/drift/src/state/perp_market/tests.rs @@ -84,21 +84,137 @@ mod get_margin_ratio { let margin_ratio_initial = perp_market .get_margin_ratio(BASE_PRECISION, MarginRequirementType::Initial, true) .unwrap(); + assert_eq!(margin_ratio_initial, MARGIN_PRECISION / 50); let margin_ratio_maintenance = perp_market .get_margin_ratio(BASE_PRECISION, MarginRequirementType::Maintenance, true) .unwrap(); + assert_eq!(margin_ratio_maintenance, MARGIN_PRECISION / 100); + let margin_ratio_fill = perp_market .get_margin_ratio(BASE_PRECISION, MarginRequirementType::Fill, true) .unwrap(); - assert_eq!(margin_ratio_initial, MARGIN_PRECISION / 50); assert_eq!( margin_ratio_fill, (MARGIN_PRECISION / 50 + MARGIN_PRECISION / 100) / 2 ); - assert_eq!(margin_ratio_maintenance, MARGIN_PRECISION / 100); + } + + #[test] + fn new_hlm_imf_size_loop() { + let perp_market = PerpMarket { + margin_ratio_initial: MARGIN_PRECISION / 20, + margin_ratio_maintenance: MARGIN_PRECISION / 33, + high_leverage_margin_ratio_initial: (MARGIN_PRECISION / 100) as u16, + high_leverage_margin_ratio_maintenance: (MARGIN_PRECISION / 151) as u16, + imf_factor: 50, + ..PerpMarket::default() + }; + + let mut cnt = 0; + + for i in 1..1_000 { + let hlm_margin_ratio_initial = perp_market + .get_margin_ratio( + BASE_PRECISION * i * 1000, + MarginRequirementType::Initial, + true, + ) + .unwrap(); + + let margin_ratio_initial = perp_market + .get_margin_ratio( + BASE_PRECISION * i * 1000, + MarginRequirementType::Initial, + false, + ) + .unwrap(); + + if margin_ratio_initial != perp_market.margin_ratio_initial { + // crate::msg!("{}", BASE_PRECISION * i); + assert_eq!(hlm_margin_ratio_initial, margin_ratio_initial); + cnt += 1; + } + } + + assert_eq!(cnt, 959_196 / 1_000); + } + + #[test] + fn new_hlm_imf_size() { + let perp_market = PerpMarket { + margin_ratio_initial: MARGIN_PRECISION / 10, + margin_ratio_maintenance: MARGIN_PRECISION / 20, + high_leverage_margin_ratio_initial: (MARGIN_PRECISION / 100) as u16, + high_leverage_margin_ratio_maintenance: (MARGIN_PRECISION / 200) as u16, + imf_factor: 50, + ..PerpMarket::default() + }; + + let normal_margin_ratio_initial = perp_market + .get_margin_ratio( + BASE_PRECISION * 1000000, + MarginRequirementType::Initial, + false, + ) + .unwrap(); + + assert_eq!(normal_margin_ratio_initial, 1300); + + let hlm_margin_ratio_initial = perp_market + .get_margin_ratio(BASE_PRECISION / 10, MarginRequirementType::Initial, true) + .unwrap(); + + assert_eq!( + hlm_margin_ratio_initial, + perp_market.high_leverage_margin_ratio_initial as u32 + ); + + let hlm_margin_ratio_initial = perp_market + .get_margin_ratio(BASE_PRECISION, MarginRequirementType::Initial, true) + .unwrap(); + + assert_eq!( + hlm_margin_ratio_initial, + perp_market.high_leverage_margin_ratio_initial as u32 + ); + + let hlm_margin_ratio_initial = perp_market + .get_margin_ratio(BASE_PRECISION * 10, MarginRequirementType::Initial, true) + .unwrap(); + + assert_eq!( + hlm_margin_ratio_initial, + 104 // slightly under 100x at 10 base + ); + + let hlm_margin_ratio_initial_sized = perp_market + .get_margin_ratio(BASE_PRECISION * 3000, MarginRequirementType::Initial, true) + .unwrap(); + assert_eq!(hlm_margin_ratio_initial_sized, 221); + assert!( + hlm_margin_ratio_initial_sized > perp_market.high_leverage_margin_ratio_initial as u32 + ); + + let hlm_margin_ratio_maint = perp_market + .get_margin_ratio( + BASE_PRECISION * 3000, + MarginRequirementType::Maintenance, + true, + ) + .unwrap(); + assert_eq!(hlm_margin_ratio_maint, 67); // hardly changed + + let hlm_margin_ratio_maint = perp_market + .get_margin_ratio( + BASE_PRECISION * 300000, + MarginRequirementType::Maintenance, + true, + ) + .unwrap(); + assert_eq!(hlm_margin_ratio_maint, 313); // changed more at large size } } diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 84a0cdd89d..8891e1a99b 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -426,6 +426,7 @@ impl SpotMarket { self.imf_factor, default_liability_weight, SPOT_WEIGHT_PRECISION_U128, + true, )?; let liability_weight = size_based_liability_weight.max(default_liability_weight); diff --git a/sdk/src/math/margin.ts b/sdk/src/math/margin.ts index 63fada436b..2dde85d11b 100644 --- a/sdk/src/math/margin.ts +++ b/sdk/src/math/margin.ts @@ -10,6 +10,7 @@ import { MARGIN_PRECISION, PRICE_PRECISION, QUOTE_PRECISION, + PERCENTAGE_PRECISION, } from '../constants/numericConstants'; import { BN } from '@coral-xyz/anchor'; import { OraclePriceData } from '../oracles/types'; @@ -32,7 +33,8 @@ export function calculateSizePremiumLiabilityWeight( size: BN, // AMM_RESERVE_PRECISION imfFactor: BN, liabilityWeight: BN, - precision: BN + precision: BN, + isBounded = true ): BN { if (imfFactor.eq(ZERO)) { return liabilityWeight; @@ -53,10 +55,13 @@ export function calculateSizePremiumLiabilityWeight( .div(denom) // 1e5 ); - const maxLiabilityWeight = BN.max( - liabilityWeight, - sizePremiumLiabilityWeight - ); + let maxLiabilityWeight; + if (isBounded) { + maxLiabilityWeight = BN.max(liabilityWeight, sizePremiumLiabilityWeight); + } else { + maxLiabilityWeight = sizePremiumLiabilityWeight; + } + return maxLiabilityWeight; } @@ -370,3 +375,37 @@ export function calculateUserMaxPerpOrderSize( return user.getMaxTradeSizeUSDCForPerp(targetMarketIndex, tradeSide); } + +export function calcHighLeverageModeInitialMarginRatioFromSize( + preSizeAdjMarginRatio: BN, + sizeAdjMarginRatio: BN, + defaultMarginRatio: BN +): BN { + let result: BN; + + if (sizeAdjMarginRatio.lt(preSizeAdjMarginRatio)) { + const sizePctDiscountFactor = PERCENTAGE_PRECISION.sub( + preSizeAdjMarginRatio + .sub(sizeAdjMarginRatio) + .mul(PERCENTAGE_PRECISION) + .div(preSizeAdjMarginRatio.div(new BN(5))) + ); + + const hlmMarginDelta = BN.max( + preSizeAdjMarginRatio.sub(defaultMarginRatio), + new BN(1) + ); + + const hlmMarginDeltaProportion = hlmMarginDelta + .mul(sizePctDiscountFactor) + .div(PERCENTAGE_PRECISION); + + result = hlmMarginDeltaProportion.add(defaultMarginRatio); + } else if (sizeAdjMarginRatio.eq(preSizeAdjMarginRatio)) { + result = defaultMarginRatio; + } else { + result = sizeAdjMarginRatio; + } + + return result; +} diff --git a/sdk/src/math/market.ts b/sdk/src/math/market.ts index 6dd3697de6..cae257898a 100644 --- a/sdk/src/math/market.ts +++ b/sdk/src/math/market.ts @@ -19,6 +19,7 @@ import { import { calculateSizeDiscountAssetWeight, calculateSizePremiumLiabilityWeight, + calcHighLeverageModeInitialMarginRatioFromSize, } from './margin'; import { MMOraclePriceData, OraclePriceData } from '../oracles/types'; import { @@ -143,45 +144,74 @@ export function calculateMarketMarginRatio( customMarginRatio = 0, userHighLeverageMode = false ): number { - let marginRationInitial; - let marginRatioMaintenance; + if (market.status === 'Settlement') return 0; - if ( + const isHighLeverageUser = userHighLeverageMode && market.highLeverageMarginRatioInitial > 0 && - market.highLeverageMarginRatioMaintenance - ) { - marginRationInitial = market.highLeverageMarginRatioInitial; - marginRatioMaintenance = market.highLeverageMarginRatioMaintenance; - } else { - marginRationInitial = market.marginRatioInitial; - marginRatioMaintenance = market.marginRatioMaintenance; - } + market.highLeverageMarginRatioMaintenance > 0; + + const marginRatioInitial = isHighLeverageUser + ? market.highLeverageMarginRatioInitial + : market.marginRatioInitial; + + const marginRatioMaintenance = isHighLeverageUser + ? market.highLeverageMarginRatioMaintenance + : market.marginRatioMaintenance; - let marginRatio; + let defaultMarginRatio: number; switch (marginCategory) { - case 'Initial': { - // use lowest leverage between max allowed and optional user custom max - marginRatio = Math.max( - calculateSizePremiumLiabilityWeight( - size, - new BN(market.imfFactor), - new BN(marginRationInitial), - MARGIN_PRECISION - ).toNumber(), - customMarginRatio - ); + case 'Initial': + defaultMarginRatio = marginRatioInitial; break; - } - case 'Maintenance': { - marginRatio = calculateSizePremiumLiabilityWeight( - size, - new BN(market.imfFactor), - new BN(marginRatioMaintenance), - MARGIN_PRECISION - ).toNumber(); + case 'Maintenance': + defaultMarginRatio = marginRatioMaintenance; break; + default: + throw new Error('Invalid margin category'); + } + + let marginRatio: number; + + if (isHighLeverageUser && marginCategory !== 'Maintenance') { + // Use ordinary-mode initial/fill ratios for size-adjusted calc + let preSizeAdjMarginRatio: number; + switch (marginCategory) { + case 'Initial': + preSizeAdjMarginRatio = market.marginRatioInitial; + break; + default: + preSizeAdjMarginRatio = marginRatioMaintenance; + break; } + + const sizeAdjMarginRatio = calculateSizePremiumLiabilityWeight( + size, + new BN(market.imfFactor), + new BN(preSizeAdjMarginRatio), + MARGIN_PRECISION, + false + ).toNumber(); + + marginRatio = calcHighLeverageModeInitialMarginRatioFromSize( + new BN(preSizeAdjMarginRatio), + new BN(sizeAdjMarginRatio), + new BN(defaultMarginRatio) + ).toNumber(); + } else { + const sizeAdjMarginRatio = calculateSizePremiumLiabilityWeight( + size, + new BN(market.imfFactor), + new BN(defaultMarginRatio), + MARGIN_PRECISION, + true + ).toNumber(); + + marginRatio = Math.max(defaultMarginRatio, sizeAdjMarginRatio); + } + + if (marginCategory === 'Initial') { + marginRatio = Math.max(marginRatio, customMarginRatio); } return marginRatio; From 6af0b76525ca065d0e7f6bf6c1ab28eae3c30de6 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:07:27 +0000 Subject: [PATCH 175/247] sdk: release v2.143.0-beta.8 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 31694b8593..53b97a37b7 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.7 \ No newline at end of file +2.143.0-beta.8 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index ee843c7a2e..daeb56ad3d 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.7", + "version": "2.143.0-beta.8", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From d04bb0cc17c016b8001fecd668093c5bca6bec89 Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:27:02 -0700 Subject: [PATCH 176/247] amm available for low risk orders (#1968) * amm available for low risk orders * remove unnecessary import * all builds * repurpose taker_speed_bump_override * fix one test * better CU opt for amm_can_fill_order * add tests for amm_can_fill_order and is_oracle_low_risk * fix test * sdk changes starting * sdk still has errors * fix sdk * update idl * tests pass + sdk changes + rename taker_speed_bump_override * apply max in getOracleValidity * add changelog and address comments * make linter happy * calculate_repeg_validity change * cump switchboard tx cus ix * update idl --------- Co-authored-by: Chris Heaney --- CHANGELOG.md | 3 + programs/drift/src/controller/orders.rs | 91 +- .../src/controller/orders/amm_jit_tests.rs | 259 +- .../src/controller/orders/amm_lp_jit_tests.rs | 2516 ----------------- .../drift/src/controller/orders/fuel_tests.rs | 66 +- programs/drift/src/controller/orders/tests.rs | 297 +- programs/drift/src/controller/pnl.rs | 1 + .../drift/src/controller/position/tests.rs | 5 +- programs/drift/src/controller/repeg.rs | 6 +- programs/drift/src/controller/repeg/tests.rs | 4 +- programs/drift/src/controller/spot_balance.rs | 1 + programs/drift/src/instructions/admin.rs | 16 +- programs/drift/src/instructions/user.rs | 1 + programs/drift/src/lib.rs | 9 +- programs/drift/src/math/auction.rs | 41 +- programs/drift/src/math/fulfillment.rs | 46 +- programs/drift/src/math/fulfillment/tests.rs | 78 +- programs/drift/src/math/margin.rs | 6 + programs/drift/src/math/oracle.rs | 76 +- programs/drift/src/math/repeg.rs | 3 +- programs/drift/src/math/repeg/tests.rs | 2 +- programs/drift/src/state/oracle_map.rs | 3 + programs/drift/src/state/perp_market.rs | 114 +- programs/drift/src/state/perp_market/tests.rs | 295 +- programs/drift/src/state/user.rs | 20 + sdk/src/adminClient.ts | 20 +- sdk/src/dlob/DLOB.ts | 43 +- sdk/src/idl/drift.json | 40 +- sdk/src/math/auction.ts | 57 +- sdk/src/math/oracles.ts | 88 + sdk/src/math/orders.ts | 32 +- sdk/src/types.ts | 14 +- sdk/tests/dlob/helpers.ts | 8 +- tests/switchboardTxCus.ts | 2 +- 34 files changed, 1275 insertions(+), 2988 deletions(-) delete mode 100644 programs/drift/src/controller/orders/amm_lp_jit_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2b470d82..76c85a0197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- program: relax filling conditions for low risk orders vs amm ([#1968](https://github.com/drift-labs/protocol-v2/pull/1968)) +- sdk: make oracle validity match program and propogate to dlob and math functions ([#1968](https://github.com/drift-labs/protocol-v2/pull/1968)) + ### Features - program: make imf smoother between hlm and non hlm users ([#1969](https://github.com/drift-labs/protocol-v2/pull/1969)) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 6d4533dd33..67d4643ba6 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -62,7 +62,7 @@ use crate::state::order_params::{ ModifyOrderParams, OrderParams, PlaceOrderOptions, PostOnlyParam, }; use crate::state::paused_operations::{PerpOperation, SpotOperation}; -use crate::state::perp_market::{AMMAvailability, MarketStatus, PerpMarket}; +use crate::state::perp_market::{MarketStatus, PerpMarket}; use crate::state::perp_market_map::PerpMarketMap; use crate::state::protected_maker_mode_config::ProtectedMakerParams; use crate::state::spot_fulfillment_params::{ExternalSpotFill, SpotFulfillmentParams}; @@ -88,10 +88,6 @@ mod tests; #[cfg(test)] mod amm_jit_tests; - -#[cfg(test)] -mod amm_lp_jit_tests; - #[cfg(test)] mod fuel_tests; @@ -998,13 +994,8 @@ pub fn fill_perp_order( .position(|order| order.order_id == order_id && order.status == OrderStatus::Open) .ok_or_else(print_error!(ErrorCode::OrderDoesNotExist))?; - let (order_status, market_index, order_market_type, order_direction) = get_struct_values!( - user.orders[order_index], - status, - market_index, - market_type, - direction - ); + let (order_status, market_index, order_market_type) = + get_struct_values!(user.orders[order_index], status, market_index, market_type); validate!( order_market_type == MarketType::Perp, @@ -1070,14 +1061,9 @@ pub fn fill_perp_order( let safe_oracle_validity: OracleValidity; let oracle_price: i64; let oracle_twap_5min: i64; - let perp_market_index: u16; let user_can_skip_duration: bool; - let amm_can_skip_duration: bool; - let amm_has_low_enough_inventory: bool; - let oracle_valid_for_amm_fill: bool; let oracle_stale_for_margin: bool; - let min_auction_duration: u8; - let mut amm_is_available = !state.amm_paused()?; + let mut amm_is_available: bool = !state.amm_paused()?; { let market = &mut perp_market_map.get_ref_mut(&market_index)?; validation::perp_market::validate_perp_market(market)?; @@ -1104,10 +1090,19 @@ pub fn fill_perp_order( &market.amm.oracle_source, oracle::LogMode::SafeMMOracle, market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )?; - oracle_valid_for_amm_fill = - is_oracle_valid_for_action(safe_oracle_validity, Some(DriftAction::FillOrderAmm))?; + user_can_skip_duration = user.can_skip_auction_duration(user_stats)?; + amm_is_available &= market.amm_can_fill_order( + &user.orders[order_index], + slot, + fill_mode, + state, + safe_oracle_validity, + user_can_skip_duration, + &mm_oracle_price_data, + )?; oracle_stale_for_margin = mm_oracle_price_data.get_delay() > state @@ -1115,40 +1110,12 @@ pub fn fill_perp_order( .validity .slots_before_stale_for_margin; - amm_is_available &= oracle_valid_for_amm_fill; - amm_is_available &= !market.is_operation_paused(PerpOperation::AmmFill); - amm_is_available &= !market.has_too_much_drawdown()?; - - // We are already using safe oracle data from MM oracle. - // But AMM isnt available if we could have used MM oracle but fell back due to price diff - let amm_available_mm_oracle_recent_but_volatile = - if mm_oracle_price_data.is_enabled() && mm_oracle_price_data.is_mm_oracle_as_recent() { - let amm_available = !mm_oracle_price_data.is_mm_exchange_diff_bps_high(); - amm_available - } else { - true - }; - amm_is_available &= amm_available_mm_oracle_recent_but_volatile; - - let amm_wants_to_jit_make = market.amm.amm_wants_to_jit_make(order_direction)?; - amm_has_low_enough_inventory = market - .amm - .amm_has_low_enough_inventory(amm_wants_to_jit_make)?; - amm_can_skip_duration = - market.can_skip_auction_duration(&state, amm_has_low_enough_inventory)?; - - user_can_skip_duration = user.can_skip_auction_duration(user_stats)?; - reserve_price_before = market.amm.reserve_price()?; oracle_price = mm_oracle_price_data.get_price(); oracle_twap_5min = market .amm .historical_oracle_data .last_oracle_price_twap_5min; - perp_market_index = market.market_index; - - min_auction_duration = - market.get_min_perp_auction_duration(state.min_perp_auction_duration); } // allow oracle price to be used to calculate limit price if it's valid or stale for amm @@ -1156,7 +1123,7 @@ pub fn fill_perp_order( if is_oracle_valid_for_action(safe_oracle_validity, Some(DriftAction::OracleOrderPrice))? { Some(oracle_price) } else { - msg!("Perp market = {} oracle deemed invalid", perp_market_index); + msg!("Perp market = {} oracle deemed invalid", market_index); None }; @@ -1285,16 +1252,6 @@ pub fn fill_perp_order( return Ok((0, 0)); } - let amm_availability = if amm_is_available { - if amm_can_skip_duration && user_can_skip_duration { - AMMAvailability::Immediate - } else { - AMMAvailability::AfterMinDuration - } - } else { - AMMAvailability::Unavailable - }; - let (base_asset_amount, quote_asset_amount) = fulfill_perp_order( user, order_index, @@ -1315,8 +1272,7 @@ pub fn fill_perp_order( valid_oracle_price, now, slot, - min_auction_duration, - amm_availability, + amm_is_available, fill_mode, oracle_stale_for_margin, rev_share_escrow, @@ -1782,8 +1738,7 @@ fn fulfill_perp_order( valid_oracle_price: Option, now: i64, slot: u64, - min_auction_duration: u8, - amm_availability: AMMAvailability, + amm_is_available: bool, fill_mode: FillMode, oracle_stale_for_margin: bool, rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, @@ -1807,19 +1762,13 @@ fn fulfill_perp_order( let fulfillment_methods = { let market = perp_market_map.get_ref(&market_index)?; - let oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; - determine_perp_fulfillment_methods( &user.orders[user_order_index], maker_orders_info, &market.amm, reserve_price_before, - Some(oracle_price), limit_price, - amm_availability, - slot, - min_auction_duration, - fill_mode, + amm_is_available, )? }; @@ -3138,6 +3087,7 @@ pub fn trigger_order( .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let is_oracle_valid = @@ -5435,6 +5385,7 @@ pub fn trigger_spot_order( spot_market.historical_oracle_data.last_oracle_price_twap, spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let strict_oracle_price = StrictOraclePrice { current: oracle_price_data.price, diff --git a/programs/drift/src/controller/orders/amm_jit_tests.rs b/programs/drift/src/controller/orders/amm_jit_tests.rs index c14fd58b62..87c160b78d 100644 --- a/programs/drift/src/controller/orders/amm_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_jit_tests.rs @@ -2,8 +2,14 @@ use anchor_lang::prelude::Pubkey; use anchor_lang::Owner; use crate::math::constants::ONE_BPS_DENOMINATOR; +use crate::math::oracle; +use crate::math::oracle::oracle_validity; +use crate::state::fill_mode::FillMode; use crate::state::oracle_map::OracleMap; +use crate::state::perp_market::PerpMarket; +use crate::state::state::State; use crate::state::state::{FeeStructure, FeeTier}; +use crate::state::user::MarketType; use crate::state::user::{Order, PerpPosition}; fn get_fee_structure() -> FeeStructure { @@ -25,6 +31,53 @@ fn get_user_keys() -> (Pubkey, Pubkey, Pubkey) { (Pubkey::default(), Pubkey::default(), Pubkey::default()) } +fn get_state(min_auction_duration: u8) -> State { + State { + min_perp_auction_duration: min_auction_duration, + ..State::default() + } +} + +pub fn get_amm_is_available( + order: &Order, + min_auction_duration: u8, + market: &PerpMarket, + oracle_map: &mut OracleMap, + slot: u64, + user_can_skip_auction_duration: bool, +) -> bool { + let state = get_state(min_auction_duration); + let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); + let mm_oracle_price_data = market + .get_mm_oracle_price_data(*oracle_price_data, slot, &state.oracle_guard_rails.validity) + .unwrap(); + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let safe_oracle_validity = oracle_validity( + MarketType::Perp, + market.market_index, + market.amm.historical_oracle_data.last_oracle_price_twap, + &safe_oracle_price_data, + &state.oracle_guard_rails.validity, + market.get_max_confidence_interval_multiplier().unwrap(), + &market.amm.oracle_source, + oracle::LogMode::SafeMMOracle, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + ) + .unwrap(); + market + .amm_can_fill_order( + order, + slot, + FillMode::Fill, + &state, + safe_oracle_validity, + user_can_skip_auction_duration, + &mm_oracle_price_data, + ) + .unwrap() +} + #[cfg(test)] pub mod amm_jit { use std::str::FromStr; @@ -41,6 +94,7 @@ pub mod amm_jit { SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::math::constants::{CONCENTRATION_PRECISION, PRICE_PRECISION_U64}; + use crate::state::fill_mode::FillMode; use crate::state::oracle::{HistoricalOracleData, OracleSource}; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; @@ -276,9 +330,21 @@ pub mod amm_jit { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -296,8 +362,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -468,9 +533,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -488,8 +565,7 @@ pub mod amm_jit { Some(PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -507,7 +583,10 @@ pub mod amm_jit { // nets to zero let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.base_asset_amount_with_amm, 0); + assert_eq!( + market_after.amm.base_asset_amount_with_amm, + BASE_PRECISION_I128 / 2 + ); // make sure lps didnt get anything assert_eq!(market_after.amm.base_asset_amount_per_lp, 0); @@ -668,9 +747,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -688,8 +779,7 @@ pub mod amm_jit { Some(PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -867,9 +957,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -887,8 +989,7 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -1066,11 +1167,24 @@ pub mod amm_jit { }; create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1088,8 +1202,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -1278,9 +1391,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1298,8 +1423,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -1506,8 +1630,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::Unavailable, + false, FillMode::Fill, false, &mut None, @@ -1688,9 +1811,21 @@ pub mod amm_jit { let reserve_price_before = market.amm.reserve_price().unwrap(); assert_eq!(reserve_price_before, 100 * PRICE_PRECISION_U64); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1708,8 +1843,7 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -1877,10 +2011,22 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + // fulfill with match let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1898,8 +2044,7 @@ pub mod amm_jit { Some(1), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -2079,10 +2224,22 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + // fulfill with match let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2100,8 +2257,7 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -2332,10 +2488,11 @@ pub mod amm_jit { create_anchor_account_info!(maker, &maker_key, User, maker_account_info); let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); + let order_index = 0; // fulfill with match fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2353,8 +2510,7 @@ pub mod amm_jit { Some(1), now, slot, - auction_duration, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + false, FillMode::Fill, false, &mut None, @@ -2619,9 +2775,23 @@ pub mod amm_jit { let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); // fulfill with match + + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = + taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2639,8 +2809,7 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -2850,9 +3019,22 @@ pub mod amm_jit { assert_eq!(taker.perp_positions[0].open_orders, 1); // fulfill with match + + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2870,8 +3052,7 @@ pub mod amm_jit { Some(1), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, diff --git a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs deleted file mode 100644 index ae6666328e..0000000000 --- a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs +++ /dev/null @@ -1,2516 +0,0 @@ -use anchor_lang::prelude::Pubkey; -use anchor_lang::Owner; - -use crate::math::constants::ONE_BPS_DENOMINATOR; -use crate::state::oracle_map::OracleMap; -use crate::state::state::{FeeStructure, FeeTier}; -use crate::state::user::{Order, PerpPosition}; - -fn get_fee_structure() -> FeeStructure { - let mut fee_tiers = [FeeTier::default(); 10]; - fee_tiers[0] = FeeTier { - fee_numerator: 5, - fee_denominator: ONE_BPS_DENOMINATOR, - maker_rebate_numerator: 3, - maker_rebate_denominator: ONE_BPS_DENOMINATOR, - ..FeeTier::default() - }; - FeeStructure { - fee_tiers, - ..FeeStructure::test_default() - } -} - -fn get_user_keys() -> (Pubkey, Pubkey, Pubkey) { - (Pubkey::default(), Pubkey::default(), Pubkey::default()) -} - -#[cfg(test)] -pub mod amm_lp_jit { - use std::str::FromStr; - - use crate::controller::orders::fulfill_perp_order; - use crate::controller::position::PositionDirection; - use crate::create_account_info; - use crate::create_anchor_account_info; - use crate::math::constants::{ - PERCENTAGE_PRECISION_I128, PRICE_PRECISION_I64, QUOTE_PRECISION_I64, - }; - - use crate::math::amm_jit::calculate_amm_jit_liquidity; - use crate::math::amm_spread::calculate_inventory_liquidity_ratio; - use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, BASE_PRECISION_U64, - PEG_PRECISION, PRICE_PRECISION, SPOT_BALANCE_PRECISION_U64, - SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, - }; - use crate::math::constants::{CONCENTRATION_PRECISION, PRICE_PRECISION_U64}; - use crate::state::fill_mode::FillMode; - use crate::state::oracle::{HistoricalOracleData, OracleSource}; - use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; - use crate::state::perp_market_map::PerpMarketMap; - use crate::state::spot_market::{SpotBalanceType, SpotMarket}; - use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{OrderStatus, OrderType, SpotPosition, User, UserStats}; - use crate::state::user_map::{UserMap, UserStatsMap}; - use crate::test_utils::*; - use crate::test_utils::{get_orders, get_positions, get_pyth_price, get_spot_positions}; - use crate::validation::perp_market::validate_perp_market; - - use super::*; - - #[test] - fn max_jit_amounts() { - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -1000000000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 2000, - short_spread: 2000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 99_920_000, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 300000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 99 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - market.amm.long_spread = 11000; - market.amm.short_spread = 11000; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 99 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 45454000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 101 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 45454000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 102 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 104 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - market.amm.short_spread = 20000; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 104 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 105 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - market.amm.long_spread = 51000; - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 105 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 9803000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 95 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - } - - #[test] - fn zero_asks_with_amm_lp_jit_taker_long() { - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -1000000000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.base_asset_reserve = 0; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Short, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64, - BASE_PRECISION_U64, - false, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - } - - #[test] - fn no_fulfill_with_amm_lp_jit_taker_long() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(21, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (21 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (21 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (21 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Active, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - assert_eq!(new_bid_quote_asset_reserve, 99000000000); - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - price: 100 * PRICE_PRECISION_U64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - - let mut filler_stats = UserStats::default(); - - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - // amm jit doesnt take anything - assert_eq!( - market_after.amm.base_asset_amount_with_amm, - market.amm.base_asset_amount_with_amm - ); - assert_eq!( - market_after.amm.base_asset_amount_per_lp, - market.amm.base_asset_amount_per_lp - ); - assert_eq!( - market_after.amm.quote_asset_amount_per_lp, - market.amm.quote_asset_amount_per_lp - ); - assert_eq!(market_after.amm.total_fee_minus_distributions, 7500); - assert_eq!(market_after.amm.total_exchange_fee, 7500); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_short_max_amount() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let taker_mul: i64 = 20; - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 * taker_mul as u64, // if amm takes half it would flip - slot: 0, - price: 100 * PRICE_PRECISION_U64, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 * taker_mul, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64 * taker_mul as u64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 * taker_mul as u64, // maker wants full = amm wants BASE_PERCISION - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 * taker_mul, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64 * taker_mul as u64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(200 * PRICE_PRECISION_I64), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - // nets to zero - assert_eq!(market_after.amm.base_asset_amount_with_amm, 0); - - let maker = makers_and_referrers.get_ref_mut(&maker_key).unwrap(); - let maker_position = &maker.perp_positions[0]; - // maker got (full - net_baa) - assert_eq!( - maker_position.base_asset_amount as i128, - BASE_PRECISION_I128 * taker_mul as i128 - market.amm.base_asset_amount_with_amm - ); - } - - #[test] - fn no_fulfill_with_amm_lp_jit_taker_short() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // amm is short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - // bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - // bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - // ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - // ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - assert_eq!(new_bid_quote_asset_reserve, 99000000000); - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, // doesnt improve balance - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - assert_eq!(base_asset_amount, BASE_PRECISION_U64); - - let taker_position = &taker.perp_positions[0]; - assert_eq!(taker_position.base_asset_amount, -BASE_PRECISION_I64); - assert!(taker.orders[0].is_available()); - - let maker = makers_and_referrers.get_ref_mut(&maker_key).unwrap(); - let maker_position = &maker.perp_positions[0]; - assert_eq!(maker_position.base_asset_amount, BASE_PRECISION_I64 / 2); - assert_eq!(maker_position.quote_asset_amount, -49985000); - assert_eq!(maker_position.quote_entry_amount, -50 * QUOTE_PRECISION_I64); - assert_eq!(maker_position.quote_break_even_amount, -49985000); - assert_eq!(maker_position.open_orders, 0); - - let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.base_asset_amount_with_amm, -1000000000); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_short() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - base_spread: 250, - long_spread: 125, - short_spread: 125, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - // base is filled - assert!(base_asset_amount > 0); - - let market_after = market_map.get_ref(&0).unwrap(); - assert!( - market_after.amm.base_asset_amount_with_amm.abs() - < market.amm.base_asset_amount_with_amm.abs() - ); - - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - market.amm.total_mm_fee; - assert!(quote_asset_amount_surplus > 0); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_long() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 99 * PRICE_PRECISION_I64, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - let reserve_price_before = market.amm.reserve_price().unwrap(); - assert_eq!(reserve_price_before, 100 * PRICE_PRECISION_U64); - - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - // net baa improves - let market_after = market_map.get_ref(&0).unwrap(); - assert!( - market_after.amm.base_asset_amount_with_amm.abs() - < market.amm.base_asset_amount_with_amm.abs() - ); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_long_neg_qas() { - let now = 0_i64; - let slot = 10_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 50, // !! amm will bid before the ask spread price - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 10 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - // fulfill with match - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 10 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(1), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - assert_eq!(base_asset_amount, BASE_PRECISION_U64 * 3 / 4); // auctions not over so no amm fill - - let maker = makers_and_referrers.get_ref_mut(&maker_key).unwrap(); - let maker_position = &maker.perp_positions[0]; - assert_eq!(maker_position.base_asset_amount, -BASE_PRECISION_I64 / 2); - - let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.base_asset_amount_with_amm, -250000000); - - let taker_position = &taker.perp_positions[0]; - assert_eq!( - taker_position.base_asset_amount, - BASE_PRECISION_I64 + market_after.amm.base_asset_amount_with_amm as i64 - ); - - // market pays extra for trade - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - market.amm.total_mm_fee; - assert!(quote_asset_amount_surplus < 0); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_short_neg_qas() { - let now = 0_i64; - let slot = 10_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 100, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_end_price: 0, - auction_start_price: 200 * PRICE_PRECISION as i64, - auction_duration: 50, // !! amm will bid before the ask spread price - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 200 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - // fulfill with match - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 200 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(200 * PRICE_PRECISION_I64), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - assert_eq!(base_asset_amount, BASE_PRECISION_U64 * 3 / 4); // auctions not over so no amm fill - - let market_after = market_map.get_ref(&0).unwrap(); - - let taker_position = &taker.perp_positions[0]; - assert_eq!( - taker_position.base_asset_amount, - -3 * BASE_PRECISION_I64 / 4 - ); - - // mm gains from trade - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - market.amm.total_mm_fee; - assert!(quote_asset_amount_surplus < 0); - } - - #[allow(clippy::comparison_chain)] - #[test] - fn fulfill_with_amm_lp_jit_full_long() { - let now = 0_i64; - let mut slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let reserves = 5 * AMM_RESERVE_PRECISION; - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: reserves, - quote_asset_reserve: reserves, - base_asset_amount_with_amm: -(100 * AMM_RESERVE_PRECISION as i128), - base_asset_amount_short: -(100 * AMM_RESERVE_PRECISION as i128), - sqrt_k: reserves, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - base_spread: 5000, - max_spread: 1000000, - long_spread: 50000, - short_spread: 50000, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let auction_duration = 50; - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: 100 * BASE_PRECISION_U64, - slot: 0, - auction_duration, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: -100 * BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let auction_start_price = 95062500_i64; - let auction_end_price = 132154089_i64; - taker.orders[0].auction_start_price = auction_start_price; - taker.orders[0].auction_end_price = auction_end_price; - println!("start stop {} {}", auction_start_price, auction_end_price); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - let (mut neg, mut pos, mut none) = (false, false, false); - let mut prev_mm_fee = 0; - let mut prev_net_baa = market.amm.base_asset_amount_with_amm; - // track scaling - let mut prev_qas = 0; - let mut has_set_prev_qas = false; - loop { - println!("------"); - - // compute auction price - let is_complete = crate::math::auction::is_auction_complete( - taker.orders[0].slot, - auction_duration, - slot, - ) - .unwrap(); - if is_complete { - break; - } - - let auction_price = crate::math::auction::calculate_auction_price( - &taker.orders[0], - slot, - 1, - None, - false, - ) - .unwrap(); - let baa = market.amm.order_step_size * 4; - - let (mark, ask, bid) = { - let market = market_map.get_ref(&0).unwrap(); - let mark = market.amm.reserve_price().unwrap(); - let ask = market.amm.ask_price(mark).unwrap(); - let bid = market.amm.bid_price(mark).unwrap(); - (mark, ask, bid) - }; - println!("mark: {} bid ask: {} {}", mark, bid, ask); - - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: baa, - price: auction_price, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -(baa as i64), - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - // fulfill with match - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, auction_price)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(1), - now, - slot, - auction_duration, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - prev_mm_fee; - prev_mm_fee = market_after.amm.total_mm_fee; - - // imbalance decreases - assert!(market_after.amm.base_asset_amount_with_amm.abs() < prev_net_baa.abs()); - prev_net_baa = market_after.amm.base_asset_amount_with_amm; - - println!( - "slot {} auction: {} surplus: {}", - slot, auction_price, quote_asset_amount_surplus - ); - - if !has_set_prev_qas { - prev_qas = quote_asset_amount_surplus; - has_set_prev_qas = true; - } else { - // decreasing (amm paying less / earning more) - assert!(prev_qas < quote_asset_amount_surplus); - prev_qas = quote_asset_amount_surplus; - } - - if quote_asset_amount_surplus < 0 { - neg = true; - assert!(!pos); // neg first - } else if quote_asset_amount_surplus > 0 { - pos = true; - assert!(neg); // neg first - // sometimes skips over == 0 surplus - } else { - none = true; - assert!(neg); - assert!(!pos); - } - slot += 1; - } - // auction should go through both position and negative - assert!(neg); - assert!(pos); - // assert!(none); //todo: skips over this (-1 -> 1) - - println!("{} {} {}", neg, pos, none); - } - - #[allow(clippy::comparison_chain)] - #[test] - fn fulfill_with_amm_lp_jit_full_short() { - let now = 0_i64; - let mut slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let reserves = 5 * AMM_RESERVE_PRECISION; - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: reserves, - quote_asset_reserve: reserves, - base_asset_amount_with_amm: 100 * AMM_RESERVE_PRECISION as i128, - base_asset_amount_long: 100 * AMM_RESERVE_PRECISION as i128, - sqrt_k: reserves, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1, - order_tick_size: 1, - oracle: oracle_price_key, - base_spread: 5000, - max_spread: 1000000, - long_spread: 50000, - short_spread: 50000, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let auction_duration = 50; - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: 100 * BASE_PRECISION_U64, - slot: 0, - auction_duration, // !! amm will bid before the ask spread price - auction_end_price: 0, - auction_start_price: 200 * PRICE_PRECISION as i64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -100 * BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let auction_start_price = 105062500; - let auction_end_price = 79550209; - taker.orders[0].auction_start_price = auction_start_price; - taker.orders[0].auction_end_price = auction_end_price; - println!("start stop {} {}", auction_start_price, auction_end_price); - - let mut filler = User::default(); - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - let (mut neg, mut pos, mut none) = (false, false, false); - let mut prev_mm_fee = 0; - let mut prev_net_baa = market.amm.base_asset_amount_with_amm; - // track scaling - let mut prev_qas = 0; - let mut has_set_prev_qas = false; - - loop { - println!("------"); - - // compute auction price - let is_complete = crate::math::auction::is_auction_complete( - taker.orders[0].slot, - auction_duration, - slot, - ) - .unwrap(); - - if is_complete { - break; - } - - let auction_price = crate::math::auction::calculate_auction_price( - &taker.orders[0], - slot, - 1, - None, - false, - ) - .unwrap(); - let baa = 1000 * 4; - - let (mark, ask, bid) = { - let market = market_map.get_ref(&0).unwrap(); - let mark = market.amm.reserve_price().unwrap(); - let ask = market.amm.ask_price(mark).unwrap(); - let bid = market.amm.bid_price(mark).unwrap(); - (mark, ask, bid) - }; - println!("mark: {} bid ask: {} {}", mark, bid, ask); - - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: baa as u64, - price: auction_price, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: baa as i64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - // fulfill with match - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, auction_price)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(200 * PRICE_PRECISION_I64), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - prev_mm_fee; - prev_mm_fee = market_after.amm.total_mm_fee; - - // imbalance decreases or remains the same (damm wont always take on positions) - assert!(market_after.amm.base_asset_amount_with_amm.abs() <= prev_net_baa.abs()); - prev_net_baa = market_after.amm.base_asset_amount_with_amm; - - println!( - "slot {} auction: {} surplus: {}", - slot, auction_price, quote_asset_amount_surplus - ); - - if !has_set_prev_qas { - prev_qas = quote_asset_amount_surplus; - has_set_prev_qas = true; - } else { - // decreasing (amm paying less / earning more) - assert!(prev_qas <= quote_asset_amount_surplus); - prev_qas = quote_asset_amount_surplus; - } - - if quote_asset_amount_surplus < 0 { - neg = true; - assert!(!pos); // neg first - } else if quote_asset_amount_surplus > 0 { - pos = true; - assert!(neg); // neg first - // sometimes skips over == 0 surplus - } else { - none = true; - assert!(neg); - assert!(!pos); - } - slot += 1; - } - // auction should go through both position and negative - assert!(neg); - assert!(pos); - assert!(none); - - println!("{} {} {}", neg, pos, none); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_zero_price_long_imbalance() { - let now = 0_i64; - let slot = 10_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: ((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_long: ((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, // expired auction - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - next_order_id: 1, - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 10 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - assert_eq!(taker.perp_positions[0].open_orders, 1); - - // fulfill with match - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 10 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(1), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - &mut None, - false, - ) - .unwrap(); - - assert_eq!(taker.perp_positions[0].open_orders, 0); - assert_eq!(base_asset_amount, 1000000000); - - let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.total_mm_fee, 2033008); // jit occured even tho maker offered full amount - assert_eq!(market_after.amm.total_fee, 2057287); - } -} diff --git a/programs/drift/src/controller/orders/fuel_tests.rs b/programs/drift/src/controller/orders/fuel_tests.rs index b4021e6b7b..24959f9e3b 100644 --- a/programs/drift/src/controller/orders/fuel_tests.rs +++ b/programs/drift/src/controller/orders/fuel_tests.rs @@ -5,7 +5,7 @@ use crate::math::constants::ONE_BPS_DENOMINATOR; use crate::math::margin::MarginRequirementType; use crate::state::margin_calculation::{MarginCalculation, MarginContext}; use crate::state::oracle_map::OracleMap; -use crate::state::state::{FeeStructure, FeeTier}; +use crate::state::state::{FeeStructure, FeeTier, State}; use crate::state::user::{Order, PerpPosition}; fn get_fee_structure() -> FeeStructure { @@ -27,6 +27,53 @@ fn get_user_keys() -> (Pubkey, Pubkey, Pubkey) { (Pubkey::default(), Pubkey::default(), Pubkey::default()) } +fn get_state(min_auction_duration: u8) -> State { + State { + min_perp_auction_duration: min_auction_duration, + ..State::default() + } +} + +pub fn get_amm_is_available( + order: &Order, + min_auction_duration: u8, + market: &crate::state::perp_market::PerpMarket, + oracle_map: &mut OracleMap, + slot: u64, + user_can_skip_auction_duration: bool, +) -> bool { + let state = get_state(min_auction_duration); + let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); + let mm_oracle_price_data = market + .get_mm_oracle_price_data(*oracle_price_data, slot, &state.oracle_guard_rails.validity) + .unwrap(); + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let safe_oracle_validity = crate::math::oracle::oracle_validity( + crate::state::user::MarketType::Perp, + market.market_index, + market.amm.historical_oracle_data.last_oracle_price_twap, + &safe_oracle_price_data, + &state.oracle_guard_rails.validity, + market.get_max_confidence_interval_multiplier().unwrap(), + &market.amm.oracle_source, + crate::math::oracle::LogMode::SafeMMOracle, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + ) + .unwrap(); + market + .amm_can_fill_order( + order, + slot, + crate::state::fill_mode::FillMode::Fill, + &state, + safe_oracle_validity, + user_can_skip_auction_duration, + &mm_oracle_price_data, + ) + .unwrap() +} + #[cfg(test)] pub mod fuel_scoring { use std::str::FromStr; @@ -221,9 +268,21 @@ pub mod fuel_scoring { assert_eq!(maker_stats.fuel_deposits, 0); assert_eq!(taker_stats.fuel_deposits, 0); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (ba, qa) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -241,8 +300,7 @@ pub mod fuel_scoring { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index c8428f01d0..5b97c5d14f 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -1,9 +1,11 @@ use anchor_lang::prelude::Pubkey; use anchor_lang::Owner; +use crate::math::oracle::oracle_validity; +use crate::state::fill_mode::FillMode; use crate::state::oracle_map::OracleMap; -use crate::state::perp_market::MarketStatus; -use crate::state::state::{FeeStructure, FeeTier}; +use crate::state::perp_market::{MarketStatus, PerpMarket}; +use crate::state::state::{FeeStructure, FeeTier, State}; use crate::state::user::{MarketType, Order, PerpPosition}; fn get_fee_structure() -> FeeStructure { @@ -29,6 +31,53 @@ fn get_oracle_map<'a>() -> OracleMap<'a> { OracleMap::empty() } +fn get_state(min_auction_duration: u8) -> State { + State { + min_perp_auction_duration: min_auction_duration, + ..State::default() + } +} + +pub fn get_amm_is_available( + order: &Order, + min_auction_duration: u8, + market: &PerpMarket, + oracle_map: &mut OracleMap, + slot: u64, + user_can_skip_auction_duration: bool, +) -> bool { + let state = get_state(min_auction_duration); + let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); + let mm_oracle_price_data = market + .get_mm_oracle_price_data(*oracle_price_data, slot, &state.oracle_guard_rails.validity) + .unwrap(); + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let safe_oracle_validity = oracle_validity( + MarketType::Perp, + market.market_index, + market.amm.historical_oracle_data.last_oracle_price_twap, + &safe_oracle_price_data, + &state.oracle_guard_rails.validity, + market.get_max_confidence_interval_multiplier().unwrap(), + &market.amm.oracle_source, + crate::math::oracle::LogMode::SafeMMOracle, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + ) + .unwrap(); + market + .amm_can_fill_order( + order, + slot, + FillMode::Fill, + &state, + safe_oracle_validity, + user_can_skip_auction_duration, + &mm_oracle_price_data, + ) + .unwrap() +} + pub mod fill_order_protected_maker { use std::str::FromStr; @@ -2179,6 +2228,7 @@ pub mod fulfill_order_with_maker_order { market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier().unwrap(), 0, + 0, ) .unwrap(); @@ -3314,9 +3364,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3338,8 +3400,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -3561,9 +3622,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3584,8 +3657,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -3756,9 +3828,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3776,8 +3860,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -3964,9 +4047,21 @@ pub mod fulfill_order { create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3984,8 +4079,7 @@ pub mod fulfill_order { None, now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4132,9 +4226,21 @@ pub mod fulfill_order { let mut taker_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &UserMap::empty(), @@ -4152,8 +4258,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4332,9 +4437,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let result = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4352,8 +4469,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4521,9 +4637,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let result = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4541,8 +4669,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4663,9 +4790,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4683,8 +4822,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::Immediate, + is_amm_available, FillMode::Fill, false, &mut None, @@ -4832,9 +4970,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4852,8 +5002,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -5423,9 +5572,22 @@ pub mod fulfill_order { let taker_before = taker; let maker_before = maker; + + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market_map.get_ref(&0).unwrap(), + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -5443,8 +5605,7 @@ pub mod fulfill_order { None, now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -5669,9 +5830,21 @@ pub mod fulfill_order { create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market_map.get_ref(&0).unwrap(), + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -5689,8 +5862,7 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, &mut None, @@ -12881,3 +13053,58 @@ mod update_maker_fills_map { assert_eq!(*map.get(&maker_key).unwrap(), -2 * fill as i64); } } + +mod order_is_low_risk_for_amm { + use super::*; + use crate::state::user::{OrderBitFlag, OrderStatus}; + + fn base_perp_order() -> Order { + Order { + status: OrderStatus::Open, + market_type: MarketType::Perp, + slot: 100, + ..Order::default() + } + } + + #[test] + fn older_than_oracle_delay_returns_true() { + let order = base_perp_order(); + let clock_slot = 110u64; + let mm_oracle_delay = 10i64; + + let is_low = order + .is_low_risk_for_amm(mm_oracle_delay, clock_slot, false) + .unwrap(); + assert!(is_low); + } + + #[test] + fn not_older_than_delay_returns_false() { + let order = base_perp_order(); + let clock_slot = 110u64; + + let mm_oracle_delay = 11i64; + + let is_low = order + .is_low_risk_for_amm(mm_oracle_delay, clock_slot, false) + .unwrap(); + assert!(!is_low); + } + + #[test] + fn liquidation_always_low_risk() { + let order = base_perp_order(); + let is_low = order.is_low_risk_for_amm(0, order.slot, true).unwrap(); + assert!(is_low); + } + + #[test] + fn safe_trigger_order_flag_sets_low_risk() { + let mut order = base_perp_order(); + order.add_bit_flag(OrderBitFlag::SafeTriggerOrder); + + let is_low = order.is_low_risk_for_amm(0, order.slot, false).unwrap(); + assert!(is_low); + } +} diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 11911e0356..5a0d44e15e 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -135,6 +135,7 @@ pub fn settle_pnl( .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; if !is_oracle_valid_for_action(oracle_validity, Some(DriftAction::SettlePnl))? diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 89f8305340..0741d5df16 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -6,8 +6,7 @@ use crate::controller::repeg::_update_amm; use crate::math::amm::calculate_market_open_bids_asks; use crate::math::constants::{ - AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_I128, BASE_PRECISION, BASE_PRECISION_I64, - PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, + BASE_PRECISION, BASE_PRECISION_I64, PRICE_PRECISION_I64, PRICE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::math::oracle::OracleValidity; @@ -34,7 +33,6 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::SpotPosition; use crate::test_utils::get_anchor_account_bytes; use crate::test_utils::get_hardcoded_pyth_price; -use crate::QUOTE_PRECISION_I64; use anchor_lang::prelude::{AccountLoader, Clock}; use anchor_lang::Owner; use solana_program::pubkey::Pubkey; @@ -949,6 +947,7 @@ fn amm_negative_ref_price_offset_decay_logic() { assert_eq!(perp_market.amm.last_update_slot, 353317544); perp_market.amm.curve_update_intensity = 200; + perp_market.amm.oracle_slot_delay_override = -1; let max_ref_offset = perp_market.amm.get_max_reference_price_offset().unwrap(); diff --git a/programs/drift/src/controller/repeg.rs b/programs/drift/src/controller/repeg.rs index 707ccce35a..ade1c2d90c 100644 --- a/programs/drift/src/controller/repeg.rs +++ b/programs/drift/src/controller/repeg.rs @@ -175,7 +175,8 @@ pub fn _update_amm( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, oracle::LogMode::SafeMMOracle, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )?; let mut amm_update_cost = 0; @@ -230,7 +231,7 @@ pub fn _update_amm( update_spreads(market, reserve_price_after, Some(clock_slot))?; - if is_oracle_valid_for_action(oracle_validity, Some(DriftAction::FillOrderAmm))? { + if is_oracle_valid_for_action(oracle_validity, Some(DriftAction::FillOrderAmmLowRisk))? { if !amm_not_successfully_updated { market.amm.last_update_slot = clock_slot; } @@ -265,6 +266,7 @@ pub fn update_amm_and_check_validity( &market.amm.oracle_source, LogMode::SafeMMOracle, 0, + 0, )?; validate!( diff --git a/programs/drift/src/controller/repeg/tests.rs b/programs/drift/src/controller/repeg/tests.rs index 92d012dd8e..171351437a 100644 --- a/programs/drift/src/controller/repeg/tests.rs +++ b/programs/drift/src/controller/repeg/tests.rs @@ -114,7 +114,8 @@ pub fn update_amm_test() { market.get_max_confidence_interval_multiplier().unwrap(), &market.amm.oracle_source, LogMode::ExchangeOracle, - 0, + state.oracle_guard_rails.validity.slots_before_stale_for_amm as i8, + state.oracle_guard_rails.validity.slots_before_stale_for_amm as i8, ) .unwrap() == OracleValidity::Valid; @@ -249,6 +250,7 @@ pub fn update_amm_test_bad_oracle() { &market.amm.oracle_source, LogMode::None, 0, + 0, ) .unwrap() == OracleValidity::Valid; diff --git a/programs/drift/src/controller/spot_balance.rs b/programs/drift/src/controller/spot_balance.rs index 6e34edb369..f8e1b625e9 100644 --- a/programs/drift/src/controller/spot_balance.rs +++ b/programs/drift/src/controller/spot_balance.rs @@ -412,6 +412,7 @@ pub fn update_spot_market_and_check_validity( &spot_market.oracle_source, LogMode::ExchangeOracle, 0, + 0, )?; validate!( diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 198bcd6377..18cc6d3872 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1071,8 +1071,8 @@ pub fn handle_initialize_perp_market( last_oracle_valid: false, target_base_asset_amount_per_lp: 0, per_lp_base: 0, - oracle_slot_delay_override: 0, - taker_speed_bump_override: 0, + oracle_slot_delay_override: -1, + oracle_low_risk_slot_delay_override: 0, amm_spread_adjustment: 0, mm_oracle_sequence_id: 0, net_unsettled_funding_pnl: 0, @@ -4150,20 +4150,20 @@ pub fn handle_update_perp_market_protected_maker_params( #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] -pub fn handle_update_perp_market_taker_speed_bump_override( +pub fn handle_update_perp_market_oracle_low_risk_slot_delay_override( ctx: Context, - taker_speed_bump_override: i8, + oracle_low_risk_slot_delay_override: i8, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; msg!("perp market {}", perp_market.market_index); msg!( - "perp_market.amm.taker_speed_bump_override: {:?} -> {:?}", - perp_market.amm.taker_speed_bump_override, - taker_speed_bump_override + "perp_market.amm.oracle_low_risk_slot_delay_override: {:?} -> {:?}", + perp_market.amm.oracle_low_risk_slot_delay_override, + oracle_low_risk_slot_delay_override ); - perp_market.amm.taker_speed_bump_override = taker_speed_bump_override; + perp_market.amm.oracle_low_risk_slot_delay_override = oracle_low_risk_slot_delay_override; Ok(()) } diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 3cb9d8e41a..405af3f895 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1789,6 +1789,7 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, perp_market.amm.oracle_slot_delay_override, + perp_market.amm.oracle_low_risk_slot_delay_override, )?; step_size = perp_market.amm.order_step_size; tick_size = perp_market.amm.order_tick_size; diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 42c9e9f050..a393cf0670 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1583,11 +1583,14 @@ pub mod drift { ) } - pub fn update_perp_market_taker_speed_bump_override( + pub fn update_perp_market_oracle_low_risk_slot_delay_override( ctx: Context, - taker_speed_bump_override: i8, + oracle_low_risk_slot_delay_override: i8, ) -> Result<()> { - handle_update_perp_market_taker_speed_bump_override(ctx, taker_speed_bump_override) + handle_update_perp_market_oracle_low_risk_slot_delay_override( + ctx, + oracle_low_risk_slot_delay_override, + ) } pub fn update_perp_market_amm_spread_adjustment( diff --git a/programs/drift/src/math/auction.rs b/programs/drift/src/math/auction.rs index 88049e246f..53263983c0 100644 --- a/programs/drift/src/math/auction.rs +++ b/programs/drift/src/math/auction.rs @@ -9,13 +9,10 @@ use crate::state::oracle::OraclePriceData; use crate::state::perp_market::ContractTier; use crate::state::user::{Order, OrderBitFlag, OrderType}; -use crate::state::fill_mode::FillMode; -use crate::state::perp_market::{AMMAvailability, PerpMarket}; +use crate::state::perp_market::PerpMarket; use crate::{OrderParams, MAX_PREDICTION_MARKET_PRICE}; use std::cmp::min; -use super::orders::get_posted_slot_from_clock_slot; - #[cfg(test)] mod tests; @@ -234,42 +231,6 @@ pub fn is_auction_complete(order_slot: u64, auction_duration: u8, slot: u64) -> Ok(slots_elapsed > auction_duration.cast()?) } -pub fn can_fill_with_amm( - amm_availability: AMMAvailability, - valid_oracle_price: Option, - order: &Order, - min_auction_duration: u8, - slot: u64, - fill_mode: FillMode, -) -> DriftResult { - Ok(amm_availability != AMMAvailability::Unavailable - && valid_oracle_price.is_some() - && (amm_availability == AMMAvailability::Immediate - || is_amm_available_liquidity_source(order, min_auction_duration, slot, fill_mode)?)) -} - -pub fn is_amm_available_liquidity_source( - order: &Order, - min_auction_duration: u8, - slot: u64, - fill_mode: FillMode, -) -> DriftResult { - if fill_mode.is_liquidation() { - return Ok(true); - } - - if order.is_bit_flag_set(OrderBitFlag::SafeTriggerOrder) { - return Ok(true); - } - - if order.is_signed_msg() { - let clock_slot_tail = get_posted_slot_from_clock_slot(slot); - return Ok(clock_slot_tail.wrapping_sub(order.posted_slot_tail) >= min_auction_duration); - } - - Ok(is_auction_complete(order.slot, min_auction_duration, slot)?) -} - pub fn calculate_auction_params_for_trigger_order( order: &Order, oracle_price_data: &OraclePriceData, diff --git a/programs/drift/src/math/fulfillment.rs b/programs/drift/src/math/fulfillment.rs index c4892bedf8..15103bddcd 100644 --- a/programs/drift/src/math/fulfillment.rs +++ b/programs/drift/src/math/fulfillment.rs @@ -1,12 +1,10 @@ use crate::controller::position::PositionDirection; use crate::error::DriftResult; -use crate::math::auction::can_fill_with_amm; use crate::math::casting::Cast; use crate::math::matching::do_orders_cross; use crate::math::safe_unwrap::SafeUnwrap; -use crate::state::fill_mode::FillMode; use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod}; -use crate::state::perp_market::{AMMAvailability, AMM}; +use crate::state::perp_market::AMM; use crate::state::user::Order; use solana_program::pubkey::Pubkey; @@ -18,38 +16,21 @@ pub fn determine_perp_fulfillment_methods( maker_orders_info: &[(Pubkey, usize, u64)], amm: &AMM, amm_reserve_price: u64, - valid_oracle_price: Option, limit_price: Option, - amm_availability: AMMAvailability, - slot: u64, - min_auction_duration: u8, - fill_mode: FillMode, + amm_is_available: bool, ) -> DriftResult> { if order.post_only { return determine_perp_fulfillment_methods_for_maker( order, amm, amm_reserve_price, - valid_oracle_price, limit_price, - amm_availability, - slot, - min_auction_duration, - fill_mode, + amm_is_available, ); } let mut fulfillment_methods = Vec::with_capacity(8); - let can_fill_with_amm = can_fill_with_amm( - amm_availability, - valid_oracle_price, - order, - min_auction_duration, - slot, - fill_mode, - )?; - let maker_direction = order.direction.opposite(); let mut amm_price = match maker_direction { @@ -67,7 +48,7 @@ pub fn determine_perp_fulfillment_methods( break; } - if can_fill_with_amm { + if amm_is_available { let maker_better_than_amm = match order.direction { PositionDirection::Long => *maker_price <= amm_price, PositionDirection::Short => *maker_price >= amm_price, @@ -90,7 +71,7 @@ pub fn determine_perp_fulfillment_methods( } } - if can_fill_with_amm { + if amm_is_available { let taker_crosses_amm = match limit_price { Some(taker_price) => do_orders_cross(maker_direction, amm_price, taker_price), None => true, @@ -108,25 +89,12 @@ fn determine_perp_fulfillment_methods_for_maker( order: &Order, amm: &AMM, amm_reserve_price: u64, - valid_oracle_price: Option, limit_price: Option, - amm_availability: AMMAvailability, - slot: u64, - min_auction_duration: u8, - fill_mode: FillMode, + amm_is_available: bool, ) -> DriftResult> { let maker_direction = order.direction; - let can_fill_with_amm = can_fill_with_amm( - amm_availability, - valid_oracle_price, - order, - min_auction_duration, - slot, - fill_mode, - )?; - - if !can_fill_with_amm { + if !amm_is_available { return Ok(vec![]); } diff --git a/programs/drift/src/math/fulfillment/tests.rs b/programs/drift/src/math/fulfillment/tests.rs index 23e32cb923..d12afa60b1 100644 --- a/programs/drift/src/math/fulfillment/tests.rs +++ b/programs/drift/src/math/fulfillment/tests.rs @@ -61,12 +61,8 @@ mod determine_perp_fulfillment_methods { &[(Pubkey::default(), 0, 103 * PRICE_PRECISION_U64)], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -122,12 +118,8 @@ mod determine_perp_fulfillment_methods { &[(Pubkey::default(), 0, 99 * PRICE_PRECISION_U64)], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -195,12 +187,8 @@ mod determine_perp_fulfillment_methods { &[(Pubkey::default(), 0, 101 * PRICE_PRECISION_U64)], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -266,12 +254,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -342,12 +326,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -417,12 +397,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -490,12 +466,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -562,12 +534,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -633,12 +601,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -706,12 +670,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -770,12 +730,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -832,12 +788,8 @@ mod determine_perp_fulfillment_methods { &[], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -894,12 +846,8 @@ mod determine_perp_fulfillment_methods { &[], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 2285c23bc1..6af33cf411 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -304,6 +304,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.historical_oracle_data.last_oracle_price_twap, spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let mut skip_token_value = false; @@ -556,6 +557,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( .last_oracle_price_twap, quote_spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let strict_quote_price = StrictOraclePrice::new( @@ -574,6 +576,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; let perp_position_custom_margin_ratio = @@ -989,6 +992,7 @@ pub fn calculate_user_equity( spot_market.historical_oracle_data.last_oracle_price_twap, spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; all_oracles_valid &= is_oracle_valid_for_action(oracle_validity, Some(DriftAction::MarginCalc))?; @@ -1019,6 +1023,7 @@ pub fn calculate_user_equity( .last_oracle_price_twap, quote_spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; all_oracles_valid &= @@ -1034,6 +1039,7 @@ pub fn calculate_user_equity( market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier()?, 0, + 0, )?; all_oracles_valid &= diff --git a/programs/drift/src/math/oracle.rs b/programs/drift/src/math/oracle.rs index 78be9ce782..1ac2c0e158 100644 --- a/programs/drift/src/math/oracle.rs +++ b/programs/drift/src/math/oracle.rs @@ -24,7 +24,10 @@ pub enum OracleValidity { TooUncertain, StaleForMargin, InsufficientDataPoints, - StaleForAMM, + StaleForAMM { + immediate: bool, + low_risk: bool, + }, #[default] Valid, } @@ -37,7 +40,7 @@ impl OracleValidity { OracleValidity::TooUncertain => ErrorCode::OracleTooUncertain, OracleValidity::StaleForMargin => ErrorCode::OracleStaleForMargin, OracleValidity::InsufficientDataPoints => ErrorCode::OracleInsufficientDataPoints, - OracleValidity::StaleForAMM => ErrorCode::OracleStaleForAMM, + OracleValidity::StaleForAMM { .. } => ErrorCode::OracleStaleForAMM, OracleValidity::Valid => unreachable!(), } } @@ -51,7 +54,18 @@ impl fmt::Display for OracleValidity { OracleValidity::TooUncertain => write!(f, "TooUncertain"), OracleValidity::StaleForMargin => write!(f, "StaleForMargin"), OracleValidity::InsufficientDataPoints => write!(f, "InsufficientDataPoints"), - OracleValidity::StaleForAMM => write!(f, "StaleForAMM"), + OracleValidity::StaleForAMM { + immediate, + low_risk, + } => { + if *immediate { + write!(f, "StaleForAMM (immediate)") + } else if *low_risk { + write!(f, "StaleForAMM (low risk)") + } else { + write!(f, "StaleForAMM") + } + } OracleValidity::Valid => write!(f, "Valid"), } } @@ -63,7 +77,8 @@ pub enum DriftAction { SettlePnl, TriggerOrder, FillOrderMatch, - FillOrderAmm, + FillOrderAmmLowRisk, + FillOrderAmmImmediate, Liquidate, MarginCalc, UpdateTwap, @@ -78,15 +93,25 @@ pub fn is_oracle_valid_for_action( ) -> DriftResult { let is_ok = match action { Some(action) => match action { - DriftAction::FillOrderAmm => { + DriftAction::FillOrderAmmImmediate => { matches!(oracle_validity, OracleValidity::Valid) } + DriftAction::FillOrderAmmLowRisk => { + matches!( + oracle_validity, + OracleValidity::Valid + | OracleValidity::StaleForAMM { + immediate: _, + low_risk: false + } + ) + } // relax oracle staleness, later checks for sufficiently recent amm slot update for funding update DriftAction::UpdateFunding => { matches!( oracle_validity, OracleValidity::Valid - | OracleValidity::StaleForAMM + | OracleValidity::StaleForAMM { .. } | OracleValidity::InsufficientDataPoints | OracleValidity::StaleForMargin ) @@ -95,7 +120,7 @@ pub fn is_oracle_valid_for_action( matches!( oracle_validity, OracleValidity::Valid - | OracleValidity::StaleForAMM + | OracleValidity::StaleForAMM { .. } | OracleValidity::InsufficientDataPoints ) } @@ -113,7 +138,7 @@ pub fn is_oracle_valid_for_action( DriftAction::SettlePnl => matches!( oracle_validity, OracleValidity::Valid - | OracleValidity::StaleForAMM + | OracleValidity::StaleForAMM { .. } | OracleValidity::InsufficientDataPoints | OracleValidity::StaleForMargin ), @@ -184,6 +209,7 @@ pub fn get_oracle_status( guard_rails: &OracleGuardRails, reserve_price: u64, ) -> DriftResult { + let slot_delay_override = guard_rails.validity.slots_before_stale_for_amm.cast()?; let oracle_validity = oracle_validity( MarketType::Perp, market.market_index, @@ -193,7 +219,8 @@ pub fn get_oracle_status( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, LogMode::None, - 0, + slot_delay_override, + slot_delay_override, )?; let oracle_reserve_price_spread_pct = amm::calculate_oracle_twap_5min_price_spread_pct(&market.amm, reserve_price)?; @@ -227,7 +254,8 @@ pub fn oracle_validity( max_confidence_interval_multiplier: u64, oracle_source: &OracleSource, log_mode: LogMode, - slots_before_stale_for_amm_override: i8, + slots_before_stale_for_amm_immdiate_override: i8, + oracle_low_risk_slot_delay_override: i8, ) -> DriftResult { let OraclePriceData { price: oracle_price, @@ -252,15 +280,21 @@ pub fn oracle_validity( .confidence_interval_max_size .safe_mul(max_confidence_interval_multiplier)?); - let is_stale_for_amm = if slots_before_stale_for_amm_override != 0 { - oracle_delay.gt(&slots_before_stale_for_amm_override.max(0).cast()?) + let is_stale_for_amm_immediate = if slots_before_stale_for_amm_immdiate_override != 0 { + oracle_delay.gt(&slots_before_stale_for_amm_immdiate_override.max(0).cast()?) + } else { + true + }; + + let is_stale_for_amm_low_risk = if oracle_low_risk_slot_delay_override != 0 { + oracle_delay.gt(&oracle_low_risk_slot_delay_override.max(0).cast()?) } else { oracle_delay.gt(&valid_oracle_guard_rails.slots_before_stale_for_amm) }; let is_stale_for_margin = if matches!( oracle_source, - OracleSource::PythStableCoinPull | OracleSource::PythStableCoin + OracleSource::PythStableCoinPull | OracleSource::PythLazerStableCoin ) { oracle_delay.gt(&(valid_oracle_guard_rails .slots_before_stale_for_margin @@ -279,8 +313,11 @@ pub fn oracle_validity( OracleValidity::StaleForMargin } else if !has_sufficient_number_of_data_points { OracleValidity::InsufficientDataPoints - } else if is_stale_for_amm { - OracleValidity::StaleForAMM + } else if is_stale_for_amm_immediate || is_stale_for_amm_low_risk { + OracleValidity::StaleForAMM { + immediate: is_stale_for_amm_immediate, + low_risk: is_stale_for_amm_low_risk, + } } else { OracleValidity::Valid }; @@ -332,13 +369,16 @@ pub fn oracle_validity( ); } - if is_stale_for_amm || is_stale_for_margin { + if is_stale_for_amm_immediate || is_stale_for_margin || is_stale_for_amm_low_risk { crate::msg!( - "Invalid {} {} {} Oracle: Stale (oracle_delay={:?})", + "Invalid {} {} {} Oracle: Stale (oracle_delay={:?}), (stale_for_amm_immediate={}, stale_for_amm_low_risk={}, stale_for_margin={})", market_type, market_index, oracle_type, - oracle_delay + oracle_delay, + is_stale_for_amm_immediate, + is_stale_for_amm_low_risk, + is_stale_for_margin ); } } diff --git a/programs/drift/src/math/repeg.rs b/programs/drift/src/math/repeg.rs index 4cab184297..995e425f64 100644 --- a/programs/drift/src/math/repeg.rs +++ b/programs/drift/src/math/repeg.rs @@ -46,7 +46,8 @@ pub fn calculate_repeg_validity_from_oracle_account( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, oracle::LogMode::ExchangeOracle, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )? == OracleValidity::Valid; let (oracle_is_valid, direction_valid, profitability_valid, price_impact_valid) = diff --git a/programs/drift/src/math/repeg/tests.rs b/programs/drift/src/math/repeg/tests.rs index 4beb1c7bfb..0619d0b35f 100644 --- a/programs/drift/src/math/repeg/tests.rs +++ b/programs/drift/src/math/repeg/tests.rs @@ -256,7 +256,7 @@ fn calculate_optimal_peg_and_budget_2_test() { let oracle_price_data = OraclePriceData { price: (17_800 * PRICE_PRECISION) as i64, confidence: 10233, - delay: 2, + delay: 0, has_sufficient_number_of_data_points: true, sequence_id: None, }; diff --git a/programs/drift/src/state/oracle_map.rs b/programs/drift/src/state/oracle_map.rs index dc574b8d98..47c0daba1d 100644 --- a/programs/drift/src/state/oracle_map.rs +++ b/programs/drift/src/state/oracle_map.rs @@ -89,6 +89,7 @@ impl<'a> OracleMap<'a> { last_oracle_price_twap: i64, max_confidence_interval_multiplier: u64, slots_before_stale_for_amm_override: i8, + oracle_low_risk_slot_delay_override_override: i8, ) -> DriftResult<(&OraclePriceData, OracleValidity)> { if self.should_get_quote_asset_price_data(&oracle_id.0) { return Ok((&self.quote_asset_price_data, OracleValidity::Valid)); @@ -110,6 +111,7 @@ impl<'a> OracleMap<'a> { &oracle_id.1, LogMode::ExchangeOracle, slots_before_stale_for_amm_override, + oracle_low_risk_slot_delay_override_override, )?; self.validity.insert(*oracle_id, oracle_validity); oracle_validity @@ -140,6 +142,7 @@ impl<'a> OracleMap<'a> { &oracle_id.1, LogMode::ExchangeOracle, slots_before_stale_for_amm_override, + oracle_low_risk_slot_delay_override_override, )?; self.validity.insert(*oracle_id, oracle_validity); diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index ba0cd0ee10..15d6462c83 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1,6 +1,7 @@ use crate::msg; +use crate::state::fill_mode::FillMode; use crate::state::pyth_lazer_oracle::PythLazerOracle; -use crate::state::user::MarketType; +use crate::state::user::{MarketType, Order}; use anchor_lang::prelude::*; use crate::state::state::{State, ValidityGuardRails}; @@ -46,7 +47,9 @@ use static_assertions::const_assert_eq; use super::oracle_map::OracleIdentifier; use super::protected_maker_mode_config::ProtectedMakerParams; -use crate::math::oracle::{oracle_validity, LogMode, OracleValidity}; +use crate::math::oracle::{ + is_oracle_valid_for_action, oracle_validity, DriftAction, LogMode, OracleValidity, +}; #[cfg(test)] mod tests; @@ -137,13 +140,6 @@ impl ContractTier { } } -#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, PartialOrd, Ord)] -pub enum AMMAvailability { - Immediate, - AfterMinDuration, - Unavailable, -} - #[account(zero_copy(unsafe))] #[derive(Eq, PartialEq, Debug)] #[repr(C)] @@ -329,16 +325,12 @@ impl PerpMarket { let amm_low_inventory_and_profitable = self.amm.net_revenue_since_last_funding >= DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT && amm_has_low_enough_inventory; - let amm_oracle_no_latency = self.amm.oracle_source == OracleSource::Prelaunch - || (self.amm.historical_oracle_data.last_oracle_delay == 0 - && self.amm.oracle_source == OracleSource::PythLazer); - let can_skip = amm_low_inventory_and_profitable || amm_oracle_no_latency; - if can_skip { + if amm_low_inventory_and_profitable { msg!("market {} amm skipping auction duration", self.market_index); } - Ok(can_skip) + Ok(amm_low_inventory_and_profitable) } pub fn has_too_much_drawdown(&self) -> DriftResult { @@ -736,14 +728,6 @@ impl PerpMarket { } } - pub fn get_min_perp_auction_duration(&self, default_min_auction_duration: u8) -> u8 { - if self.amm.taker_speed_bump_override != 0 { - self.amm.taker_speed_bump_override.max(0).unsigned_abs() - } else { - default_min_auction_duration - } - } - pub fn get_trigger_price( &self, oracle_price: i64, @@ -882,6 +866,7 @@ impl PerpMarket { &self.amm.oracle_source, LogMode::MMOracle, self.amm.oracle_slot_delay_override, + self.amm.oracle_low_risk_slot_delay_override, )? }; Ok(MMOraclePriceData::new( @@ -892,6 +877,83 @@ impl PerpMarket { oracle_price_data, )?) } + + pub fn amm_can_fill_order( + &self, + order: &Order, + clock_slot: u64, + fill_mode: FillMode, + state: &State, + safe_oracle_validity: OracleValidity, + user_can_skip_auction_duration: bool, + mm_oracle_price_data: &MMOraclePriceData, + ) -> DriftResult { + if self.is_operation_paused(PerpOperation::AmmFill) { + return Ok(false); + } + + if self.has_too_much_drawdown()? { + return Ok(false); + } + + // We are already using safe oracle data from MM oracle. + // But AMM isnt available if we could have used MM oracle but fell back due to price diff + // This is basically early volatility protection + let mm_oracle_not_too_volatile = + if mm_oracle_price_data.is_enabled() && mm_oracle_price_data.is_mm_oracle_as_recent() { + let amm_available = !mm_oracle_price_data.is_mm_exchange_diff_bps_high(); + amm_available + } else { + true + }; + + if !mm_oracle_not_too_volatile { + msg!("AMM cannot fill order: MM oracle too volatile compared to exchange oracle"); + return Ok(false); + } + + // Determine if order is fillable with low risk + let oracle_valid_for_amm_fill_low_risk = is_oracle_valid_for_action( + safe_oracle_validity, + Some(DriftAction::FillOrderAmmLowRisk), + )?; + if !oracle_valid_for_amm_fill_low_risk { + msg!("AMM cannot fill order: oracle not valid for low risk fills"); + return Ok(false); + } + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let can_fill_low_risk = order.is_low_risk_for_amm( + safe_oracle_price_data.delay, + clock_slot, + fill_mode.is_liquidation(), + )?; + + // Proceed if order is low risk and we can fill it. Otherwise check if we can higher risk order immediately + let can_fill_order = if can_fill_low_risk { + true + } else { + let oracle_valid_for_can_fill_immediately = is_oracle_valid_for_action( + safe_oracle_validity, + Some(DriftAction::FillOrderAmmImmediate), + )?; + if !oracle_valid_for_can_fill_immediately { + msg!("AMM cannot fill order: oracle not valid for immediate fills"); + return Ok(false); + } + let amm_wants_to_jit_make = self.amm.amm_wants_to_jit_make(order.direction)?; + let amm_has_low_enough_inventory = self + .amm + .amm_has_low_enough_inventory(amm_wants_to_jit_make)?; + let amm_can_skip_duration = + self.can_skip_auction_duration(&state, amm_has_low_enough_inventory)?; + + amm_can_skip_duration + && oracle_valid_for_can_fill_immediately + && user_can_skip_auction_duration + }; + + Ok(can_fill_order) + } } #[cfg(test)] @@ -1191,7 +1253,7 @@ pub struct AMM { pub per_lp_base: i8, /// the override for the state.min_perp_auction_duration /// 0 is no override, -1 is disable speed bump, 1-100 is literal speed bump - pub taker_speed_bump_override: i8, + pub oracle_low_risk_slot_delay_override: i8, /// signed scale amm_spread similar to fee_adjustment logic (-100 = 0, 100 = double) pub amm_spread_adjustment: i8, pub oracle_slot_delay_override: i8, @@ -1285,9 +1347,9 @@ impl Default for AMM { last_oracle_valid: false, target_base_asset_amount_per_lp: 0, per_lp_base: 0, - taker_speed_bump_override: 0, + oracle_low_risk_slot_delay_override: 0, amm_spread_adjustment: 0, - oracle_slot_delay_override: 0, + oracle_slot_delay_override: -1, mm_oracle_sequence_id: 0, net_unsettled_funding_pnl: 0, quote_asset_amount_with_unsettled_lp: 0, diff --git a/programs/drift/src/state/perp_market/tests.rs b/programs/drift/src/state/perp_market/tests.rs index f98e7ef01e..24e34a9e14 100644 --- a/programs/drift/src/state/perp_market/tests.rs +++ b/programs/drift/src/state/perp_market/tests.rs @@ -218,61 +218,6 @@ mod get_margin_ratio { } } -mod get_min_perp_auction_duration { - use crate::state::perp_market::{PerpMarket, AMM}; - use crate::State; - - #[test] - fn test_get_speed_bump() { - let perp_market = PerpMarket { - amm: AMM { - taker_speed_bump_override: 0, - ..AMM::default() - }, - ..PerpMarket::default() - }; - - let state = State { - min_perp_auction_duration: 10, - ..State::default() - }; - - // no override uses state value - assert_eq!( - perp_market.get_min_perp_auction_duration(state.min_perp_auction_duration), - 10 - ); - - let perp_market = PerpMarket { - amm: AMM { - taker_speed_bump_override: -1, - ..AMM::default() - }, - ..PerpMarket::default() - }; - - // -1 override disables speed bump - assert_eq!( - perp_market.get_min_perp_auction_duration(state.min_perp_auction_duration), - 0 - ); - - let perp_market = PerpMarket { - amm: AMM { - taker_speed_bump_override: 20, - ..AMM::default() - }, - ..PerpMarket::default() - }; - - // positive override uses override value - assert_eq!( - perp_market.get_min_perp_auction_duration(state.min_perp_auction_duration), - 20 - ); - } -} - mod get_trigger_price { use crate::state::perp_market::HistoricalOracleData; use crate::state::perp_market::{PerpMarket, AMM}; @@ -452,3 +397,243 @@ mod get_trigger_price { assert_eq!(clamped_price, large_oracle_price + max_oracle_diff_large); } } + +mod amm_can_fill_order_tests { + use crate::controller::position::PositionDirection; + use crate::math::oracle::OracleValidity; + use crate::state::fill_mode::FillMode; + use crate::state::oracle::{MMOraclePriceData, OraclePriceData}; + use crate::state::paused_operations::PerpOperation; + use crate::state::perp_market::{PerpMarket, AMM}; + use crate::state::state::{State, ValidityGuardRails}; + use crate::state::user::{Order, OrderStatus}; + use crate::PRICE_PRECISION_I64; + + fn base_state() -> State { + State { + min_perp_auction_duration: 10, + ..State::default() + } + } + + fn base_market() -> PerpMarket { + PerpMarket { + amm: AMM { + mm_oracle_price: PRICE_PRECISION_I64, + mm_oracle_slot: 0, + order_step_size: 1, + amm_jit_intensity: 100, + ..AMM::default() + }, + ..PerpMarket::default() + } + } + + fn base_order() -> Order { + Order { + status: OrderStatus::Open, + slot: 0, + base_asset_amount: 1, + direction: PositionDirection::Long, + ..Order::default() + } + } + + fn mm_oracle_ok_and_as_recent() -> (MMOraclePriceData, ValidityGuardRails) { + let exchange = OraclePriceData { + price: PRICE_PRECISION_I64, + confidence: 1, + delay: 5, + has_sufficient_number_of_data_points: true, + sequence_id: Some(100), + }; + let mm = + MMOraclePriceData::new(PRICE_PRECISION_I64, 5, 100, OracleValidity::Valid, exchange) + .unwrap(); + (mm, ValidityGuardRails::default()) + } + + #[test] + fn paused_operation_returns_false() { + let mut market = base_market(); + // Pause AMM fill + market.paused_operations = PerpOperation::AmmFill as u8; + let order = base_order(); + let state = base_state(); + let (mm, guard) = mm_oracle_ok_and_as_recent(); + + let can = market + .amm_can_fill_order( + &order, + 10, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(!can); + } + + #[test] + fn mm_oracle_too_volatile_blocks() { + let market = base_market(); + let order = base_order(); + let state = base_state(); + + // Create MM oracle data with >1% diff vs exchange to force fallback and block + let exchange = OraclePriceData { + price: PRICE_PRECISION_I64, // 1.0 + confidence: 1, + delay: 1, + has_sufficient_number_of_data_points: true, + sequence_id: Some(100), + }; + // 3% higher than exchange + let mm = MMOraclePriceData::new( + PRICE_PRECISION_I64 + (PRICE_PRECISION_I64 / 33), + 1, + 99, + OracleValidity::Valid, + exchange, + ) + .unwrap(); + + let can = market + .amm_can_fill_order( + &order, + 10, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(!can); + } + + #[test] + fn low_risk_path_succeeds_when_auction_elapsed() { + let market = base_market(); + let mut order = base_order(); + order.slot = 0; + + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // clock_slot sufficiently beyond min_auction_duration + let can = market + .amm_can_fill_order( + &order, + 15, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(can); + } + + #[test] + fn low_risk_path_succeeds_when_auction_elapsed_with_stale_for_immediate() { + let market = base_market(); + let mut order = base_order(); + order.slot = 0; // order placed at slot 0 + + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // clock_slot sufficiently beyond min_auction_duration + let can = market + .amm_can_fill_order( + &order, + 15, + FillMode::Fill, + &state, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: false, + }, + true, + &mm, + ) + .unwrap(); + assert!(can); + } + + #[test] + fn high_risk_immediate_requires_user_and_market_skip() { + let mut market = base_market(); + let mut order = base_order(); + order.slot = 20; + market.amm.amm_jit_intensity = 100; + + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // cnat fill if user cant skip auction duration + let can1 = market + .amm_can_fill_order( + &order, + 21, + FillMode::Fill, + &state, + OracleValidity::Valid, + false, + &mm, + ) + .unwrap(); + assert!(!can1); + + // valid oracle for immediate and user can skip, market can skip due to low inventory => can fill + market.amm.base_asset_amount_with_amm = -2; // taker long improves balance + market.amm.order_step_size = 1; + market.amm.base_asset_reserve = 1_000_000; + market.amm.quote_asset_reserve = 1_000_000; + market.amm.sqrt_k = 1_000_000; + market.amm.max_base_asset_reserve = 2_000_000; + market.amm.min_base_asset_reserve = 0; + + let can2 = market + .amm_can_fill_order( + &order, + 21, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(can2); + } + + #[test] + fn invalid_safe_oracle_validity_blocks_low_risk() { + let market = base_market(); + let order = base_order(); + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // Order is old but invalid oracle validity + let can = market + .amm_can_fill_order( + &order, + 20, + FillMode::Fill, + &state, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: true, + }, + true, + &mm, + ) + .unwrap(); + assert!(!can); + } +} diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index b36607e3dc..ef702e90e1 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1524,6 +1524,26 @@ impl Order { || (self.triggered() && !(self.reduce_only && self.is_bit_flag_set(OrderBitFlag::NewTriggerReduceOnly))) } + + pub fn is_low_risk_for_amm( + &self, + mm_oracle_delay: i64, + clock_slot: u64, + is_liquidation: bool, + ) -> DriftResult { + if self.market_type == MarketType::Spot { + return Ok(false); + } + + let order_older_than_oracle_delay = { + let clock_minus_delay = clock_slot.cast::()?.safe_sub(mm_oracle_delay)?; + clock_minus_delay >= self.slot.cast::()? + }; + + Ok(order_older_than_oracle_delay + || is_liquidation + || self.is_bit_flag_set(OrderBitFlag::SafeTriggerOrder)) + } } impl Default for Order { diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index ad5307ad70..693637f55f 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4048,34 +4048,34 @@ export class AdminClient extends DriftClient { ); } - public async updatePerpMarketTakerSpeedBumpOverride( + public async updatePerpMarketOracleLowRiskSlotDelayOverride( perpMarketIndex: number, - takerSpeedBumpOverride: number + oracleLowRiskSlotDelayOverride: number ): Promise { - const updatePerpMarketTakerSpeedBumpOverrideIx = - await this.getUpdatePerpMarketTakerSpeedBumpOverrideIx( + const updatePerpMarketOracleLowRiskSlotDelayOverrideIx = + await this.getUpdatePerpMarketOracleLowRiskSlotDelayOverrideIx( perpMarketIndex, - takerSpeedBumpOverride + oracleLowRiskSlotDelayOverride ); const tx = await this.buildTransaction( - updatePerpMarketTakerSpeedBumpOverrideIx + updatePerpMarketOracleLowRiskSlotDelayOverrideIx ); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getUpdatePerpMarketTakerSpeedBumpOverrideIx( + public async getUpdatePerpMarketOracleLowRiskSlotDelayOverrideIx( perpMarketIndex: number, - takerSpeedBumpOverride: number + oracleLowRiskSlotDelayOverride: number ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, perpMarketIndex ); - return await this.program.instruction.updatePerpMarketTakerSpeedBumpOverride( - takerSpeedBumpOverride, + return await this.program.instruction.updatePerpMarketOracleLowRiskSlotDelayOverride( + oracleLowRiskSlotDelayOverride, { accounts: { admin: this.useHotWalletAdmin diff --git a/sdk/src/dlob/DLOB.ts b/sdk/src/dlob/DLOB.ts index 15c2ec72f2..5b8ec0c896 100644 --- a/sdk/src/dlob/DLOB.ts +++ b/sdk/src/dlob/DLOB.ts @@ -467,10 +467,6 @@ export class DLOB { const isAmmPaused = ammPaused(stateAccount, marketAccount); - const minAuctionDuration = isVariant(marketType, 'perp') - ? stateAccount.minPerpAuctionDuration - : 0; - const { makerRebateNumerator, makerRebateDenominator } = this.getMakerRebate(marketType, stateAccount, marketAccount); @@ -481,7 +477,8 @@ export class DLOB { marketType, oraclePriceData, isAmmPaused, - minAuctionDuration, + stateAccount, + marketAccount, fallbackAsk, fallbackBid ); @@ -493,7 +490,8 @@ export class DLOB { marketType, oraclePriceData, isAmmPaused, - minAuctionDuration, + stateAccount, + marketAccount, makerRebateNumerator, makerRebateDenominator, fallbackAsk, @@ -597,7 +595,10 @@ export class DLOB { ? OraclePriceData : MMOraclePriceData, isAmmPaused: boolean, - minAuctionDuration: number, + stateAccount: StateAccount, + marketAccount: T extends { spot: unknown } + ? SpotMarketAccount + : PerpMarketAccount, makerRebateNumerator: number, makerRebateDenominator: number, fallbackAsk: BN | undefined, @@ -636,7 +637,8 @@ export class DLOB { (askPrice) => { return askPrice.lte(fallbackBidWithBuffer); }, - minAuctionDuration + stateAccount, + marketAccount ); for (const askCrossingFallback of asksCrossingFallback) { @@ -664,7 +666,8 @@ export class DLOB { (bidPrice) => { return bidPrice.gte(fallbackAskWithBuffer); }, - minAuctionDuration + stateAccount, + marketAccount ); for (const bidCrossingFallback of bidsCrossingFallback) { @@ -683,7 +686,10 @@ export class DLOB { ? OraclePriceData : MMOraclePriceData, isAmmPaused: boolean, - minAuctionDuration: number, + state: StateAccount, + marketAccount: T extends { spot: unknown } + ? SpotMarketAccount + : PerpMarketAccount, fallbackAsk: BN | undefined, fallbackBid?: BN | undefined ): NodeToFill[] { @@ -736,7 +742,8 @@ export class DLOB { (takerPrice) => { return takerPrice === undefined || takerPrice.lte(fallbackBid); }, - minAuctionDuration + state, + marketAccount ); for (const takingAskCrossingFallback of takingAsksCrossingFallback) { @@ -793,7 +800,8 @@ export class DLOB { (takerPrice) => { return takerPrice === undefined || takerPrice.gte(fallbackAsk); }, - minAuctionDuration + state, + marketAccount ); for (const marketBidCrossingFallback of takingBidsCrossingFallback) { nodesToFill.push(marketBidCrossingFallback); @@ -911,7 +919,10 @@ export class DLOB { : MMOraclePriceData, nodeGenerator: Generator, doesCross: (nodePrice: BN | undefined) => boolean, - minAuctionDuration: number + state: StateAccount, + marketAccount: T extends { spot: unknown } + ? SpotMarketAccount + : PerpMarketAccount ): NodeToFill[] { const nodesToFill = new Array(); @@ -934,8 +945,10 @@ export class DLOB { isVariant(marketType, 'spot') || isFallbackAvailableLiquiditySource( node.order, - minAuctionDuration, - slot + oraclePriceData as MMOraclePriceData, + slot, + state, + marketAccount as PerpMarketAccount ); if (crosses && fallbackAvailable) { diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index eed45293ca..7f5ca59818 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -6566,7 +6566,7 @@ ] }, { - "name": "updatePerpMarketTakerSpeedBumpOverride", + "name": "updatePerpMarketOracleLowRiskSlotDelayOverride", "accounts": [ { "name": "admin", @@ -6586,7 +6586,7 @@ ], "args": [ { - "name": "takerSpeedBumpOverride", + "name": "oracleLowRiskSlotDelayOverride", "type": "i8" } ] @@ -11194,7 +11194,7 @@ "type": "i8" }, { - "name": "takerSpeedBumpOverride", + "name": "oracleLowRiskSlotDelayOverride", "docs": [ "the override for the state.min_perp_auction_duration", "0 is no override, -1 is disable speed bump, 1-100 is literal speed bump" @@ -12278,7 +12278,17 @@ "name": "InsufficientDataPoints" }, { - "name": "StaleForAMM" + "name": "StaleForAMM", + "fields": [ + { + "name": "immediate", + "type": "bool" + }, + { + "name": "lowRisk", + "type": "bool" + } + ] }, { "name": "Valid" @@ -12304,7 +12314,10 @@ "name": "FillOrderMatch" }, { - "name": "FillOrderAmm" + "name": "FillOrderAmmLowRisk" + }, + { + "name": "FillOrderAmmImmediate" }, { "name": "Liquidate" @@ -12954,23 +12967,6 @@ ] } }, - { - "name": "AMMAvailability", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Immediate" - }, - { - "name": "AfterMinDuration" - }, - { - "name": "Unavailable" - } - ] - } - }, { "name": "RevenueShareOrderBitFlag", "type": { diff --git a/sdk/src/math/auction.ts b/sdk/src/math/auction.ts index cabf5f4860..013f1388b7 100644 --- a/sdk/src/math/auction.ts +++ b/sdk/src/math/auction.ts @@ -1,4 +1,12 @@ -import { isOneOfVariant, isVariant, Order, PositionDirection } from '../types'; +import { + isOneOfVariant, + isVariant, + OracleValidity, + Order, + PerpOperation, + PositionDirection, + StateAccount, +} from '../types'; import { BN } from '@coral-xyz/anchor'; import { ONE, @@ -8,6 +16,10 @@ import { } from '../constants/numericConstants'; import { getVariant, OrderBitFlag, PerpMarketAccount } from '../types'; import { getPerpMarketTierNumber } from './tiers'; +import { MMOraclePriceData } from '../oracles/types'; +import { isLowRiskForAmm } from './orders'; +import { getOracleValidity } from './oracles'; +import { isOperationPaused } from './exchangeStatus'; export function isAuctionComplete(order: Order, slot: number): boolean { if (order.auctionDuration === 0) { @@ -19,18 +31,49 @@ export function isAuctionComplete(order: Order, slot: number): boolean { export function isFallbackAvailableLiquiditySource( order: Order, - minAuctionDuration: number, - slot: number + mmOraclePriceData: MMOraclePriceData, + slot: number, + state: StateAccount, + market: PerpMarketAccount, + isLiquidation?: boolean ): boolean { - if (minAuctionDuration === 0) { - return true; + if (isOperationPaused(market.pausedOperations, PerpOperation.AMM_FILL)) { + return false; + } + + // TODO: include too much drawdown check & mm oracle volatility + + const oracleValidity = getOracleValidity( + market!, + { + price: mmOraclePriceData.price, + slot: mmOraclePriceData.slot, + confidence: mmOraclePriceData.confidence, + hasSufficientNumberOfDataPoints: + mmOraclePriceData.hasSufficientNumberOfDataPoints, + }, + state.oracleGuardRails, + new BN(slot) + ); + if (oracleValidity <= OracleValidity.StaleForAMMLowRisk) { + return false; } - if ((order.bitFlags & OrderBitFlag.SafeTriggerOrder) !== 0) { + if (oracleValidity == OracleValidity.Valid) { return true; } - return new BN(slot).sub(order.slot).gt(new BN(minAuctionDuration)); + const isOrderLowRiskForAmm = isLowRiskForAmm( + order, + mmOraclePriceData, + isLiquidation + ); + + if (!isOrderLowRiskForAmm) { + return false; + } else { + return true; + } } /** diff --git a/sdk/src/math/oracles.ts b/sdk/src/math/oracles.ts index 6575ed7a1d..27248326f0 100644 --- a/sdk/src/math/oracles.ts +++ b/sdk/src/math/oracles.ts @@ -3,7 +3,9 @@ import { HistoricalOracleData, OracleGuardRails, OracleSource, + OracleValidity, PerpMarketAccount, + isOneOfVariant, isVariant, } from '../types'; import { OraclePriceData } from '../oracles/types'; @@ -14,6 +16,7 @@ import { ZERO, FIVE_MINUTE, PERCENTAGE_PRECISION, + FIVE, } from '../constants/numericConstants'; import { assert } from '../assert/assert'; import { BN } from '@coral-xyz/anchor'; @@ -51,6 +54,91 @@ export function getMaxConfidenceIntervalMultiplier( return maxConfidenceIntervalMultiplier; } +export function getOracleValidity( + market: PerpMarketAccount, + oraclePriceData: OraclePriceData, + oracleGuardRails: OracleGuardRails, + slot: BN, + oracleStalenessBuffer = FIVE +): OracleValidity { + const isNonPositive = oraclePriceData.price.lte(ZERO); + const isTooVolatile = BN.max( + oraclePriceData.price, + market.amm.historicalOracleData.lastOraclePriceTwap + ) + .div( + BN.max( + ONE, + BN.min( + oraclePriceData.price, + market.amm.historicalOracleData.lastOraclePriceTwap + ) + ) + ) + .gt(oracleGuardRails.validity.tooVolatileRatio); + + const confPctOfPrice = oraclePriceData.confidence + .mul(BID_ASK_SPREAD_PRECISION) + .div(oraclePriceData.price); + const isConfTooLarge = confPctOfPrice.gt( + oracleGuardRails.validity.confidenceIntervalMaxSize.mul( + getMaxConfidenceIntervalMultiplier(market) + ) + ); + + const oracleDelay = slot.sub(oraclePriceData.slot).sub(oracleStalenessBuffer); + + let isStaleForAmmImmediate = true; + if (market.amm.oracleSlotDelayOverride != 0) { + isStaleForAmmImmediate = oracleDelay.gt( + BN.max(new BN(market.amm.oracleSlotDelayOverride), ZERO) + ); + } + + let isStaleForAmmLowRisk = false; + if (market.amm.oracleLowRiskSlotDelayOverride != 0) { + isStaleForAmmLowRisk = oracleDelay.gt( + BN.max(new BN(market.amm.oracleLowRiskSlotDelayOverride), ZERO) + ); + } else { + isStaleForAmmLowRisk = oracleDelay.gt( + oracleGuardRails.validity.slotsBeforeStaleForAmm + ); + } + + let isStaleForMargin = oracleDelay.gt( + new BN(oracleGuardRails.validity.slotsBeforeStaleForMargin) + ); + if ( + isOneOfVariant(market.amm.oracleSource, [ + 'pythStableCoinPull', + 'pythLazerStableCoin', + ]) + ) { + isStaleForMargin = oracleDelay.gt( + new BN(oracleGuardRails.validity.slotsBeforeStaleForMargin).muln(3) + ); + } + + if (isNonPositive) { + return OracleValidity.NonPositive; + } else if (isTooVolatile) { + return OracleValidity.TooVolatile; + } else if (isConfTooLarge) { + return OracleValidity.TooUncertain; + } else if (isStaleForMargin) { + return OracleValidity.StaleForMargin; + } else if (!oraclePriceData.hasSufficientNumberOfDataPoints) { + return OracleValidity.InsufficientDataPoints; + } else if (isStaleForAmmLowRisk) { + return OracleValidity.StaleForAMMLowRisk; + } else if (isStaleForAmmImmediate) { + return OracleValidity.isStaleForAmmImmediate; + } else { + return OracleValidity.Valid; + } +} + export function isOracleValid( market: PerpMarketAccount, oraclePriceData: OraclePriceData, diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index 2dc9196ab8..6886821be7 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -8,6 +8,8 @@ import { PositionDirection, ProtectedMakerParams, MarketTypeStr, + OrderBitFlag, + StateAccount, } from '../types'; import { ZERO, @@ -243,10 +245,16 @@ export function isFillableByVAMM( mmOraclePriceData: MMOraclePriceData, slot: number, ts: number, - minAuctionDuration: number + state: StateAccount ): boolean { return ( - (isFallbackAvailableLiquiditySource(order, minAuctionDuration, slot) && + (isFallbackAvailableLiquiditySource( + order, + mmOraclePriceData, + slot, + state, + market + ) && calculateBaseAssetAmountForAmmToFulfill( order, market, @@ -257,6 +265,26 @@ export function isFillableByVAMM( ); } +export function isLowRiskForAmm( + order: Order, + mmOraclePriceData: MMOraclePriceData, + isLiquidation?: boolean +): boolean { + if (isVariant(order.marketType, 'spot')) { + return false; + } + + const orderOlderThanOracleDelay = new BN(order.slot).lte( + mmOraclePriceData.slot + ); + + return ( + orderOlderThanOracleDelay || + isLiquidation || + (order.bitFlags & OrderBitFlag.SafeTriggerOrder) !== 0 + ); +} + export function calculateBaseAssetAmountForAmmToFulfill( order: Order, market: PerpMarketAccount, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index fd0cb165cd..e2801414ed 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1107,7 +1107,8 @@ export type AMM = { quoteAssetAmountWithUnsettledLp: BN; referencePriceOffset: number; - takerSpeedBumpOverride: number; + oracleLowRiskSlotDelayOverride: number; + oracleSlotDelayOverride: number; ammSpreadAdjustment: number; ammInventorySpreadAdjustment: number; @@ -1473,6 +1474,17 @@ export type OracleGuardRails = { }; }; +export enum OracleValidity { + NonPositive = 0, + TooVolatile = 1, + TooUncertain = 2, + StaleForMargin = 3, + InsufficientDataPoints = 4, + StaleForAMMLowRisk = 5, + isStaleForAmmImmediate = 6, + Valid = 7, +} + export type PrelaunchOracle = { price: BN; maxPrice: BN; diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index d682f5e757..287c77d725 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -145,7 +145,7 @@ export const mockAMM: AMM = { quoteAssetAmountWithUnsettledLp: new BN(0), referencePriceOffset: 0, - takerSpeedBumpOverride: 0, + oracleLowRiskSlotDelayOverride: 0, ammSpreadAdjustment: 0, ammInventorySpreadAdjustment: 0, mmOracleSequenceId: new BN(0), @@ -672,9 +672,9 @@ export class MockUserMap implements UserMapInterface { }); } - public async subscribe(): Promise {} + public async subscribe(): Promise { } - public async unsubscribe(): Promise {} + public async unsubscribe(): Promise { } public async addPubkey(userAccountPublicKey: PublicKey): Promise { const user = new User({ @@ -733,7 +733,7 @@ export class MockUserMap implements UserMapInterface { ); } - public async updateWithOrderRecord(_record: OrderRecord): Promise {} + public async updateWithOrderRecord(_record: OrderRecord): Promise { } public values(): IterableIterator { return this.userMap.values(); diff --git a/tests/switchboardTxCus.ts b/tests/switchboardTxCus.ts index b3a933eb18..9c46994d99 100644 --- a/tests/switchboardTxCus.ts +++ b/tests/switchboardTxCus.ts @@ -219,6 +219,6 @@ describe('switchboard place orders cus', () => { const cus = bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); console.log(cus); - assert(cus < 413000); + assert(cus < 415000); }); }); From 9b5c0b1cbb61a0f182f20513368caffeedc38ef2 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:32:30 +0000 Subject: [PATCH 177/247] sdk: release v2.143.0-beta.9 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 53b97a37b7..a7e82b2220 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.8 \ No newline at end of file +2.143.0-beta.9 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index daeb56ad3d..008d2c8c6f 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.8", + "version": "2.143.0-beta.9", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From a51c28b7bd60443ad71684e4e1c7600ac091ebf7 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 22 Oct 2025 14:52:46 -0600 Subject: [PATCH 178/247] feat: cancels withdraw from iso pos --- sdk/src/driftClient.ts | 103 ++++++++++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index d39f70dcd9..c633043a51 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -4201,6 +4201,24 @@ export class DriftClient { subAccountId?: number, txParams?: TxParams ): Promise { + const instructions = await this.getWithdrawFromIsolatedPerpPositionIxsBundle( + amount, + perpMarketIndex, + subAccountId, + userTokenAccount + ); + const { txSig } = await this.sendTransaction( + await this.buildTransaction(instructions, txParams) + ); + return txSig; + } + + public async getWithdrawFromIsolatedPerpPositionIxsBundle( + amount: BN, + perpMarketIndex: number, + subAccountId?: number, + userTokenAccount?: PublicKey + ): Promise { const userAccountPublicKey = await getUserAccountPublicKey( this.program.programId, this.authority, @@ -4213,16 +4231,22 @@ export class DriftClient { [perpMarketIndex], SettlePnlMode.TRY_SETTLE ); + let associatedTokenAccount = userTokenAccount; + if (!associatedTokenAccount) { + const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); + const quoteSpotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + associatedTokenAccount = await this.getAssociatedTokenAccount( + quoteSpotMarketIndex + ); + } + const withdrawIx = await this.getWithdrawFromIsolatedPerpPositionIx( amount, perpMarketIndex, - userTokenAccount, + associatedTokenAccount, subAccountId ); - const { txSig } = await this.sendTransaction( - await this.buildTransaction([settleIx, withdrawIx], txParams) - ); - return txSig; + return [settleIx, withdrawIx]; } public async getWithdrawFromIsolatedPerpPositionIx( @@ -4936,21 +4960,42 @@ export class DriftClient { ); } - public async cancelOrder( - orderId?: number, - txParams?: TxParams, - subAccountId?: number - ): Promise { - const { txSig } = await this.sendTransaction( - await this.buildTransaction( - await this.getCancelOrderIx(orderId, subAccountId), - txParams - ), - [], - this.opts - ); - return txSig; - } + public async cancelOrder( + orderId?: number, + txParams?: TxParams, + subAccountId?: number, + overrides?: { withdrawIsolatedDepositForPerpMarket?: number } + ): Promise { + const cancelIx = await this.getCancelOrderIx(orderId, subAccountId); + + const instructions: TransactionInstruction[] = [cancelIx]; + + if (overrides?.withdrawIsolatedDepositForPerpMarket !== undefined) { + const perpMarketIndex = overrides.withdrawIsolatedDepositForPerpMarket; + + const withdrawAmount = this.getIsolatedPerpPositionTokenAmount( + perpMarketIndex, + subAccountId + ); + + if (withdrawAmount.gt(ZERO)) { + const withdrawIxs = + await this.getWithdrawFromIsolatedPerpPositionIxsBundle( + withdrawAmount, + perpMarketIndex, + subAccountId + ); + instructions.push(...withdrawIxs); + } + } + + const { txSig } = await this.sendTransaction( + await this.buildTransaction(instructions, txParams), + [], + this.opts + ); + return txSig; + } public async getCancelOrderIx( orderId?: number, @@ -7345,12 +7390,6 @@ export class DriftClient { precedingIxs: TransactionInstruction[] = [], overrideCustomIxIndex?: number ): Promise { - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [takerInfo.takerUserAccount], - useMarketLastSlotCache: false, - readablePerpMarketIndex: marketIndex, - }); - const isDelegateSigner = takerInfo.signingAuthority.equals( takerInfo.takerUserAccount.delegate ); @@ -7364,6 +7403,14 @@ export class DriftClient { borshBuf, isDelegateSigner ); + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [takerInfo.takerUserAccount], + useMarketLastSlotCache: false, + readablePerpMarketIndex: marketIndex, + writableSpotMarketIndexes: signedMessage.isolatedPositionDeposit?.gt(new BN(0)) ? [QUOTE_SPOT_MARKET_INDEX] : undefined, + }); + if (isUpdateHighLeverageMode(signedMessage.signedMsgOrderParams.bitFlags)) { remainingAccounts.push({ pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), @@ -11356,8 +11403,10 @@ export class DriftClient { const currentBase = perpPosition.baseAssetAmount; if (currentBase.eq(ZERO)) return true; + const orderBaseAmount = isVariant(orderParams.direction, 'long') ? orderParams.baseAssetAmount : orderParams.baseAssetAmount.neg(); + return currentBase - .add(orderParams.baseAssetAmount) + .add(orderBaseAmount) .abs() .gt(currentBase.abs()); } From 043e1817f44cf109f055e15a0cfc7e25a0cd72be Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 22 Oct 2025 20:07:16 -0700 Subject: [PATCH 179/247] v2.143.0 --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c85a0197..7569ea585a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Features + +### Fixes + +### Breaking + +## [2.143.0] - 2025-10-22 + - program: relax filling conditions for low risk orders vs amm ([#1968](https://github.com/drift-labs/protocol-v2/pull/1968)) - sdk: make oracle validity match program and propogate to dlob and math functions ([#1968](https://github.com/drift-labs/protocol-v2/pull/1968)) diff --git a/Cargo.lock b/Cargo.lock index 4d0ca24568..e77492a445 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.142.0" +version = "2.143.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index ba4ab87aeb..0a2f8cb862 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.142.0" +version = "2.143.0" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index 008d2c8c6f..f90c6ea76d 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0-beta.9", + "version": "2.143.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 7f5ca59818..28b245a6ec 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.142.0", + "version": "2.143.0", "name": "drift", "instructions": [ { @@ -16809,4 +16809,4 @@ "msg": "Unable to load builder account" } ] -} \ No newline at end of file +} From fc7a81e6a3ce6c0c0090f7812c62d35d2fd838a0 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 03:15:30 +0000 Subject: [PATCH 180/247] sdk: release v2.144.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index a7e82b2220..7618c787c4 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.143.0-beta.9 \ No newline at end of file +2.144.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index f90c6ea76d..247430f500 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.143.0", + "version": "2.144.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From afbb584a9707df839c59a417b94d11d22bd079be Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:55:50 -0400 Subject: [PATCH 181/247] sdk: add to constants spot-plus-market-index-82 (#1982) --- sdk/src/constants/perpMarkets.ts | 11 +++++++++++ sdk/src/constants/spotMarkets.ts | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 26fe5637dc..43b10a39ec 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1388,6 +1388,17 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ oracleSource: OracleSource.PYTH_LAZER_1K, pythLazerId: 1578, }, + { + fullName: 'Meteroa', + category: ['Solana', 'DEX'], + symbol: 'MET-PERP', + baseAssetSymbol: 'MET', + marketIndex: 82, + oracle: new PublicKey('HN7qfUNM5Q7gQTwyEucmYdCF4CjwUrspj3DbNQ4V8P52'), + launchTs: 1761225524000, + oracleSource: OracleSource.PYTH_LAZER, + pythLazerId: 2382, + }, ]; export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = { diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index a8b5e14e0f..8388699374 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -973,6 +973,20 @@ export const MainnetSpotMarkets: SpotMarketConfig[] = [ pythLazerId: 2316, launchTs: 1759412919000, }, + { + symbol: 'MET', + marketIndex: 60, + poolId: 0, + oracle: new PublicKey('HN7qfUNM5Q7gQTwyEucmYdCF4CjwUrspj3DbNQ4V8P52'), + oracleSource: OracleSource.PYTH_LAZER, + mint: new PublicKey('METvsvVRapdj9cFLzq4Tr43xK4tAjQfwX76z3n6mWQL'), + precision: new BN(10).pow(SIX), + precisionExp: SIX, + pythFeedId: + '0x0292e0f405bcd4a496d34e48307f6787349ad2bcd8505c3d3a9f77d81a67a682', + pythLazerId: 2382, + launchTs: 1761225524000, + }, ]; export const SpotMarkets: { [key in DriftEnv]: SpotMarketConfig[] } = { From 6a1d7a26dcff2e87f255fca7f874782bca300574 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:02:05 +0000 Subject: [PATCH 182/247] sdk: release v2.144.0-beta.1 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 7618c787c4..3dd7c6135e 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.144.0-beta.0 \ No newline at end of file +2.144.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 247430f500..0bdbae9d8f 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.144.0-beta.0", + "version": "2.144.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From e72c529e8a6af5bf51a11db0fef3b6665b87e9a8 Mon Sep 17 00:00:00 2001 From: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:06:58 -0400 Subject: [PATCH 183/247] cap max size fn at max oi (#1983) --- sdk/src/math/orders.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index 6886821be7..653d66ac0b 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -459,7 +459,8 @@ export function calculateOrderBaseAssetAmount( export function maxSizeForTargetLiabilityWeightBN( target: BN, imfFactor: BN, - liabilityWeight: BN + liabilityWeight: BN, + market: PerpMarketAccount ): BN | null { if (target.lt(liabilityWeight)) return null; if (imfFactor.isZero()) return null; @@ -506,5 +507,11 @@ export function maxSizeForTargetLiabilityWeightBN( } } + // cap at max OI + const maxOpenInterest = market.amm.maxOpenInterest; + if (lo.gt(maxOpenInterest)) { + return maxOpenInterest; + } + return lo; } From 935e19523ae40ab705b4eedaf1742157336e2916 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:12:32 +0000 Subject: [PATCH 184/247] sdk: release v2.144.0-beta.2 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 3dd7c6135e..b33a0cb430 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.144.0-beta.1 \ No newline at end of file +2.144.0-beta.2 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 0bdbae9d8f..0b1da8f2b2 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.144.0-beta.1", + "version": "2.144.0-beta.2", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 7f9aec959582c339bb8be3a4511a52aaac55a390 Mon Sep 17 00:00:00 2001 From: jeremyhw Date: Fri, 24 Oct 2025 16:15:34 -0400 Subject: [PATCH 185/247] sdk: add titan client (#1972) --- package.json | 7 +- sdk/src/driftClient.ts | 195 +++++++++++++++-- sdk/src/titan/titanClient.ts | 414 +++++++++++++++++++++++++++++++++++ yarn.lock | 5 + 4 files changed, 596 insertions(+), 25 deletions(-) create mode 100644 sdk/src/titan/titanClient.ts diff --git a/package.json b/package.json index 854fea3020..4e4b68a0b4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@project-serum/common": "0.0.1-beta.3", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "2.21.0", + "@pythnetwork/price-service-client": "1.9.0", "@solana/spl-token": "0.4.13", "@solana/web3.js": "1.73.2", "@types/bn.js": "5.1.6", @@ -24,11 +25,11 @@ "husky": "7.0.4", "prettier": "3.0.1", "typedoc": "0.23.23", - "typescript": "5.4.5", - "@pythnetwork/price-service-client": "1.9.0" + "typescript": "5.4.5" }, "dependencies": { "@ellipsis-labs/phoenix-sdk": "1.4.2", + "@msgpack/msgpack": "^3.1.2", "@pythnetwork/pyth-solana-receiver": "0.8.0", "@switchboard-xyz/common": "3.0.14", "@switchboard-xyz/on-demand": "2.4.1", @@ -95,4 +96,4 @@ "supports-hyperlinks": "<4.1.1", "has-ansi": "<6.0.1" } -} \ No newline at end of file +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 2d1ecb13ae..41a3c3e6c1 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -190,6 +190,7 @@ import { createMinimalEd25519VerifyIx } from './util/ed25519Utils'; import { createNativeInstructionDiscriminatorBuffer, isVersionedTransaction, + MAX_TX_BYTE_SIZE, } from './tx/utils'; import pythSolanaReceiverIdl from './idl/pyth_solana_receiver.json'; import { asV0Tx, PullFeed, AnchorUtils } from '@switchboard-xyz/on-demand'; @@ -208,6 +209,12 @@ import { isBuilderOrderReferral, isBuilderOrderCompleted, } from './math/builder'; +import { TitanClient, SwapMode as TitanSwapMode } from './titan/titanClient'; + +/** + * Union type for swap clients (Titan and Jupiter) + */ +export type SwapClient = TitanClient | JupiterClient; type RemainingAccountParams = { userAccounts: UserAccount[]; @@ -5733,23 +5740,23 @@ export class DriftClient { } /** - * Swap tokens in drift account using jupiter - * @param jupiterClient jupiter client to find routes and jupiter instructions + * Swap tokens in drift account using titan or jupiter + * @param swapClient swap client to find routes and instructions (Titan or Jupiter) * @param outMarketIndex the market index of the token you're buying * @param inMarketIndex the market index of the token you're selling - * @param outAssociatedTokenAccount the token account to receive the token being sold on jupiter + * @param outAssociatedTokenAccount the token account to receive the token being sold on titan or jupiter * @param inAssociatedTokenAccount the token account to * @param amount the amount of TokenIn, regardless of swapMode - * @param slippageBps the max slippage passed to jupiter api - * @param swapMode jupiter swapMode (ExactIn or ExactOut), default is ExactIn - * @param route the jupiter route to use for the swap + * @param slippageBps the max slippage passed to titan or jupiter api + * @param swapMode titan or jupiter swapMode (ExactIn or ExactOut), default is ExactIn + * @param route the titan or jupiter route to use for the swap * @param reduceOnly specify if In or Out token on the drift account must reduceOnly, checked at end of swap * @param v6 pass in the quote response from Jupiter quote's API (deprecated, use quote instead) * @param quote pass in the quote response from Jupiter quote's API * @param txParams */ public async swap({ - jupiterClient, + swapClient, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, @@ -5763,7 +5770,7 @@ export class DriftClient { quote, onlyDirectRoutes = false, }: { - jupiterClient: JupiterClient; + swapClient: SwapClient; outMarketIndex: number; inMarketIndex: number; outAssociatedTokenAccount?: PublicKey; @@ -5779,21 +5786,45 @@ export class DriftClient { }; quote?: QuoteResponse; }): Promise { - const quoteToUse = quote ?? v6?.quote; + let res: { + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }; + + if (swapClient instanceof TitanClient) { + res = await this.getTitanSwapIx({ + titanClient: swapClient, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + reduceOnly, + }); + } else if (swapClient instanceof JupiterClient) { + const quoteToUse = quote ?? v6?.quote; + res = await this.getJupiterSwapIxV6({ + jupiterClient: swapClient, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + quote: quoteToUse, + reduceOnly, + onlyDirectRoutes, + }); + } else { + throw new Error( + 'Invalid swap client type. Must be TitanClient or JupiterClient.' + ); + } - const res = await this.getJupiterSwapIxV6({ - jupiterClient, - outMarketIndex, - inMarketIndex, - outAssociatedTokenAccount, - inAssociatedTokenAccount, - amount, - slippageBps, - swapMode, - quote: quoteToUse, - reduceOnly, - onlyDirectRoutes, - }); const ixs = res.ixs; const lookupTables = res.lookupTables; @@ -5811,6 +5842,126 @@ export class DriftClient { return txSig; } + public async getTitanSwapIx({ + titanClient, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + reduceOnly, + userAccountPublicKey, + }: { + titanClient: TitanClient; + outMarketIndex: number; + inMarketIndex: number; + outAssociatedTokenAccount?: PublicKey; + inAssociatedTokenAccount?: PublicKey; + amount: BN; + slippageBps?: number; + swapMode?: string; + onlyDirectRoutes?: boolean; + reduceOnly?: SwapReduceOnly; + userAccountPublicKey?: PublicKey; + }): Promise<{ + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + const outMarket = this.getSpotMarketAccount(outMarketIndex); + const inMarket = this.getSpotMarketAccount(inMarketIndex); + + const isExactOut = swapMode === 'ExactOut'; + const exactOutBufferedAmountIn = amount.muln(1001).divn(1000); // Add 10bp buffer + + const preInstructions = []; + if (!outAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + outAssociatedTokenAccount = await this.getAssociatedTokenAccount( + outMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + outAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + outAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + outMarket.mint, + tokenProgram + ) + ); + } + } + + if (!inAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(inMarket); + inAssociatedTokenAccount = await this.getAssociatedTokenAccount( + inMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + inAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + inAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + inMarket.mint, + tokenProgram + ) + ); + } + } + + const { beginSwapIx, endSwapIx } = await this.getSwapIx({ + outMarketIndex, + inMarketIndex, + amountIn: isExactOut ? exactOutBufferedAmountIn : amount, + inTokenAccount: inAssociatedTokenAccount, + outTokenAccount: outAssociatedTokenAccount, + reduceOnly, + userAccountPublicKey, + }); + + const { transactionMessage, lookupTables } = await titanClient.getSwap({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + userPublicKey: this.provider.wallet.publicKey, + slippageBps, + swapMode: isExactOut ? TitanSwapMode.ExactOut : TitanSwapMode.ExactIn, + onlyDirectRoutes, + sizeConstraint: MAX_TX_BYTE_SIZE - 375, // buffer for drift instructions + }); + + const titanInstructions = titanClient.getTitanInstructions({ + transactionMessage, + inputMint: inMarket.mint, + outputMint: outMarket.mint, + }); + + const ixs = [ + ...preInstructions, + beginSwapIx, + ...titanInstructions, + endSwapIx, + ]; + + return { ixs, lookupTables }; + } + public async getJupiterSwapIxV6({ jupiterClient, outMarketIndex, diff --git a/sdk/src/titan/titanClient.ts b/sdk/src/titan/titanClient.ts new file mode 100644 index 0000000000..e85251957c --- /dev/null +++ b/sdk/src/titan/titanClient.ts @@ -0,0 +1,414 @@ +import { + Connection, + PublicKey, + TransactionMessage, + AddressLookupTableAccount, + TransactionInstruction, +} from '@solana/web3.js'; +import { BN } from '@coral-xyz/anchor'; +import { decode } from '@msgpack/msgpack'; + +export enum SwapMode { + ExactIn = 'ExactIn', + ExactOut = 'ExactOut', +} + +interface RoutePlanStep { + ammKey: Uint8Array; + label: string; + inputMint: Uint8Array; + outputMint: Uint8Array; + inAmount: number; + outAmount: number; + allocPpb: number; + feeMint?: Uint8Array; + feeAmount?: number; + contextSlot?: number; +} + +interface PlatformFee { + amount: number; + fee_bps: number; +} + +type Pubkey = Uint8Array; + +interface AccountMeta { + p: Pubkey; + s: boolean; + w: boolean; +} + +interface Instruction { + p: Pubkey; + a: AccountMeta[]; + d: Uint8Array; +} + +interface SwapRoute { + inAmount: number; + outAmount: number; + slippageBps: number; + platformFee?: PlatformFee; + steps: RoutePlanStep[]; + instructions: Instruction[]; + addressLookupTables: Pubkey[]; + contextSlot?: number; + timeTaken?: number; + expiresAtMs?: number; + expiresAfterSlot?: number; + computeUnits?: number; + computeUnitsSafe?: number; + transaction?: Uint8Array; + referenceId?: string; +} + +interface SwapQuotes { + id: string; + inputMint: Uint8Array; + outputMint: Uint8Array; + swapMode: SwapMode; + amount: number; + quotes: { [key: string]: SwapRoute }; +} + +export interface QuoteResponse { + inputMint: string; + inAmount: string; + outputMint: string; + outAmount: string; + swapMode: SwapMode; + slippageBps: number; + platformFee?: { amount?: string; feeBps?: number }; + routePlan: Array<{ swapInfo: any; percent: number }>; + contextSlot?: number; + timeTaken?: number; + error?: string; + errorCode?: string; +} + +const TITAN_API_URL = 'https://api.titan.exchange'; + +export class TitanClient { + authToken: string; + url: string; + connection: Connection; + + constructor({ + connection, + authToken, + url, + }: { + connection: Connection; + authToken: string; + url?: string; + }) { + this.connection = connection; + this.authToken = authToken; + this.url = url ?? TITAN_API_URL; + } + + /** + * Get routes for a swap + */ + public async getQuote({ + inputMint, + outputMint, + amount, + userPublicKey, + maxAccounts = 50, // 50 is an estimated amount with buffer + slippageBps, + swapMode, + onlyDirectRoutes, + excludeDexes, + sizeConstraint, + accountsLimitWritable, + }: { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey: PublicKey; + maxAccounts?: number; + slippageBps?: number; + swapMode?: string; + onlyDirectRoutes?: boolean; + excludeDexes?: string[]; + sizeConstraint?: number; + accountsLimitWritable?: number; + }): Promise { + const params = new URLSearchParams({ + inputMint: inputMint.toString(), + outputMint: outputMint.toString(), + amount: amount.toString(), + userPublicKey: userPublicKey.toString(), + ...(slippageBps && { slippageBps: slippageBps.toString() }), + ...(swapMode && { + swapMode: + swapMode === 'ExactOut' ? SwapMode.ExactOut : SwapMode.ExactIn, + }), + ...(onlyDirectRoutes && { + onlyDirectRoutes: onlyDirectRoutes.toString(), + }), + ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }), + ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }), + ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }), + ...(accountsLimitWritable && { + accountsLimitWritable: accountsLimitWritable.toString(), + }), + }); + + const response = await fetch( + `${this.url}/api/v1/quote/swap?${params.toString()}`, + { + headers: { + Accept: 'application/vnd.msgpack', + 'Accept-Encoding': 'gzip, deflate, br', + Authorization: `Bearer ${this.authToken}`, + }, + } + ); + + if (!response.ok) { + throw new Error( + `Titan API error: ${response.status} ${response.statusText}` + ); + } + + const buffer = await response.arrayBuffer(); + const data = decode(buffer) as SwapQuotes; + + const route = + data.quotes[ + Object.keys(data.quotes).find((key) => key.toLowerCase() === 'titan') || + '' + ]; + + if (!route) { + throw new Error('No routes available'); + } + + return { + inputMint: inputMint.toString(), + inAmount: amount.toString(), + outputMint: outputMint.toString(), + outAmount: route.outAmount.toString(), + swapMode: data.swapMode, + slippageBps: route.slippageBps, + platformFee: route.platformFee + ? { + amount: route.platformFee.amount.toString(), + feeBps: route.platformFee.fee_bps, + } + : undefined, + routePlan: + route.steps?.map((step: any) => ({ + swapInfo: { + ammKey: new PublicKey(step.ammKey).toString(), + label: step.label, + inputMint: new PublicKey(step.inputMint).toString(), + outputMint: new PublicKey(step.outputMint).toString(), + inAmount: step.inAmount.toString(), + outAmount: step.outAmount.toString(), + feeAmount: step.feeAmount?.toString() || '0', + feeMint: step.feeMint ? new PublicKey(step.feeMint).toString() : '', + }, + percent: 100, + })) || [], + contextSlot: route.contextSlot, + timeTaken: route.timeTaken, + }; + } + + /** + * Get a swap transaction for quote + */ + public async getSwap({ + inputMint, + outputMint, + amount, + userPublicKey, + maxAccounts = 50, // 50 is an estimated amount with buffer + slippageBps, + swapMode, + onlyDirectRoutes, + excludeDexes, + sizeConstraint, + accountsLimitWritable, + }: { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey: PublicKey; + maxAccounts?: number; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + excludeDexes?: string[]; + sizeConstraint?: number; + accountsLimitWritable?: number; + }): Promise<{ + transactionMessage: TransactionMessage; + lookupTables: AddressLookupTableAccount[]; + }> { + const params = new URLSearchParams({ + inputMint: inputMint.toString(), + outputMint: outputMint.toString(), + amount: amount.toString(), + userPublicKey: userPublicKey.toString(), + ...(slippageBps && { slippageBps: slippageBps.toString() }), + ...(swapMode && { swapMode: swapMode }), + ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }), + ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }), + ...(onlyDirectRoutes && { + onlyDirectRoutes: onlyDirectRoutes.toString(), + }), + ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }), + ...(accountsLimitWritable && { + accountsLimitWritable: accountsLimitWritable.toString(), + }), + }); + + const response = await fetch( + `${this.url}/api/v1/quote/swap?${params.toString()}`, + { + headers: { + Accept: 'application/vnd.msgpack', + 'Accept-Encoding': 'gzip, deflate, br', + Authorization: `Bearer ${this.authToken}`, + }, + } + ); + + if (!response.ok) { + if (response.status === 404) { + throw new Error('No routes available'); + } + throw new Error( + `Titan API error: ${response.status} ${response.statusText}` + ); + } + + const buffer = await response.arrayBuffer(); + const data = decode(buffer) as SwapQuotes; + + const route = + data.quotes[ + Object.keys(data.quotes).find((key) => key.toLowerCase() === 'titan') || + '' + ]; + + if (!route) { + throw new Error('No routes available'); + } + + if (route.instructions && route.instructions.length > 0) { + try { + const { transactionMessage, lookupTables } = + await this.getTransactionMessageAndLookupTables(route, userPublicKey); + return { transactionMessage, lookupTables }; + } catch (err) { + throw new Error( + 'Something went wrong with creating the Titan swap transaction. Please try again.' + ); + } + } + throw new Error('No instructions provided in the route'); + } + + /** + * Get the titan instructions from transaction by filtering out instructions to compute budget and associated token programs + * @param transactionMessage the transaction message + * @param inputMint the input mint + * @param outputMint the output mint + */ + public getTitanInstructions({ + transactionMessage, + inputMint, + outputMint, + }: { + transactionMessage: TransactionMessage; + inputMint: PublicKey; + outputMint: PublicKey; + }): TransactionInstruction[] { + // Filter out common system instructions that can be handled by DriftClient + const filteredInstructions = transactionMessage.instructions.filter( + (instruction) => { + const programId = instruction.programId.toString(); + + // Filter out system programs + if (programId === 'ComputeBudget111111111111111111111111111111') { + return false; + } + + if (programId === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') { + return false; + } + + if (programId === '11111111111111111111111111111111') { + return false; + } + + // Filter out Associated Token Account creation for input/output mints + if (programId === 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') { + if (instruction.keys.length > 3) { + const mint = instruction.keys[3].pubkey; + if (mint.equals(inputMint) || mint.equals(outputMint)) { + return false; + } + } + } + + return true; + } + ); + return filteredInstructions; + } + + private async getTransactionMessageAndLookupTables( + route: SwapRoute, + userPublicKey: PublicKey + ): Promise<{ + transactionMessage: TransactionMessage; + lookupTables: AddressLookupTableAccount[]; + }> { + const solanaInstructions: TransactionInstruction[] = route.instructions.map( + (instruction) => ({ + programId: new PublicKey(instruction.p), + keys: instruction.a.map((meta) => ({ + pubkey: new PublicKey(meta.p), + isSigner: meta.s, + isWritable: meta.w, + })), + data: Buffer.from(instruction.d), + }) + ); + + // Get recent blockhash + const { blockhash } = await this.connection.getLatestBlockhash(); + + // Build address lookup tables if provided + const addressLookupTables: AddressLookupTableAccount[] = []; + if (route.addressLookupTables && route.addressLookupTables.length > 0) { + for (const altPubkey of route.addressLookupTables) { + try { + const altAccount = await this.connection.getAddressLookupTable( + new PublicKey(altPubkey) + ); + if (altAccount.value) { + addressLookupTables.push(altAccount.value); + } + } catch (err) { + console.warn(`Failed to fetch address lookup table:`, err); + } + } + } + + const transactionMessage = new TransactionMessage({ + payerKey: userPublicKey, + recentBlockhash: blockhash, + instructions: solanaInstructions, + }); + + return { transactionMessage, lookupTables: addressLookupTables }; + } +} diff --git a/yarn.lock b/yarn.lock index 2678bc1b59..cafe370cd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -225,6 +225,11 @@ snake-case "^3.0.4" spok "^1.4.3" +"@msgpack/msgpack@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.1.2.tgz#fdd25cc2202297519798bbaf4689152ad9609e19" + integrity sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ== + "@noble/curves@^1.0.0", "@noble/curves@^1.4.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.2.tgz#73388356ce733922396214a933ff7c95afcef911" From 4dfd3f763784fe9f3cc063dcc052e7479482789d Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Fri, 24 Oct 2025 14:16:21 -0600 Subject: [PATCH 186/247] fix: support fetching initial data for delisted markets, just not being subscribed (#1985) --- .../grpcDriftClientAccountSubscriberV2.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index ccea364002..9b64380a43 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -709,21 +709,6 @@ export class grpcDriftClientAccountSubscriberV2 await this.perpMarketsSubscriber.removeAccounts( perpMarketPubkeysToRemove ); - // Clean up the mapping for removed perp markets - for (const pubkey of perpMarketPubkeysToRemove) { - const pubkeyString = pubkey.toBase58(); - for (const [ - marketIndex, - accountPubkey, - ] of this.perpMarketIndexToAccountPubkeyMap.entries()) { - if (accountPubkey === pubkeyString) { - this.perpMarketIndexToAccountPubkeyMap.delete(marketIndex); - this.perpOracleMap.delete(marketIndex); - this.perpOracleStringMap.delete(marketIndex); - break; - } - } - } } // Remove accounts in batches - oracles From c4d4a51e05c653f9edace91c9b4e04b5bc5fc116 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Fri, 24 Oct 2025 14:27:46 -0600 Subject: [PATCH 187/247] fix: only settle if needed iso withdraw + i64 min --- sdk/src/driftClient.ts | 117 ++++++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 41 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index ff3adb4b89..93ad86efa4 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -134,7 +134,11 @@ import { QUOTE_SPOT_MARKET_INDEX, ZERO, } from './constants/numericConstants'; -import { findDirectionToClose, positionIsAvailable } from './math/position'; +import { + calculateClaimablePnl, + findDirectionToClose, + positionIsAvailable, +} from './math/position'; import { getSignedTokenAmount, getTokenAmount } from './math/spotBalance'; import { decodeName, DEFAULT_USER_NAME, encodeName } from './userName'; import { MMOraclePriceData, OraclePriceData } from './oracles/types'; @@ -210,6 +214,7 @@ import { isBuilderOrderReferral, isBuilderOrderCompleted, } from './math/builder'; +import { BigNum } from './factory/bigNum'; type RemainingAccountParams = { userAccounts: UserAccount[]; @@ -4216,12 +4221,13 @@ export class DriftClient { subAccountId?: number, txParams?: TxParams ): Promise { - const instructions = await this.getWithdrawFromIsolatedPerpPositionIxsBundle( - amount, - perpMarketIndex, - subAccountId, - userTokenAccount - ); + const instructions = + await this.getWithdrawFromIsolatedPerpPositionIxsBundle( + amount, + perpMarketIndex, + subAccountId, + userTokenAccount + ); const { txSig } = await this.sendTransaction( await this.buildTransaction(instructions, txParams) ); @@ -4240,12 +4246,28 @@ export class DriftClient { subAccountId ?? this.activeSubAccountId ); const userAccount = this.getUserAccount(subAccountId); - const settleIx = await this.settleMultiplePNLsIx( - userAccountPublicKey, - userAccount, - [perpMarketIndex], - SettlePnlMode.TRY_SETTLE + + const tokenAmountDeposited = + this.getIsolatedPerpPositionTokenAmount(perpMarketIndex); + const isolatedPositionUnrealizedPnl = calculateClaimablePnl( + this.getPerpMarketAccount(perpMarketIndex), + this.getSpotMarketAccount( + this.getPerpMarketAccount(perpMarketIndex).quoteSpotMarketIndex + ), + userAccount.perpPositions.find((p) => p.marketIndex === perpMarketIndex), + this.getOracleDataForSpotMarket( + this.getPerpMarketAccount(perpMarketIndex).quoteSpotMarketIndex + ) ); + + const depositAmountPlusUnrealizedPnl = tokenAmountDeposited.add( + isolatedPositionUnrealizedPnl + ); + + const amountToWithdraw = amount.gt(depositAmountPlusUnrealizedPnl) + ? BigNum.fromPrint('-9223372036854775808').val // min i64 + : amount; + let associatedTokenAccount = userTokenAccount; if (!associatedTokenAccount) { const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); @@ -4256,12 +4278,25 @@ export class DriftClient { } const withdrawIx = await this.getWithdrawFromIsolatedPerpPositionIx( - amount, + amountToWithdraw, perpMarketIndex, associatedTokenAccount, subAccountId ); - return [settleIx, withdrawIx]; + const ixs = [withdrawIx]; + + const needsToSettle = + amount.gt(tokenAmountDeposited) && isolatedPositionUnrealizedPnl.gt(ZERO); + if (needsToSettle) { + const settleIx = await this.settleMultiplePNLsIx( + userAccountPublicKey, + userAccount, + [perpMarketIndex], + SettlePnlMode.TRY_SETTLE + ); + ixs.push(settleIx); + } + return ixs; } public async getWithdrawFromIsolatedPerpPositionIx( @@ -4975,23 +5010,20 @@ export class DriftClient { ); } - public async cancelOrder( - orderId?: number, - txParams?: TxParams, - subAccountId?: number, - overrides?: { withdrawIsolatedDepositForPerpMarket?: number } - ): Promise { + public async cancelOrder( + orderId?: number, + txParams?: TxParams, + subAccountId?: number, + overrides?: { withdrawIsolatedDepositAmount?: BN } + ): Promise { const cancelIx = await this.getCancelOrderIx(orderId, subAccountId); const instructions: TransactionInstruction[] = [cancelIx]; - if (overrides?.withdrawIsolatedDepositForPerpMarket !== undefined) { - const perpMarketIndex = overrides.withdrawIsolatedDepositForPerpMarket; - - const withdrawAmount = this.getIsolatedPerpPositionTokenAmount( - perpMarketIndex, - subAccountId - ); + if (overrides?.withdrawIsolatedDepositAmount !== undefined) { + const order = this.getOrder(orderId, subAccountId); + const perpMarketIndex = order?.marketIndex; + const withdrawAmount = overrides.withdrawIsolatedDepositAmount; if (withdrawAmount.gt(ZERO)) { const withdrawIxs = @@ -5002,15 +5034,15 @@ export class DriftClient { ); instructions.push(...withdrawIxs); } - } + } - const { txSig } = await this.sendTransaction( - await this.buildTransaction(instructions, txParams), - [], - this.opts - ); - return txSig; - } + const { txSig } = await this.sendTransaction( + await this.buildTransaction(instructions, txParams), + [], + this.opts + ); + return txSig; + } public async getCancelOrderIx( orderId?: number, @@ -7423,7 +7455,11 @@ export class DriftClient { userAccounts: [takerInfo.takerUserAccount], useMarketLastSlotCache: false, readablePerpMarketIndex: marketIndex, - writableSpotMarketIndexes: signedMessage.isolatedPositionDeposit?.gt(new BN(0)) ? [QUOTE_SPOT_MARKET_INDEX] : undefined, + writableSpotMarketIndexes: signedMessage.isolatedPositionDeposit?.gt( + new BN(0) + ) + ? [QUOTE_SPOT_MARKET_INDEX] + : undefined, }); if (isUpdateHighLeverageMode(signedMessage.signedMsgOrderParams.bitFlags)) { @@ -11423,11 +11459,10 @@ export class DriftClient { const currentBase = perpPosition.baseAssetAmount; if (currentBase.eq(ZERO)) return true; - const orderBaseAmount = isVariant(orderParams.direction, 'long') ? orderParams.baseAssetAmount : orderParams.baseAssetAmount.neg(); + const orderBaseAmount = isVariant(orderParams.direction, 'long') + ? orderParams.baseAssetAmount + : orderParams.baseAssetAmount.neg(); - return currentBase - .add(orderBaseAmount) - .abs() - .gt(currentBase.abs()); + return currentBase.add(orderBaseAmount).abs().gt(currentBase.abs()); } } From 5c252951f026c470a54e579d084f47cb16841c09 Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:47:42 -0700 Subject: [PATCH 188/247] clean up oracle validity logs (#1984) * clean up oracle validity logs * generalize log level to oracle map --- programs/drift/src/controller/orders.rs | 6 ++++-- programs/drift/src/controller/orders/tests.rs | 1 + programs/drift/src/controller/pnl.rs | 1 + programs/drift/src/controller/repeg.rs | 4 ++-- programs/drift/src/instructions/user.rs | 2 ++ programs/drift/src/math/margin.rs | 15 +++++++++++---- programs/drift/src/math/oracle.rs | 16 ++++++++++++++-- programs/drift/src/state/oracle_map.rs | 11 +++++++++-- 8 files changed, 44 insertions(+), 12 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 67d4643ba6..7514d9ea0d 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -3086,8 +3086,9 @@ pub fn trigger_order( .historical_oracle_data .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, - 0, - 0, + perp_market.amm.oracle_slot_delay_override, + perp_market.amm.oracle_low_risk_slot_delay_override, + None, )?; let is_oracle_valid = @@ -5386,6 +5387,7 @@ pub fn trigger_spot_order( spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; let strict_oracle_price = StrictOraclePrice { current: oracle_price_data.price, diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 5b97c5d14f..5a51991cba 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -2229,6 +2229,7 @@ pub mod fulfill_order_with_maker_order { market.get_max_confidence_interval_multiplier().unwrap(), 0, 0, + None, ) .unwrap(); diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 5a0d44e15e..a5b8370c55 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -136,6 +136,7 @@ pub fn settle_pnl( perp_market.get_max_confidence_interval_multiplier()?, 0, 0, + None, )?; if !is_oracle_valid_for_action(oracle_validity, Some(DriftAction::SettlePnl))? diff --git a/programs/drift/src/controller/repeg.rs b/programs/drift/src/controller/repeg.rs index ade1c2d90c..1e2366af22 100644 --- a/programs/drift/src/controller/repeg.rs +++ b/programs/drift/src/controller/repeg.rs @@ -265,8 +265,8 @@ pub fn update_amm_and_check_validity( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, LogMode::SafeMMOracle, - 0, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )?; validate!( diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 405af3f895..2d7a56e6af 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -42,6 +42,7 @@ use crate::math::margin::{ }; use crate::math::oracle::is_oracle_valid_for_action; use crate::math::oracle::DriftAction; +use crate::math::oracle::LogMode; use crate::math::orders::calculate_existing_position_fields_for_order_action; use crate::math::orders::get_position_delta_for_fill; use crate::math::orders::is_multiple_of_step_size; @@ -1790,6 +1791,7 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( perp_market.get_max_confidence_interval_multiplier()?, perp_market.amm.oracle_slot_delay_override, perp_market.amm.oracle_low_risk_slot_delay_override, + Some(LogMode::Margin), )?; step_size = perp_market.amm.order_step_size; tick_size = perp_market.amm.order_tick_size; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 6af33cf411..341f2d8b81 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -4,6 +4,7 @@ use crate::math::constants::{ MARGIN_PRECISION_U128, MAX_POSITIVE_UPNL_FOR_INITIAL_MARGIN, PERCENTAGE_PRECISION, PRICE_PRECISION, SPOT_IMF_PRECISION_U128, SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_U128, }; +use crate::math::oracle::LogMode; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; use crate::MARGIN_PRECISION; @@ -305,6 +306,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + Some(LogMode::Margin), )?; let mut skip_token_value = false; @@ -558,6 +560,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( quote_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + Some(LogMode::Margin), )?; let strict_quote_price = StrictOraclePrice::new( @@ -575,8 +578,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( &market.oracle_id(), market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier()?, - 0, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + Some(LogMode::Margin), )?; let perp_position_custom_margin_ratio = @@ -993,6 +997,7 @@ pub fn calculate_user_equity( spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + Some(LogMode::Margin), )?; all_oracles_valid &= is_oracle_valid_for_action(oracle_validity, Some(DriftAction::MarginCalc))?; @@ -1024,6 +1029,7 @@ pub fn calculate_user_equity( quote_spot_market.get_max_confidence_interval_multiplier()?, 0, 0, + Some(LogMode::Margin), )?; all_oracles_valid &= @@ -1038,8 +1044,9 @@ pub fn calculate_user_equity( &market.oracle_id(), market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier()?, - 0, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + Some(LogMode::Margin), )?; all_oracles_valid &= diff --git a/programs/drift/src/math/oracle.rs b/programs/drift/src/math/oracle.rs index 1ac2c0e158..f2bb0f20f0 100644 --- a/programs/drift/src/math/oracle.rs +++ b/programs/drift/src/math/oracle.rs @@ -243,6 +243,7 @@ pub enum LogMode { ExchangeOracle, MMOracle, SafeMMOracle, + Margin, } pub fn oracle_validity( @@ -323,7 +324,7 @@ pub fn oracle_validity( }; if log_mode != LogMode::None { - let oracle_type = if log_mode == LogMode::ExchangeOracle { + let oracle_type = if log_mode == LogMode::ExchangeOracle || log_mode == LogMode::Margin { "Exchange" } else if log_mode == LogMode::SafeMMOracle { "SafeMM" @@ -369,7 +370,18 @@ pub fn oracle_validity( ); } - if is_stale_for_amm_immediate || is_stale_for_margin || is_stale_for_amm_low_risk { + if is_stale_for_margin { + crate::msg!( + "Invalid {} {} {} Oracle: Stale for Margin (oracle_delay={:?})", + market_type, + market_index, + oracle_type, + oracle_delay + ); + } + + if (is_stale_for_amm_immediate || is_stale_for_amm_low_risk) && log_mode != LogMode::Margin + { crate::msg!( "Invalid {} {} {} Oracle: Stale (oracle_delay={:?}), (stale_for_amm_immediate={}, stale_for_amm_low_risk={}, stale_for_margin={})", market_type, diff --git a/programs/drift/src/state/oracle_map.rs b/programs/drift/src/state/oracle_map.rs index 47c0daba1d..acbe7c618c 100644 --- a/programs/drift/src/state/oracle_map.rs +++ b/programs/drift/src/state/oracle_map.rs @@ -90,11 +90,18 @@ impl<'a> OracleMap<'a> { max_confidence_interval_multiplier: u64, slots_before_stale_for_amm_override: i8, oracle_low_risk_slot_delay_override_override: i8, + log_mode: Option, ) -> DriftResult<(&OraclePriceData, OracleValidity)> { if self.should_get_quote_asset_price_data(&oracle_id.0) { return Ok((&self.quote_asset_price_data, OracleValidity::Valid)); } + let log_mode = if let Some(lm) = log_mode { + lm + } else { + LogMode::ExchangeOracle + }; + if self.price_data.contains_key(oracle_id) { let oracle_price_data = self.price_data.get(oracle_id).safe_unwrap()?; @@ -109,7 +116,7 @@ impl<'a> OracleMap<'a> { &self.oracle_guard_rails.validity, max_confidence_interval_multiplier, &oracle_id.1, - LogMode::ExchangeOracle, + log_mode, slots_before_stale_for_amm_override, oracle_low_risk_slot_delay_override_override, )?; @@ -140,7 +147,7 @@ impl<'a> OracleMap<'a> { &self.oracle_guard_rails.validity, max_confidence_interval_multiplier, &oracle_id.1, - LogMode::ExchangeOracle, + log_mode, slots_before_stale_for_amm_override, oracle_low_risk_slot_delay_override_override, )?; From 158f559c70e187bcfb1abff02507783aeed982b5 Mon Sep 17 00:00:00 2001 From: cha-kos Date: Fri, 24 Oct 2025 18:52:02 -0400 Subject: [PATCH 189/247] Fix build --- sdk/package.json | 1 + sdk/yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/sdk/package.json b/sdk/package.json index 0b1da8f2b2..3ff63a5861 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -44,6 +44,7 @@ "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@ellipsis-labs/phoenix-sdk": "1.4.5", "@grpc/grpc-js": "1.14.0", + "@msgpack/msgpack": "^3.1.2", "@openbook-dex/openbook-v2": "0.2.10", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "2.5.3", diff --git a/sdk/yarn.lock b/sdk/yarn.lock index af07309b37..85552471dc 100644 --- a/sdk/yarn.lock +++ b/sdk/yarn.lock @@ -328,6 +328,11 @@ snake-case "^3.0.4" spok "^1.4.3" +"@msgpack/msgpack@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.1.2.tgz#fdd25cc2202297519798bbaf4689152ad9609e19" + integrity sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ== + "@noble/curves@^1.4.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.2.tgz#73388356ce733922396214a933ff7c95afcef911" From 9f515db5c500eb8b7208e815f077ac5464888c84 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 23:18:10 +0000 Subject: [PATCH 190/247] sdk: release v2.144.0-beta.3 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index b33a0cb430..b390cac1ca 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.144.0-beta.2 \ No newline at end of file +2.144.0-beta.3 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 3ff63a5861..789bc7665f 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.144.0-beta.2", + "version": "2.144.0-beta.3", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From c8f98418e63ca158be9ea1a96678ad3e62464198 Mon Sep 17 00:00:00 2001 From: DecentralizedDev <181214587+DecentralizedDev@users.noreply.github.com> Date: Sun, 26 Oct 2025 01:19:48 +0200 Subject: [PATCH 191/247] Keep oracle data of delisted markets. (#1988) --- .../accounts/grpcDriftClientAccountSubscriberV2.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts index 9b64380a43..d2770dc3c7 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -714,17 +714,6 @@ export class grpcDriftClientAccountSubscriberV2 // Remove accounts in batches - oracles if (oraclePubkeysToRemove.length > 0) { await this.oracleMultiSubscriber.removeAccounts(oraclePubkeysToRemove); - // Clean up oracle data for removed oracles by finding their sources - for (const pubkey of oraclePubkeysToRemove) { - // Find the oracle source by checking oracleInfos - const oracleInfo = this.oracleInfos.find((info) => - info.publicKey.equals(pubkey) - ); - if (oracleInfo) { - const oracleId = getOracleId(pubkey, oracleInfo.source); - this.oracleIdToOracleDataMap.delete(oracleId); - } - } } } From 96a38aa572da1cc5f1bf69325bb0fbffd94dac61 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:24:38 +0000 Subject: [PATCH 192/247] sdk: release v2.144.0-beta.4 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index b390cac1ca..20f15176f8 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.144.0-beta.3 \ No newline at end of file +2.144.0-beta.4 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 789bc7665f..d325fb44d7 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.144.0-beta.3", + "version": "2.144.0-beta.4", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From d368e2ac77ae8840c814ef3a561421735cd93be5 Mon Sep 17 00:00:00 2001 From: cha-kos Date: Mon, 27 Oct 2025 08:55:12 -0400 Subject: [PATCH 193/247] Fix build --- bun.lock | 34 +++++++++++++++++++++------------- package.json | 1 - sdk/bun.lock | 3 +++ sdk/src/index.ts | 1 + 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/bun.lock b/bun.lock index 7fa155ea36..865ade9e6e 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ "nanoid": "3.3.4", "rpc-websockets": "7.5.1", "solana-bankrun": "0.3.0", - "zod": "^4.0.17", + "zod": "4.0.17", "zstddec": "0.1.0", }, "devDependencies": { @@ -43,6 +43,26 @@ }, }, }, + "overrides": { + "ansi-regex": "5.0.1", + "ansi-styles": "4.3.0", + "backslash": "<0.2.1", + "chalk": "4.1.2", + "chalk-template": "<1.1.1", + "color-convert": "<3.1.1", + "color-name": "<2.0.1", + "color-string": "<2.1.1", + "debug": "<4.4.2", + "error-ex": "<1.3.3", + "has-ansi": "<6.0.1", + "is-arrayish": "<0.3.3", + "simple-swizzle": "<0.2.3", + "slice-ansi": "3.0.0", + "strip-ansi": "6.0.1", + "supports-color": "7.2.0", + "supports-hyperlinks": "<4.1.1", + "wrap-ansi": "7.0.0", + }, "packages": { "@babel/runtime": ["@babel/runtime@7.28.3", "", {}, "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA=="], @@ -806,8 +826,6 @@ "@solana/codecs-strings/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], - "@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/errors/commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], "@solana/options/@solana/codecs-numbers": ["@solana/codecs-numbers@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ=="], @@ -900,22 +918,14 @@ "@solana/buffer-layout-utils/@solana/web3.js/superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], - "@solana/codecs-core/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/codecs-core/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@solana/codecs-data-structures/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/codecs-data-structures/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@solana/codecs-strings/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/codecs-strings/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "@solana/codecs/@solana/codecs-numbers/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], - "@solana/options/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/options/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "@switchboard-xyz/common/@solana/web3.js/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], @@ -1018,8 +1028,6 @@ "@solana/buffer-layout-utils/@solana/web3.js/rpc-websockets/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "@solana/codecs/@solana/codecs-numbers/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/codecs/@solana/codecs-numbers/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "@switchboard-xyz/common/@solana/web3.js/jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], diff --git a/package.json b/package.json index 4e4b68a0b4..4b30b26fde 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ }, "dependencies": { "@ellipsis-labs/phoenix-sdk": "1.4.2", - "@msgpack/msgpack": "^3.1.2", "@pythnetwork/pyth-solana-receiver": "0.8.0", "@switchboard-xyz/common": "3.0.14", "@switchboard-xyz/on-demand": "2.4.1", diff --git a/sdk/bun.lock b/sdk/bun.lock index 77e0416d67..84ef9f3886 100644 --- a/sdk/bun.lock +++ b/sdk/bun.lock @@ -8,6 +8,7 @@ "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@ellipsis-labs/phoenix-sdk": "1.4.5", "@grpc/grpc-js": "1.14.0", + "@msgpack/msgpack": "^3.1.2", "@openbook-dex/openbook-v2": "0.2.10", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "2.5.3", @@ -147,6 +148,8 @@ "@metaplex-foundation/solita": ["@metaplex-foundation/solita@0.12.2", "", { "dependencies": { "@metaplex-foundation/beet": "^0.4.0", "@metaplex-foundation/beet-solana": "^0.3.0", "@metaplex-foundation/rustbin": "^0.3.0", "@solana/web3.js": "^1.36.0", "camelcase": "^6.2.1", "debug": "^4.3.3", "js-sha256": "^0.9.0", "prettier": "^2.5.1", "snake-case": "^3.0.4", "spok": "^1.4.3" }, "bin": { "solita": "dist/src/cli/solita.js" } }, "sha512-oczMfE43NNHWweSqhXPTkQBUbap/aAiwjDQw8zLKNnd/J8sXr/0+rKcN5yJIEgcHeKRkp90eTqkmt2WepQc8yw=="], + "@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="], + "@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], "@noble/ed25519": ["@noble/ed25519@1.7.3", "", {}, "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ=="], diff --git a/sdk/src/index.ts b/sdk/src/index.ts index ea511618ab..5458898cfd 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -53,6 +53,7 @@ export * from './events/webSocketLogProvider'; export * from './events/parse'; export * from './events/pollingLogProvider'; export * from './jupiter/jupiterClient'; +export { TitanClient } from './titan/titanClient'; export * from './math/auction'; export * from './math/builder'; export * from './math/spotMarket'; From 5e1fedf0face0245dc88c666b2387003daa33b5b Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:28:23 +0000 Subject: [PATCH 194/247] sdk: release v2.144.0-beta.5 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 20f15176f8..b9f8b2d147 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.144.0-beta.4 \ No newline at end of file +2.144.0-beta.5 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index d325fb44d7..674060a357 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.144.0-beta.4", + "version": "2.144.0-beta.5", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 6d1b6b6ead79f15180c0f4bf4a97d56662e2b157 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 27 Oct 2025 09:44:51 -0600 Subject: [PATCH 195/247] feat: improvements to single grpc test --- sdk/package.json | 1 + sdk/scripts/single-grpc-client-test.ts | 92 ++++++++++++++++++++------ 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/sdk/package.json b/sdk/package.json index 008d2c8c6f..f5512a621b 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -44,6 +44,7 @@ "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@ellipsis-labs/phoenix-sdk": "1.4.5", "@grpc/grpc-js": "1.14.0", + "@msgpack/msgpack": "^3.1.2", "@openbook-dex/openbook-v2": "0.2.10", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "2.5.3", diff --git a/sdk/scripts/single-grpc-client-test.ts b/sdk/scripts/single-grpc-client-test.ts index 0aca8985a0..a42829d4cf 100644 --- a/sdk/scripts/single-grpc-client-test.ts +++ b/sdk/scripts/single-grpc-client-test.ts @@ -47,9 +47,7 @@ async function initializeSingleGrpcClient() { const allPerpMarketProgramAccounts = (await program.account.perpMarket.all()) as ProgramAccount[]; const perpMarketProgramAccounts = allPerpMarketProgramAccounts.filter((val) => - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].includes( - val.account.marketIndex - ) + [46].includes(val.account.marketIndex) ); const perpMarketIndexes = perpMarketProgramAccounts.map( (val) => val.account.marketIndex @@ -59,7 +57,7 @@ async function initializeSingleGrpcClient() { const allSpotMarketProgramAccounts = (await program.account.spotMarket.all()) as ProgramAccount[]; const spotMarketProgramAccounts = allSpotMarketProgramAccounts.filter((val) => - [0, 1, 2, 3, 4, 5].includes(val.account.marketIndex) + [0].includes(val.account.marketIndex) ); const spotMarketIndexes = spotMarketProgramAccounts.map( (val) => val.account.marketIndex @@ -93,7 +91,9 @@ async function initializeSingleGrpcClient() { } } - console.log(`📊 Markets: ${perpMarketIndexes.length} perp, ${spotMarketIndexes.length} spot`); + console.log( + `📊 Markets: ${perpMarketIndexes.length} perp, ${spotMarketIndexes.length} spot` + ); console.log(`🔮 Oracles: ${oracleInfos.length}`); const baseAccountSubscription = { @@ -161,7 +161,9 @@ async function initializeSingleGrpcClient() { await client.subscribe(); console.log('✅ Client subscribed successfully!'); - console.log('🚀 Starting high-load testing (50 reads/sec per perp market)...'); + console.log( + '🚀 Starting high-load testing (50 reads/sec per perp market)...' + ); // High-frequency load testing - 50 reads per second per perp market const loadTestInterval = setInterval(async () => { @@ -169,29 +171,71 @@ async function initializeSingleGrpcClient() { // Test getPerpMarketAccount for each perp market (50 times per second per market) for (const marketIndex of perpMarketIndexes) { const perpMarketAccount = client.getPerpMarketAccount(marketIndex); - console.log("perpMarketAccount name: ", decodeName(perpMarketAccount.name)); - console.log("perpMarketAccount data: ", JSON.stringify({ - marketIndex: perpMarketAccount.marketIndex, - name: decodeName(perpMarketAccount.name), - baseAssetReserve: perpMarketAccount.amm.baseAssetReserve.toString(), - quoteAssetReserve: perpMarketAccount.amm.quoteAssetReserve.toString() - })); + if (!perpMarketAccount) { + console.log(`Perp market ${marketIndex} not found`); + continue; + } + console.log( + 'perpMarketAccount name: ', + decodeName(perpMarketAccount.name) + ); + console.log( + 'perpMarketAccount data: ', + JSON.stringify({ + marketIndex: perpMarketAccount.marketIndex, + name: decodeName(perpMarketAccount.name), + baseAssetReserve: perpMarketAccount.amm.baseAssetReserve.toString(), + quoteAssetReserve: + perpMarketAccount.amm.quoteAssetReserve.toString(), + }) + ); } // Test getMMOracleDataForPerpMarket for each perp market (50 times per second per market) for (const marketIndex of perpMarketIndexes) { try { const oracleData = client.getMMOracleDataForPerpMarket(marketIndex); - console.log("oracleData price: ", oracleData.price.toString()); - console.log("oracleData: ", JSON.stringify({ - price: oracleData.price.toString(), - confidence: oracleData.confidence?.toString(), - slot: oracleData.slot?.toString() - })); + console.log('oracleData price: ', oracleData.price.toString()); + console.log( + 'oracleData: ', + JSON.stringify({ + price: oracleData.price.toString(), + confidence: oracleData.confidence?.toString(), + slot: oracleData.slot?.toString(), + }) + ); } catch (error) { // Ignore errors for load testing } } + + for (const marketIndex of perpMarketIndexes) { + try { + const { data, slot } = + client.accountSubscriber.getMarketAccountAndSlot(marketIndex); + if (!data) { + console.log( + `Perp market getMarketAccountAndSlot ${marketIndex} not found` + ); + continue; + } + console.log( + 'marketAccountAndSlot: ', + JSON.stringify({ + marketIndex: data.marketIndex, + name: decodeName(data.name), + slot: slot?.toString(), + baseAssetReserve: data.amm.baseAssetReserve.toString(), + quoteAssetReserve: data.amm.quoteAssetReserve.toString(), + }) + ); + } catch (error) { + console.error( + `Error getting market account and slot for market ${marketIndex}:`, + error + ); + } + } } catch (error) { console.error('Load test error:', error); } @@ -201,8 +245,14 @@ async function initializeSingleGrpcClient() { const statsInterval = setInterval(() => { console.log('\n📈 Event Counts:', eventCounts); console.log(`⏱️ Client subscribed: ${client.isSubscribed}`); - console.log(`🔗 Account subscriber subscribed: ${client.accountSubscriber.isSubscribed}`); - console.log(`🔥 Load: ${perpMarketIndexes.length * 50 * 2} reads/sec (${perpMarketIndexes.length} markets × 50 getPerpMarketAccount + 50 getMMOracleDataForPerpMarket)`); + console.log( + `🔗 Account subscriber subscribed: ${client.accountSubscriber.isSubscribed}` + ); + console.log( + `🔥 Load: ${perpMarketIndexes.length * 50 * 2} reads/sec (${ + perpMarketIndexes.length + } markets × 50 getPerpMarketAccount + 50 getMMOracleDataForPerpMarket)` + ); }, 5000); // Handle shutdown signals - just exit without cleanup since they never unsubscribe From f00c4c1c0d3af8de792afd6b3f3de75f9640c01f Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 27 Oct 2025 10:37:46 -0600 Subject: [PATCH 196/247] feat: buffer on margin deposits to avoid insuff collat err --- = | 0 sdk/src/driftClient.ts | 12 +++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 = diff --git a/= b/= new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 93ad86efa4..c7c7961cd0 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -4179,7 +4179,8 @@ export class DriftClient { public async getTransferIsolatedPerpPositionDepositIx( amount: BN, perpMarketIndex: number, - subAccountId?: number + subAccountId?: number, + noAmountBuffer?: boolean ): Promise { const userAccountPublicKey = await getUserAccountPublicKey( this.program.programId, @@ -4197,10 +4198,14 @@ export class DriftClient { readablePerpMarketIndex: [perpMarketIndex], }); + const amountWithBuffer = noAmountBuffer + ? amount + : amount.add(amount.div(new BN(1000))); // .1% buffer + return await this.program.instruction.transferIsolatedPerpPositionDeposit( spotMarketIndex, perpMarketIndex, - amount, + amountWithBuffer, { accounts: { state: await this.getStatePublicKey(), @@ -4238,7 +4243,8 @@ export class DriftClient { amount: BN, perpMarketIndex: number, subAccountId?: number, - userTokenAccount?: PublicKey + userTokenAccount?: PublicKey, + dontSettle?: boolean ): Promise { const userAccountPublicKey = await getUserAccountPublicKey( this.program.programId, From 3aee1642eb570c0ff6138ac54ac5eae77a06576c Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 27 Oct 2025 10:38:35 -0600 Subject: [PATCH 197/247] feat: helpful scripts for testing/manipulating iso positions --- sdk/scripts/deposit-isolated-positions.ts | 110 +++++++++++++ sdk/scripts/withdraw-isolated-positions.ts | 174 +++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 sdk/scripts/deposit-isolated-positions.ts create mode 100644 sdk/scripts/withdraw-isolated-positions.ts diff --git a/sdk/scripts/deposit-isolated-positions.ts b/sdk/scripts/deposit-isolated-positions.ts new file mode 100644 index 0000000000..6bff228d26 --- /dev/null +++ b/sdk/scripts/deposit-isolated-positions.ts @@ -0,0 +1,110 @@ +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import dotenv from 'dotenv'; +import { AnchorProvider, Idl, Program, ProgramAccount, BN } from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; +import { + DRIFT_PROGRAM_ID, + PerpMarketAccount, + SpotMarketAccount, + OracleInfo, + Wallet, + numberToSafeBN, +} from '../src'; +import { DriftClient } from '../src/driftClient'; +import { DriftClientConfig } from '../src/driftClientConfig'; + +async function main() { + dotenv.config({ path: '../' }); + + const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + if (!RPC_ENDPOINT) throw new Error('RPC_ENDPOINT env var required'); + + let keypair: Keypair; + const pk = process.env.PRIVATE_KEY; + if (pk) { + const secret = Uint8Array.from(JSON.parse(pk)); + keypair = Keypair.fromSecretKey(secret); + } else { + keypair = new Keypair(); + console.warn('Using ephemeral keypair. Provide PRIVATE_KEY to use a real wallet.'); + } + const wallet = new Wallet(keypair); + + const connection = new Connection(RPC_ENDPOINT); + const provider = new AnchorProvider(connection, wallet as any, { + commitment: 'processed', + }); + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const program = new Program(driftIDL as Idl, programId, provider); + + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketIndexes = allPerpMarketProgramAccounts.map((val) => val.account.marketIndex); + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketIndexes = allSpotMarketProgramAccounts.map((val) => val.account.marketIndex); + + const seen = new Set(); + const oracleInfos: OracleInfo[] = []; + for (const acct of allPerpMarketProgramAccounts) { + const key = `${acct.account.amm.oracle.toBase58()}-${Object.keys(acct.account.amm.oracleSource)[0]}`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ publicKey: acct.account.amm.oracle, source: acct.account.amm.oracleSource }); + } + } + for (const acct of allSpotMarketProgramAccounts) { + const key = `${acct.account.oracle.toBase58()}-${Object.keys(acct.account.oracleSource)[0]}`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ publicKey: acct.account.oracle, source: acct.account.oracleSource }); + } + } + + const clientConfig: DriftClientConfig = { + connection, + wallet, + programID: programId, + accountSubscription: { type: 'websocket', commitment: 'processed' }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + env: 'devnet', + }; + const client = new DriftClient(clientConfig); + await client.subscribe(); + + const candidates = perpMarketIndexes.filter((i) => i >= 0 && i <= 5); + const targetMarketIndex = candidates.length + ? candidates[Math.floor(Math.random() * candidates.length)] + : perpMarketIndexes[0]; + + const perpMarketAccount = client.getPerpMarketAccount(targetMarketIndex); + const quoteSpotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + const spotMarketAccount = client.getSpotMarketAccount(quoteSpotMarketIndex); + + const precision = new BN(10).pow(new BN(spotMarketAccount.decimals)); + const amount = numberToSafeBN(0.01, precision); + + const userTokenAccount = await client.getAssociatedTokenAccount(quoteSpotMarketIndex); + const ix = await client.getDepositIntoIsolatedPerpPositionIx( + amount, + targetMarketIndex, + userTokenAccount, + 0 + ); + + const tx = await client.buildTransaction([ix]); + const { txSig } = await client.sendTransaction(tx); + console.log(`Deposited into isolated perp market ${targetMarketIndex}: ${txSig}`); + + await client.getUser().unsubscribe(); + await client.unsubscribe(); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); + + diff --git a/sdk/scripts/withdraw-isolated-positions.ts b/sdk/scripts/withdraw-isolated-positions.ts new file mode 100644 index 0000000000..51f9a74c34 --- /dev/null +++ b/sdk/scripts/withdraw-isolated-positions.ts @@ -0,0 +1,174 @@ +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import dotenv from 'dotenv'; +import { + AnchorProvider, + Idl, + Program, + ProgramAccount, +} from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; +import { + DRIFT_PROGRAM_ID, + PerpMarketAccount, + SpotMarketAccount, + OracleInfo, + Wallet, + ZERO, +} from '../src'; +import { DriftClient } from '../src/driftClient'; +import { DriftClientConfig } from '../src/driftClientConfig'; + +function isStatusOpen(status: any) { + return !!status && 'open' in status; +} + +function isPerpMarketType(marketType: any) { + return !!marketType && 'perp' in marketType; +} + +async function main() { + dotenv.config({ path: '../' }); + + const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + if (!RPC_ENDPOINT) throw new Error('RPC_ENDPOINT env var required'); + + // Load wallet + // For safety this creates a new ephemeral wallet unless PRIVATE_KEY is provided (base58 array) + let keypair: Keypair; + const pk = process.env.PRIVATE_KEY; + if (pk) { + const secret = Uint8Array.from(JSON.parse(pk)); + keypair = Keypair.fromSecretKey(secret); + } else { + keypair = new Keypair(); + console.warn( + 'Using ephemeral keypair. Provide PRIVATE_KEY for real withdrawals.' + ); + } + const wallet = new Wallet(keypair); + + // Connection and program for market discovery + const connection = new Connection(RPC_ENDPOINT); + const provider = new AnchorProvider(connection, wallet as any, { + commitment: 'processed', + }); + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const program = new Program(driftIDL as Idl, programId, provider); + + // Discover markets and oracles (like the example test script) + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketIndexes = allPerpMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketIndexes = allSpotMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + const seen = new Set(); + const oracleInfos: OracleInfo[] = []; + for (const acct of allPerpMarketProgramAccounts) { + const key = `${acct.account.amm.oracle.toBase58()}-${ + Object.keys(acct.account.amm.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.amm.oracle, + source: acct.account.amm.oracleSource, + }); + } + } + for (const acct of allSpotMarketProgramAccounts) { + const key = `${acct.account.oracle.toBase58()}-${ + Object.keys(acct.account.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.oracle, + source: acct.account.oracleSource, + }); + } + } + + // Build DriftClient with websocket subscription (lightweight) + const clientConfig: DriftClientConfig = { + connection, + wallet, + programID: programId, + accountSubscription: { + type: 'websocket', + commitment: 'processed', + }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + env: 'devnet', + }; + const client = new DriftClient(clientConfig); + await client.subscribe(); + + // Ensure user exists and is subscribed + const user = client.getUser(); + await user.subscribe(); + + const userAccount = user.getUserAccount(); + const openOrders = user.getOpenOrders(); + + const marketsWithOpenOrders = new Set(); + for (const o of openOrders ?? []) { + if (isStatusOpen(o.status) && isPerpMarketType(o.marketType)) { + marketsWithOpenOrders.add(o.marketIndex); + } + } + + const withdrawTargets = userAccount.perpPositions.filter((pos) => { + const isZeroBase = pos.baseAssetAmount.eq(ZERO); + const hasIso = pos.isolatedPositionScaledBalance.gt(ZERO); + const hasOpenOrders = marketsWithOpenOrders.has(pos.marketIndex); + return isZeroBase && hasIso && !hasOpenOrders; + }); + + console.log( + `Found ${withdrawTargets.length} isolated perp positions to withdraw` + ); + + for (const pos of withdrawTargets) { + try { + const amount = client.getIsolatedPerpPositionTokenAmount(pos.marketIndex); + if (amount.lte(ZERO)) continue; + + const perpMarketAccount = client.getPerpMarketAccount(pos.marketIndex); + const quoteAta = await client.getAssociatedTokenAccount( + perpMarketAccount.quoteSpotMarketIndex + ); + + const ixs = await client.getWithdrawFromIsolatedPerpPositionIxsBundle( + amount, + pos.marketIndex, + 0, + quoteAta, + true + ); + + const tx = await client.buildTransaction(ixs); + const { txSig } = await client.sendTransaction(tx); + console.log( + `Withdrew isolated deposit for perp market ${pos.marketIndex}: ${txSig}` + ); + } catch (e) { + console.error(`Failed to withdraw for market ${pos.marketIndex}:`, e); + } + } + + await user.unsubscribe(); + await client.unsubscribe(); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); From a45758be8cadd2bdd430b3377cca75a4fccd91e2 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Mon, 27 Oct 2025 13:38:20 -0600 Subject: [PATCH 198/247] Multi User GRPC (#1970) * feat: multi user grpc w/ debug script * fix: user data not set multi user grpc on fetch * fix: promise in constructor grpc multi user acct sub * fix: lint and formatting --- .../grpc-multiuser-client-test-comparison.ts | 156 ++++++++++ sdk/scripts/single-grpc-client-test.ts | 30 +- .../accounts/grpcMultiAccountSubscriber.ts | 1 + .../grpcMultiUserAccountSubscriber.ts | 274 ++++++++++++++++++ sdk/src/driftClient.ts | 2 + sdk/src/driftClientConfig.ts | 2 + sdk/src/user.ts | 25 +- sdk/src/userConfig.ts | 2 + 8 files changed, 473 insertions(+), 19 deletions(-) create mode 100644 sdk/scripts/grpc-multiuser-client-test-comparison.ts create mode 100644 sdk/src/accounts/grpcMultiUserAccountSubscriber.ts diff --git a/sdk/scripts/grpc-multiuser-client-test-comparison.ts b/sdk/scripts/grpc-multiuser-client-test-comparison.ts new file mode 100644 index 0000000000..7c1ec78c0b --- /dev/null +++ b/sdk/scripts/grpc-multiuser-client-test-comparison.ts @@ -0,0 +1,156 @@ +import { grpcUserAccountSubscriber } from '../src/accounts/grpcUserAccountSubscriber'; +import { grpcMultiUserAccountSubscriber } from '../src/accounts/grpcMultiUserAccountSubscriber'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { DRIFT_PROGRAM_ID } from '../src'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; +import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; +import assert from 'assert'; +import { Wallet } from '../src'; + +const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; +const TOKEN = process.env.TOKEN; +const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + +const USER_ACCOUNT_PUBKEYS = [ + // Add user account public keys here, e.g.: + // new PublicKey('...') +]; + +async function testGrpcUserAccountSubscriberV1VsV2() { + console.log('🚀 Initializing User Account Subscriber V1 vs V2 Test...'); + + if (USER_ACCOUNT_PUBKEYS.length === 0) { + console.error('❌ No user account public keys provided. Please add some to USER_ACCOUNT_PUBKEYS array.'); + process.exit(1); + } + + const connection = new Connection(RPC_ENDPOINT); + const wallet = new Wallet(new Keypair()); + + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const provider = new AnchorProvider( + connection, + // @ts-ignore + wallet, + { + commitment: 'processed', + } + ); + + const program = new Program(driftIDL as Idl, programId, provider); + + const grpcConfigs = { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: CommitmentLevel.PROCESSED, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }; + + console.log(`📊 Testing ${USER_ACCOUNT_PUBKEYS.length} user accounts...`); + + // V1: Create individual subscribers for each user account + const v1Subscribers = USER_ACCOUNT_PUBKEYS.map( + (pubkey) => + new grpcUserAccountSubscriber( + grpcConfigs, + program, + pubkey, + { logResubMessages: true } + ) + ); + + // V2: Create a single multi-subscriber and get per-user interfaces + const v2MultiSubscriber = new grpcMultiUserAccountSubscriber( + program, + grpcConfigs, + { logResubMessages: true } + ); + const v2Subscribers = USER_ACCOUNT_PUBKEYS.map((pubkey) => + v2MultiSubscriber.forUser(pubkey) + ); + + // Subscribe all V1 subscribers + console.log('🔗 Subscribing V1 subscribers...'); + await Promise.all(v1Subscribers.map((sub) => sub.subscribe())); + console.log('✅ V1 subscribers ready'); + + // Subscribe all V2 subscribers + console.log('🔗 Subscribing V2 subscribers...'); + await v2MultiSubscriber.subscribe(); + console.log('✅ V2 subscribers ready'); + + const compare = () => { + try { + let passedTests = 0; + let totalTests = 0; + + // Test each user account + for (let i = 0; i < USER_ACCOUNT_PUBKEYS.length; i++) { + const pubkey = USER_ACCOUNT_PUBKEYS[i]; + const v1Sub = v1Subscribers[i]; + const v2Sub = v2Subscribers[i]; + + totalTests++; + + // 1. Test isSubscribed + assert.strictEqual( + v1Sub.isSubscribed, + v2Sub.isSubscribed, + `User ${pubkey.toBase58()}: isSubscribed should match` + ); + + // 2. Test getUserAccountAndSlot + const v1Data = v1Sub.getUserAccountAndSlot(); + const v2Data = v2Sub.getUserAccountAndSlot(); + + // Compare the user account data + assert.deepStrictEqual( + v1Data.data, + v2Data.data, + `User ${pubkey.toBase58()}: account data should match` + ); + + // Slots might differ slightly due to timing, but let's check if they're close + const slotDiff = Math.abs(v1Data.slot - v2Data.slot); + if (slotDiff > 10) { + console.warn( + `⚠️ User ${pubkey.toBase58()}: slot difference is ${slotDiff} (v1: ${v1Data.slot}, v2: ${v2Data.slot})` + ); + } + + passedTests++; + } + + console.log(`✅ All comparisons passed (${passedTests}/${totalTests} user accounts)`); + } catch (error) { + console.error('❌ Comparison failed:', error); + } + }; + + // Run initial comparison + compare(); + + // Run comparison every second to verify live updates + const interval = setInterval(compare, 1000); + + const cleanup = async () => { + clearInterval(interval); + console.log('🧹 Cleaning up...'); + await Promise.all([ + ...v1Subscribers.map((sub) => sub.unsubscribe()), + ...v2Subscribers.map((sub) => sub.unsubscribe()), + ]); + console.log('✅ Cleanup complete'); + process.exit(0); + }; + + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); + } + + testGrpcUserAccountSubscriberV1VsV2().catch(console.error); \ No newline at end of file diff --git a/sdk/scripts/single-grpc-client-test.ts b/sdk/scripts/single-grpc-client-test.ts index 0aca8985a0..e6724e6615 100644 --- a/sdk/scripts/single-grpc-client-test.ts +++ b/sdk/scripts/single-grpc-client-test.ts @@ -19,6 +19,7 @@ import { ProgramAccount, } from '@coral-xyz/anchor'; import driftIDL from '../src/idl/drift.json'; +import { grpcMultiUserAccountSubscriber } from '../src/accounts/grpcMultiUserAccountSubscriber'; const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; const TOKEN = process.env.TOKEN; @@ -96,18 +97,27 @@ async function initializeSingleGrpcClient() { console.log(`📊 Markets: ${perpMarketIndexes.length} perp, ${spotMarketIndexes.length} spot`); console.log(`🔮 Oracles: ${oracleInfos.length}`); + + const grpcConfigs = { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: CommitmentLevel.PROCESSED, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }; + + const multiUserSubsciber = new grpcMultiUserAccountSubscriber( + program, + grpcConfigs + ); + const baseAccountSubscription = { type: 'grpc' as const, - grpcConfigs: { - endpoint: GRPC_ENDPOINT, - token: TOKEN, - commitmentLevel: CommitmentLevel.PROCESSED, - channelOptions: { - 'grpc.keepalive_time_ms': 10_000, - 'grpc.keepalive_timeout_ms': 1_000, - 'grpc.keepalive_permit_without_calls': 1, - }, - }, + grpcConfigs, + grpcMultiUserAccountSubscriber: multiUserSubsciber, }; const config: DriftClientConfig = { diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index 8bdd1cbccd..7a442ac5cd 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -372,6 +372,7 @@ export class grpcMultiAccountSubscriber { } }); }); + await this.fetch(); } async removeAccounts(accounts: PublicKey[]): Promise { diff --git a/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts b/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts new file mode 100644 index 0000000000..b4ad528d55 --- /dev/null +++ b/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts @@ -0,0 +1,274 @@ +import { + DataAndSlot, + GrpcConfigs, + NotSubscribedError, + ResubOpts, + UserAccountEvents, + UserAccountSubscriber, +} from './types'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Context, PublicKey } from '@solana/web3.js'; +import { Program } from '@coral-xyz/anchor'; +import { UserAccount } from '../types'; +import { grpcMultiAccountSubscriber } from './grpcMultiAccountSubscriber'; + +export class grpcMultiUserAccountSubscriber { + private program: Program; + private multiSubscriber: grpcMultiAccountSubscriber; + + private userData = new Map>(); + private listeners = new Map< + string, + Set> + >(); + private keyToPk = new Map(); + private pendingAddKeys = new Set(); + private debounceTimer?: ReturnType; + private debounceMs = 20; + private isMultiSubscribed = false; + private userAccountSubscribers = new Map(); + private grpcConfigs: GrpcConfigs; + resubOpts?: ResubOpts; + + private handleAccountChange = ( + accountId: PublicKey, + data: UserAccount, + context: Context, + _buffer?: unknown, + _accountProps?: unknown + ): void => { + const k = accountId.toBase58(); + this.userData.set(k, { data, slot: context.slot }); + const setForKey = this.listeners.get(k); + if (setForKey) { + for (const emitter of setForKey) { + emitter.emit('userAccountUpdate', data); + emitter.emit('update'); + } + } + }; + + public constructor( + program: Program, + grpcConfigs: GrpcConfigs, + resubOpts?: ResubOpts, + multiSubscriber?: grpcMultiAccountSubscriber + ) { + this.program = program; + this.multiSubscriber = multiSubscriber; + this.grpcConfigs = grpcConfigs; + this.resubOpts = resubOpts; + } + + public async subscribe(): Promise { + if (!this.multiSubscriber) { + this.multiSubscriber = + await grpcMultiAccountSubscriber.create( + this.grpcConfigs, + 'user', + this.program, + undefined, + this.resubOpts + ); + } + + // Subscribe all per-user subscribers first + await Promise.all( + Array.from(this.userAccountSubscribers.values()).map((subscriber) => + subscriber.subscribe() + ) + ); + // Ensure we immediately register any pending keys and kick off underlying subscription/fetch + await this.flushPending(); + // Proactively fetch once to populate data for all subscribed accounts + await this.multiSubscriber.fetch(); + // Wait until the underlying multi-subscriber has data for every registered user key + const targetKeys = Array.from(this.listeners.keys()); + if (targetKeys.length === 0) return; + // Poll until all keys are present in dataMap + // Use debounceMs as the polling cadence to avoid introducing new magic numbers + // eslint-disable-next-line no-constant-condition + while (true) { + const map = this.multiSubscriber.getAccountDataMap(); + let allPresent = true; + for (const k of targetKeys) { + if (!map.has(k)) { + allPresent = false; + break; + } + } + if (allPresent) break; + await new Promise((resolve) => setTimeout(resolve, this.debounceMs)); + } + } + + public forUser(userAccountPublicKey: PublicKey): UserAccountSubscriber { + if (this.userAccountSubscribers.has(userAccountPublicKey.toBase58())) { + return this.userAccountSubscribers.get(userAccountPublicKey.toBase58())!; + } + const key = userAccountPublicKey.toBase58(); + const perUserEmitter: StrictEventEmitter = + new EventEmitter(); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const parent = this; + let isSubscribed = false; + + const registerHandlerIfNeeded = async () => { + if (!this.listeners.has(key)) { + this.listeners.set(key, new Set()); + this.keyToPk.set(key, userAccountPublicKey); + this.pendingAddKeys.add(key); + this.scheduleFlush(); + } + }; + + const perUser: UserAccountSubscriber = { + get eventEmitter() { + return perUserEmitter; + }, + set eventEmitter(_v) {}, + + get isSubscribed() { + return isSubscribed; + }, + set isSubscribed(_v: boolean) { + isSubscribed = _v; + }, + + async subscribe(userAccount?: UserAccount): Promise { + if (isSubscribed) return true; + if (userAccount) { + this.updateData(userAccount, 0); + } + await registerHandlerIfNeeded(); + const setForKey = parent.listeners.get(key)!; + setForKey.add(perUserEmitter); + isSubscribed = true; + return true; + }, + + async fetch(): Promise { + if (!isSubscribed) { + throw new NotSubscribedError( + 'Must subscribe before fetching account updates' + ); + } + const account = (await parent.program.account.user.fetch( + userAccountPublicKey + )) as UserAccount; + this.updateData(account, 0); + }, + + updateData(userAccount: UserAccount, slot: number): void { + parent.userData.set(key, { data: userAccount, slot }); + perUserEmitter.emit('userAccountUpdate', userAccount); + perUserEmitter.emit('update'); + }, + + async unsubscribe(): Promise { + if (!isSubscribed) return; + const setForKey = parent.listeners.get(key); + if (setForKey) { + setForKey.delete(perUserEmitter); + if (setForKey.size === 0) { + parent.listeners.delete(key); + await parent.multiSubscriber.removeAccounts([userAccountPublicKey]); + parent.userData.delete(key); + parent.keyToPk.delete(key); + parent.pendingAddKeys.delete(key); + } + } + isSubscribed = false; + }, + + getUserAccountAndSlot(): DataAndSlot { + const das = parent.userData.get(key); + if (!das) { + throw new NotSubscribedError( + 'Must subscribe before getting user account data' + ); + } + return das; + }, + }; + + this.userAccountSubscribers.set(userAccountPublicKey.toBase58(), perUser); + return perUser; + } + + private scheduleFlush(): void { + if (this.debounceTimer) return; + this.debounceTimer = setTimeout(() => { + void this.flushPending(); + }, this.debounceMs); + } + + private async flushPending(): Promise { + const hasPending = this.pendingAddKeys.size > 0; + if (!hasPending) { + this.debounceTimer = undefined; + return; + } + + const allPks: PublicKey[] = []; + for (const k of this.listeners.keys()) { + const pk = this.keyToPk.get(k); + if (pk) allPks.push(pk); + } + if (allPks.length === 0) { + this.pendingAddKeys.clear(); + this.debounceTimer = undefined; + return; + } + + if (!this.isMultiSubscribed) { + await this.multiSubscriber.subscribe(allPks, this.handleAccountChange); + this.isMultiSubscribed = true; + await this.multiSubscriber.fetch(); + for (const k of this.pendingAddKeys) { + const pk = this.keyToPk.get(k); + if (pk) { + const data = this.multiSubscriber.getAccountData(k); + if (data) { + this.handleAccountChange( + pk, + data.data, + { slot: data.slot }, + undefined, + undefined + ); + } + } + } + } else { + const ms = this.multiSubscriber as unknown as { + onChangeMap: Map< + string, + ( + data: UserAccount, + context: Context, + buffer: unknown, + accountProps: unknown + ) => void + >; + }; + for (const k of this.pendingAddKeys) { + ms.onChangeMap.set(k, (data, ctx, buffer, accountProps) => { + this.multiSubscriber.setAccountData(k, data, ctx.slot); + this.handleAccountChange( + new PublicKey(k), + data, + ctx, + buffer, + accountProps + ); + }); + } + await this.multiSubscriber.addAccounts(allPks); + } + + this.pendingAddKeys.clear(); + this.debounceTimer = undefined; + } +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 41a3c3e6c1..fd39d50f95 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -379,6 +379,8 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, grpcConfigs: config.accountSubscription?.grpcConfigs, + grpcMultiUserAccountSubscriber: + config.accountSubscription?.grpcMultiUserAccountSubscriber, }; this.userStatsAccountSubscriptionConfig = { type: 'grpc', diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index ceb7b2603d..e805344539 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -24,6 +24,7 @@ import { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDri import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; import { grpcDriftClientAccountSubscriberV2 } from './accounts/grpcDriftClientAccountSubscriberV2'; import { grpcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; +import { grpcMultiUserAccountSubscriber } from './accounts/grpcMultiUserAccountSubscriber'; export type DriftClientConfig = { connection: Connection; @@ -73,6 +74,7 @@ export type DriftClientSubscriptionConfig = ) => | grpcDriftClientAccountSubscriberV2 | grpcDriftClientAccountSubscriber; + grpcMultiUserAccountSubscriber?: grpcMultiUserAccountSubscriber; } | { type: 'websocket'; diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 92512243fd..1c3e0730f1 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -137,15 +137,22 @@ export class User { } else if (config.accountSubscription?.type === 'custom') { this.accountSubscriber = config.accountSubscription.userAccountSubscriber; } else if (config.accountSubscription?.type === 'grpc') { - this.accountSubscriber = new grpcUserAccountSubscriber( - config.accountSubscription.grpcConfigs, - config.driftClient.program, - config.userAccountPublicKey, - { - resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, - logResubMessages: config.accountSubscription?.logResubMessages, - } - ); + if (config.accountSubscription.grpcMultiUserAccountSubscriber) { + this.accountSubscriber = + config.accountSubscription.grpcMultiUserAccountSubscriber.forUser( + config.userAccountPublicKey + ); + } else { + this.accountSubscriber = new grpcUserAccountSubscriber( + config.accountSubscription.grpcConfigs, + config.driftClient.program, + config.userAccountPublicKey, + { + resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, + logResubMessages: config.accountSubscription?.logResubMessages, + } + ); + } } else { if ( config.accountSubscription?.type === 'websocket' && diff --git a/sdk/src/userConfig.ts b/sdk/src/userConfig.ts index d5ba2147b9..9067833a78 100644 --- a/sdk/src/userConfig.ts +++ b/sdk/src/userConfig.ts @@ -4,6 +4,7 @@ import { BulkAccountLoader } from './accounts/bulkAccountLoader'; import { GrpcConfigs, UserAccountSubscriber } from './accounts/types'; import { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; import { UserAccount } from './types'; +import { grpcMultiUserAccountSubscriber } from './accounts/grpcMultiUserAccountSubscriber'; export type UserConfig = { accountSubscription?: UserSubscriptionConfig; @@ -17,6 +18,7 @@ export type UserSubscriptionConfig = resubTimeoutMs?: number; logResubMessages?: boolean; grpcConfigs: GrpcConfigs; + grpcMultiUserAccountSubscriber?: grpcMultiUserAccountSubscriber; } | { type: 'websocket'; From 8388765634a8daf5682b7b4abe427d5112ba0cf5 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:45:12 +0000 Subject: [PATCH 199/247] sdk: release v2.144.0-beta.6 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index b9f8b2d147..dd5a9d084d 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.144.0-beta.5 \ No newline at end of file +2.144.0-beta.6 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 674060a357..aab89e69a7 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.144.0-beta.5", + "version": "2.144.0-beta.6", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 8e1594c1be76ac5d726e7ed7024bef276ce40106 Mon Sep 17 00:00:00 2001 From: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:05:05 -0400 Subject: [PATCH 200/247] program: use-5min-for-target-expiry-price (#1967) * program: use-5min-for-target-expiry-price * program: support fill record for base amount at expiry * update for orderrecord * add get_existing_position_params_for_order_action * tweak * clean up * update CHANGELOG.md --------- Co-authored-by: Chris Heaney Co-authored-by: wphan --- CHANGELOG.md | 1 + programs/drift/src/controller/pnl.rs | 84 ++++++++++++++++++- .../drift/src/controller/pnl/delisting.rs | 13 +++ programs/drift/src/controller/repeg.rs | 5 +- 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7569ea585a..fb2e028aee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - program: add titan to whitelisted swap programs ([#1952](https://github.com/drift-labs/protocol-v2/pull/1952)) - program: allow hot wallet to increase max spread and pause funding ([#1957](https://github.com/drift-labs/protocol-v2/pull/1957)) +- program: use-5min-for-target-expiry-price ([#1967](https://github.com/drift-labs/protocol-v2/pull/1967)) ### Fixes diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index a5b8370c55..98add9fa67 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -20,7 +20,10 @@ use crate::math::position::calculate_base_asset_value_with_expiry_price; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; +use crate::get_then_update_id; +use crate::math::orders::calculate_existing_position_fields_for_order_action; use crate::msg; +use crate::state::events::{OrderAction, OrderActionRecord, OrderRecord}; use crate::state::events::{OrderActionExplanation, SettlePnlExplanation, SettlePnlRecord}; use crate::state::oracle_map::OracleMap; use crate::state::paused_operations::PerpOperation; @@ -30,7 +33,7 @@ use crate::state::settle_pnl_mode::SettlePnlMode; use crate::state::spot_market::{SpotBalance, SpotBalanceType}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::State; -use crate::state::user::{MarketType, User}; +use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User}; use crate::validate; use anchor_lang::prelude::Pubkey; use anchor_lang::prelude::*; @@ -438,6 +441,11 @@ pub fn settle_expired_position( let base_asset_amount = user.perp_positions[position_index].base_asset_amount; let quote_entry_amount = user.perp_positions[position_index].quote_entry_amount; + let user_position_direction_to_close = + user.perp_positions[position_index].get_direction_to_close(); + let user_existing_position_params_for_order_action = user.perp_positions[position_index] + .get_existing_position_params_for_order_action(user_position_direction_to_close); + let position_delta = PositionDelta { quote_asset_amount: base_asset_value, base_asset_amount: -user.perp_positions[position_index].base_asset_amount, @@ -470,6 +478,80 @@ pub fn settle_expired_position( -pnl_to_settle_with_user.cast()?, )?; + if position_delta.base_asset_amount != 0 { + // get ids for order fills + let user_order_id = get_then_update_id!(user, next_order_id); + let fill_record_id = get_then_update_id!(perp_market, next_fill_record_id); + + let base_asset_amount = position_delta.base_asset_amount; + let user_existing_position_direction = user.perp_positions[position_index].get_direction(); + + let user_order = Order { + slot, + base_asset_amount: base_asset_amount.unsigned_abs(), + order_id: user_order_id, + market_index: perp_market.market_index, + status: OrderStatus::Open, + order_type: OrderType::Market, + market_type: MarketType::Perp, + direction: user_position_direction_to_close, + existing_position_direction: user_existing_position_direction, + ..Order::default() + }; + + emit!(OrderRecord { + ts: now, + user: *user_key, + order: user_order + }); + + let (taker_existing_quote_entry_amount, taker_existing_base_asset_amount) = + calculate_existing_position_fields_for_order_action( + base_asset_amount.unsigned_abs(), + user_existing_position_params_for_order_action, + )?; + + let fill_record = OrderActionRecord { + ts: now, + action: OrderAction::Fill, + action_explanation: OrderActionExplanation::MarketExpired, + market_index: perp_market.market_index, + market_type: MarketType::Perp, + filler: None, + filler_reward: None, + fill_record_id: Some(fill_record_id), + base_asset_amount_filled: Some(base_asset_amount.unsigned_abs()), + quote_asset_amount_filled: Some(base_asset_value.unsigned_abs()), + taker_fee: Some(fee.unsigned_abs()), + maker_fee: None, + referrer_reward: None, + quote_asset_amount_surplus: None, + spot_fulfillment_method_fee: None, + taker: Some(*user_key), + taker_order_id: Some(user_order_id), + taker_order_direction: Some(user_position_direction_to_close), + taker_order_base_asset_amount: Some(base_asset_amount.unsigned_abs()), + taker_order_cumulative_base_asset_amount_filled: Some(base_asset_amount.unsigned_abs()), + taker_order_cumulative_quote_asset_amount_filled: Some(base_asset_value.unsigned_abs()), + maker: None, + maker_order_id: None, + maker_order_direction: None, + maker_order_base_asset_amount: None, + maker_order_cumulative_base_asset_amount_filled: None, + maker_order_cumulative_quote_asset_amount_filled: None, + oracle_price: perp_market.expiry_price, + bit_flags: 0, + taker_existing_quote_entry_amount, + taker_existing_base_asset_amount, + maker_existing_quote_entry_amount: None, + maker_existing_base_asset_amount: None, + trigger_price: None, + builder_idx: None, + builder_fee: None, + }; + emit!(fill_record); + } + update_settled_pnl(user, position_index, pnl_to_settle_with_user.cast()?)?; perp_market.amm.base_asset_amount_with_amm = perp_market diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index eafbd2148b..db148230f0 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -209,6 +209,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, quote_asset_amount: -(QUOTE_PRECISION_I128 * 50), //longs have $100 cost basis @@ -318,6 +319,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, quote_asset_amount: -(QUOTE_PRECISION_I128 * 10), //longs have $20 cost basis @@ -430,6 +432,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, total_fee_minus_distributions: -(100000 * QUOTE_PRECISION_I128), // down $100k @@ -543,6 +546,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, total_fee_minus_distributions: -(100000 * QUOTE_PRECISION_I128), // down $100k @@ -652,6 +656,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, quote_asset_amount: -(QUOTE_PRECISION_I128 * 10), //longs have $20 cost basis @@ -870,6 +875,8 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, + ..HistoricalOracleData::default() }, quote_asset_amount: (QUOTE_PRECISION_I128 * 10), //longs have -$20 cost basis @@ -1091,6 +1098,8 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, + ..HistoricalOracleData::default() }, quote_asset_amount: (QUOTE_PRECISION_I128 * 20 * 2000), //longs have -$20 cost basis @@ -1294,6 +1303,8 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, + ..HistoricalOracleData::default() }, quote_asset_amount: -(QUOTE_PRECISION_I128 * 20 * 1000 - QUOTE_PRECISION_I128), @@ -1717,6 +1728,8 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, + ..HistoricalOracleData::default() }, quote_asset_amount: (QUOTE_PRECISION_I128 * 200) diff --git a/programs/drift/src/controller/repeg.rs b/programs/drift/src/controller/repeg.rs index 1e2366af22..f41af93fdc 100644 --- a/programs/drift/src/controller/repeg.rs +++ b/programs/drift/src/controller/repeg.rs @@ -431,7 +431,10 @@ pub fn settle_expired_market( let target_expiry_price = if market.amm.oracle_source == OracleSource::Prelaunch { market.amm.historical_oracle_data.last_oracle_price } else { - market.amm.historical_oracle_data.last_oracle_price_twap + market + .amm + .historical_oracle_data + .last_oracle_price_twap_5min }; crate::dlog!(target_expiry_price); From e61ea322ea3633f371ce242d1fe27b0eac2f70d6 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 27 Oct 2025 16:09:30 -0700 Subject: [PATCH 201/247] v2.144.0 --- CHANGELOG.md | 11 ++++++++++- Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 2 +- yarn.lock | 5 ----- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb2e028aee..ba0186e9e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking +## [2.144.0] - 2025-10-27 + +### Features + +- program: use-5min-for-target-expiry-price ([#1967](https://github.com/drift-labs/protocol-v2/pull/1967)) + +### Fixes + +### Breaking + ## [2.143.0] - 2025-10-22 - program: relax filling conditions for low risk orders vs amm ([#1968](https://github.com/drift-labs/protocol-v2/pull/1968)) @@ -32,7 +42,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - program: add titan to whitelisted swap programs ([#1952](https://github.com/drift-labs/protocol-v2/pull/1952)) - program: allow hot wallet to increase max spread and pause funding ([#1957](https://github.com/drift-labs/protocol-v2/pull/1957)) -- program: use-5min-for-target-expiry-price ([#1967](https://github.com/drift-labs/protocol-v2/pull/1967)) ### Fixes diff --git a/Cargo.lock b/Cargo.lock index e77492a445..c48d6a323b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.143.0" +version = "2.144.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 0a2f8cb862..08163a1fe6 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.143.0" +version = "2.144.0" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index aab89e69a7..8dc2d3cded 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.144.0-beta.6", + "version": "2.144.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 28b245a6ec..ed9c2538a6 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.143.0", + "version": "2.144.0", "name": "drift", "instructions": [ { diff --git a/yarn.lock b/yarn.lock index cafe370cd8..2678bc1b59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -225,11 +225,6 @@ snake-case "^3.0.4" spok "^1.4.3" -"@msgpack/msgpack@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.1.2.tgz#fdd25cc2202297519798bbaf4689152ad9609e19" - integrity sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ== - "@noble/curves@^1.0.0", "@noble/curves@^1.4.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.2.tgz#73388356ce733922396214a933ff7c95afcef911" From 8ccafa571dea9cb5101915929014cae0dc3146bf Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:17:55 +0000 Subject: [PATCH 202/247] sdk: release v2.145.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index dd5a9d084d..0033687b63 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.144.0-beta.6 \ No newline at end of file +2.145.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 8dc2d3cded..2b170cb477 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.144.0", + "version": "2.145.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From aa9b1cc5a9faa1771f968e189a43ec4ddcd8bdf7 Mon Sep 17 00:00:00 2001 From: cha-kos Date: Mon, 27 Oct 2025 19:49:32 -0400 Subject: [PATCH 203/247] Implement Unified Swap Client --- sdk/src/driftClient.ts | 156 ++++++++++++++++- sdk/src/index.ts | 3 +- sdk/src/jupiter/jupiterClient.ts | 3 +- sdk/src/swap/UnifiedSwapClient.ts | 281 ++++++++++++++++++++++++++++++ 4 files changed, 435 insertions(+), 8 deletions(-) create mode 100644 sdk/src/swap/UnifiedSwapClient.ts diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index fd39d50f95..0c6cb1b500 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -160,8 +160,8 @@ import { castNumberToSpotPrecision } from './math/spotMarket'; import { JupiterClient, QuoteResponse, - SwapMode, } from './jupiter/jupiterClient'; +import { SwapMode } from './swap/UnifiedSwapClient'; import { getNonIdleUserFilter } from './memcmp'; import { UserStatsSubscriptionConfig } from './userStatsConfig'; import { getMarinadeDepositIx, getMarinadeFinanceProgram } from './marinade'; @@ -210,9 +210,11 @@ import { isBuilderOrderCompleted, } from './math/builder'; import { TitanClient, SwapMode as TitanSwapMode } from './titan/titanClient'; +import { UnifiedSwapClient } from './swap/UnifiedSwapClient'; /** - * Union type for swap clients (Titan and Jupiter) + * Union type for swap clients (Titan and Jupiter) - Legacy type + * @deprecated Use UnifiedSwapClient class instead */ export type SwapClient = TitanClient | JupiterClient; @@ -5772,7 +5774,7 @@ export class DriftClient { quote, onlyDirectRoutes = false, }: { - swapClient: SwapClient; + swapClient: UnifiedSwapClient | SwapClient; outMarketIndex: number; inMarketIndex: number; outAssociatedTokenAccount?: PublicKey; @@ -5793,7 +5795,23 @@ export class DriftClient { lookupTables: AddressLookupTableAccount[]; }; - if (swapClient instanceof TitanClient) { + // Use unified SwapClient if available + if (swapClient instanceof UnifiedSwapClient) { + res = await this.getSwapIxV2({ + swapClient, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + reduceOnly, + quote, + v6, + }); + } else if (swapClient instanceof TitanClient) { res = await this.getTitanSwapIx({ titanClient: swapClient, outMarketIndex, @@ -5823,7 +5841,7 @@ export class DriftClient { }); } else { throw new Error( - 'Invalid swap client type. Must be TitanClient or JupiterClient.' + 'Invalid swap client type. Must be SwapClient, TitanClient, or JupiterClient.' ); } @@ -6243,6 +6261,134 @@ export class DriftClient { return { beginSwapIx, endSwapIx }; } + public async getSwapIxV2({ + swapClient, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + reduceOnly, + quote, + v6, + }: { + swapClient: UnifiedSwapClient; + outMarketIndex: number; + inMarketIndex: number; + outAssociatedTokenAccount?: PublicKey; + inAssociatedTokenAccount?: PublicKey; + amount: BN; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + reduceOnly?: SwapReduceOnly; + quote?: QuoteResponse; + v6?: { + quote?: QuoteResponse; + }; + }): Promise<{ + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + // Get market accounts to determine mints + const outMarket = this.getSpotMarketAccount(outMarketIndex); + const inMarket = this.getSpotMarketAccount(inMarketIndex); + + const isExactOut = swapMode === 'ExactOut'; + const exactOutBufferedAmountIn = amount.muln(1001).divn(1000); // Add 10bp buffer + + const preInstructions: TransactionInstruction[] = []; + + // Handle token accounts if not provided + let finalOutAssociatedTokenAccount = outAssociatedTokenAccount; + let finalInAssociatedTokenAccount = inAssociatedTokenAccount; + + if (!finalOutAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + finalOutAssociatedTokenAccount = await this.getAssociatedTokenAccount( + outMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + finalOutAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + finalOutAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + outMarket.mint, + tokenProgram + ) + ); + } + } + + if (!finalInAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(inMarket); + finalInAssociatedTokenAccount = await this.getAssociatedTokenAccount( + inMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + finalInAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + finalInAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + inMarket.mint, + tokenProgram + ) + ); + } + } + + // Get drift swap instructions for begin and end + const { beginSwapIx, endSwapIx } = await this.getSwapIx({ + outMarketIndex, + inMarketIndex, + amountIn: isExactOut ? exactOutBufferedAmountIn : amount, + inTokenAccount: finalInAssociatedTokenAccount, + outTokenAccount: finalOutAssociatedTokenAccount, + reduceOnly, + }); + + // Get core swap instructions from SwapClient + const swapResult = await swapClient.getSwapInstructions({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + userPublicKey: this.provider.wallet.publicKey, + slippageBps, + swapMode, + onlyDirectRoutes, + quote: quote ?? v6?.quote, + }); + + const allInstructions = [ + ...preInstructions, + beginSwapIx, + ...swapResult.instructions, + endSwapIx, + ]; + + return { + ixs: allInstructions, + lookupTables: swapResult.lookupTables, + }; + } + public async stakeForMSOL({ amount }: { amount: BN }): Promise { const ixs = await this.getStakeForMSOLIx({ amount }); const tx = await this.buildTransaction(ixs); diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 5458898cfd..0919ebd346 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -53,7 +53,8 @@ export * from './events/webSocketLogProvider'; export * from './events/parse'; export * from './events/pollingLogProvider'; export * from './jupiter/jupiterClient'; -export { TitanClient } from './titan/titanClient'; +// Primary swap client interface - use this for all swap operations +export * from './swap/UnifiedSwapClient'; export * from './math/auction'; export * from './math/builder'; export * from './math/spotMarket'; diff --git a/sdk/src/jupiter/jupiterClient.ts b/sdk/src/jupiter/jupiterClient.ts index e943db5851..a2a2ba483b 100644 --- a/sdk/src/jupiter/jupiterClient.ts +++ b/sdk/src/jupiter/jupiterClient.ts @@ -8,8 +8,7 @@ import { } from '@solana/web3.js'; import fetch from 'node-fetch'; import { BN } from '@coral-xyz/anchor'; - -export type SwapMode = 'ExactIn' | 'ExactOut'; +import { SwapMode } from '../swap/UnifiedSwapClient'; export interface MarketInfo { id: string; diff --git a/sdk/src/swap/UnifiedSwapClient.ts b/sdk/src/swap/UnifiedSwapClient.ts new file mode 100644 index 0000000000..ddc3444683 --- /dev/null +++ b/sdk/src/swap/UnifiedSwapClient.ts @@ -0,0 +1,281 @@ +import { + Connection, + PublicKey, + TransactionMessage, + AddressLookupTableAccount, + VersionedTransaction, + TransactionInstruction, +} from '@solana/web3.js'; +import { BN } from '@coral-xyz/anchor'; +import { JupiterClient, QuoteResponse as JupiterQuoteResponse } from '../jupiter/jupiterClient'; +import { TitanClient, QuoteResponse as TitanQuoteResponse, SwapMode as TitanSwapMode } from '../titan/titanClient'; + +export type SwapMode = 'ExactIn' | 'ExactOut'; +export type SwapClientType = 'jupiter' | 'titan'; + +export type UnifiedQuoteResponse = JupiterQuoteResponse | TitanQuoteResponse; + +export interface SwapQuoteParams { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey?: PublicKey; // Required for Titan, optional for Jupiter + maxAccounts?: number; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + excludeDexes?: string[]; + sizeConstraint?: number; // Titan-specific + accountsLimitWritable?: number; // Titan-specific + autoSlippage?: boolean; // Jupiter-specific + maxAutoSlippageBps?: number; // Jupiter-specific + usdEstimate?: number; // Jupiter-specific +} + +export interface SwapTransactionParams { + quote: UnifiedQuoteResponse; + userPublicKey: PublicKey; + slippageBps?: number; +} + +export interface SwapTransactionResult { + transaction?: VersionedTransaction; // Jupiter returns this + transactionMessage?: TransactionMessage; // Titan returns this + lookupTables?: AddressLookupTableAccount[]; // Titan returns this +} + +export class UnifiedSwapClient { + private client: JupiterClient | TitanClient; + private clientType: SwapClientType; + + constructor({ + clientType, + connection, + authToken, + url, + }: { + clientType: SwapClientType; + connection: Connection; + authToken?: string; // Required for Titan, optional for Jupiter + url?: string; // Optional custom URL + }) { + this.clientType = clientType; + + if (clientType === 'jupiter') { + this.client = new JupiterClient({ + connection, + url, + }); + } else if (clientType === 'titan') { + if (!authToken) { + throw new Error('authToken is required for Titan client'); + } + this.client = new TitanClient({ + connection, + authToken, + url, + }); + } else { + throw new Error(`Unsupported client type: ${clientType}`); + } + } + + /** + * Get a swap quote from the underlying client + */ + public async getQuote(params: SwapQuoteParams): Promise { + if (this.clientType === 'jupiter') { + const jupiterClient = this.client as JupiterClient; + const { + userPublicKey: _userPublicKey, // Not needed for Jupiter + sizeConstraint: _sizeConstraint, // Jupiter-specific params to exclude + accountsLimitWritable: _accountsLimitWritable, + ...jupiterParams + } = params; + + return await jupiterClient.getQuote(jupiterParams); + } else { + const titanClient = this.client as TitanClient; + const { + autoSlippage: _autoSlippage, // Titan-specific params to exclude + maxAutoSlippageBps: _maxAutoSlippageBps, + usdEstimate: _usdEstimate, + ...titanParams + } = params; + + if (!titanParams.userPublicKey) { + throw new Error('userPublicKey is required for Titan quotes'); + } + + // Cast to ensure TypeScript knows userPublicKey is defined + const titanParamsWithUser = { + ...titanParams, + userPublicKey: titanParams.userPublicKey, + swapMode: titanParams.swapMode as string, // Titan expects string + }; + + return await titanClient.getQuote(titanParamsWithUser); + } + } + + /** + * Get a swap transaction from the underlying client + */ + public async getSwap(params: SwapTransactionParams): Promise { + if (this.clientType === 'jupiter') { + const jupiterClient = this.client as JupiterClient; + // Cast the quote to Jupiter's QuoteResponse type + const jupiterParams = { + ...params, + quote: params.quote as JupiterQuoteResponse, + }; + const transaction = await jupiterClient.getSwap(jupiterParams); + return { transaction }; + } else { + const titanClient = this.client as TitanClient; + const { quote, userPublicKey, slippageBps } = params; + + // For Titan, we need to reconstruct the parameters from the quote + const titanQuote = quote as TitanQuoteResponse; + const result = await titanClient.getSwap({ + inputMint: new PublicKey(titanQuote.inputMint), + outputMint: new PublicKey(titanQuote.outputMint), + amount: new BN(titanQuote.inAmount), + userPublicKey, + slippageBps: slippageBps || titanQuote.slippageBps, + swapMode: titanQuote.swapMode, + }); + + return { + transactionMessage: result.transactionMessage, + lookupTables: result.lookupTables, + }; + } + } + + /** + * Get swap instructions from the underlying client (Jupiter or Titan) + * This is the core swap logic without any context preparation + */ + public async getSwapInstructions({ + inputMint, + outputMint, + amount, + userPublicKey, + slippageBps, + swapMode = 'ExactIn', + onlyDirectRoutes = false, + quote, + sizeConstraint, + }: { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey: PublicKey; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + quote?: UnifiedQuoteResponse; + sizeConstraint?: number; + }): Promise<{ + instructions: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + const isExactOut = swapMode === 'ExactOut'; + let swapInstructions: TransactionInstruction[]; + let lookupTables: AddressLookupTableAccount[]; + + if (this.clientType === 'jupiter') { + const jupiterClient = this.client as JupiterClient; + + // Get quote if not provided + let finalQuote = quote as JupiterQuoteResponse; + if (!finalQuote) { + finalQuote = await jupiterClient.getQuote({ + inputMint, + outputMint, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + }); + } + + if (!finalQuote) { + throw new Error("Could not fetch Jupiter's quote. Please try again."); + } + + // Get swap transaction and extract instructions + const transaction = await jupiterClient.getSwap({ + quote: finalQuote, + userPublicKey, + slippageBps, + }); + + const { transactionMessage, lookupTables: jupiterLookupTables } = + await jupiterClient.getTransactionMessageAndLookupTables({ + transaction, + }); + + swapInstructions = jupiterClient.getJupiterInstructions({ + transactionMessage, + inputMint, + outputMint, + }); + + lookupTables = jupiterLookupTables; + } else { + const titanClient = this.client as TitanClient; + + // For Titan, get swap directly (it handles quote internally) + const { transactionMessage, lookupTables: titanLookupTables } = await titanClient.getSwap({ + inputMint, + outputMint, + amount, + userPublicKey, + slippageBps, + swapMode: isExactOut ? TitanSwapMode.ExactOut : TitanSwapMode.ExactIn, + onlyDirectRoutes, + sizeConstraint: sizeConstraint || (1280 - 375), // MAX_TX_BYTE_SIZE - buffer for drift instructions + }); + + swapInstructions = titanClient.getTitanInstructions({ + transactionMessage, + inputMint, + outputMint, + }); + + lookupTables = titanLookupTables; + } + + return { instructions: swapInstructions, lookupTables }; + } + + /** + * Get the underlying client instance + */ + public getClient(): JupiterClient | TitanClient { + return this.client; + } + + /** + * Get the client type + */ + public getClientType(): SwapClientType { + return this.clientType; + } + + /** + * Check if this is a Jupiter client + */ + public isJupiter(): boolean { + return this.clientType === 'jupiter'; + } + + /** + * Check if this is a Titan client + */ + public isTitan(): boolean { + return this.clientType === 'titan'; + } +} \ No newline at end of file From a1087aa262a8704685f592ada7dd3b21d42ccdb9 Mon Sep 17 00:00:00 2001 From: cha-kos Date: Mon, 27 Oct 2025 20:11:40 -0400 Subject: [PATCH 204/247] prettify --- sdk/src/driftClient.ts | 7 ++--- sdk/src/swap/UnifiedSwapClient.ts | 48 +++++++++++++++++++------------ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 0c6cb1b500..87681bf13c 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -157,10 +157,7 @@ import { isSpotPositionAvailable } from './math/spotPosition'; import { calculateMarketMaxAvailableInsurance } from './math/market'; import { fetchUserStatsAccount } from './accounts/fetch'; import { castNumberToSpotPrecision } from './math/spotMarket'; -import { - JupiterClient, - QuoteResponse, -} from './jupiter/jupiterClient'; +import { JupiterClient, QuoteResponse } from './jupiter/jupiterClient'; import { SwapMode } from './swap/UnifiedSwapClient'; import { getNonIdleUserFilter } from './memcmp'; import { UserStatsSubscriptionConfig } from './userStatsConfig'; @@ -6301,7 +6298,7 @@ export class DriftClient { const exactOutBufferedAmountIn = amount.muln(1001).divn(1000); // Add 10bp buffer const preInstructions: TransactionInstruction[] = []; - + // Handle token accounts if not provided let finalOutAssociatedTokenAccount = outAssociatedTokenAccount; let finalInAssociatedTokenAccount = inAssociatedTokenAccount; diff --git a/sdk/src/swap/UnifiedSwapClient.ts b/sdk/src/swap/UnifiedSwapClient.ts index ddc3444683..966f7e71bd 100644 --- a/sdk/src/swap/UnifiedSwapClient.ts +++ b/sdk/src/swap/UnifiedSwapClient.ts @@ -7,8 +7,15 @@ import { TransactionInstruction, } from '@solana/web3.js'; import { BN } from '@coral-xyz/anchor'; -import { JupiterClient, QuoteResponse as JupiterQuoteResponse } from '../jupiter/jupiterClient'; -import { TitanClient, QuoteResponse as TitanQuoteResponse, SwapMode as TitanSwapMode } from '../titan/titanClient'; +import { + JupiterClient, + QuoteResponse as JupiterQuoteResponse, +} from '../jupiter/jupiterClient'; +import { + TitanClient, + QuoteResponse as TitanQuoteResponse, + SwapMode as TitanSwapMode, +} from '../titan/titanClient'; export type SwapMode = 'ExactIn' | 'ExactOut'; export type SwapClientType = 'jupiter' | 'titan'; @@ -83,7 +90,9 @@ export class UnifiedSwapClient { /** * Get a swap quote from the underlying client */ - public async getQuote(params: SwapQuoteParams): Promise { + public async getQuote( + params: SwapQuoteParams + ): Promise { if (this.clientType === 'jupiter') { const jupiterClient = this.client as JupiterClient; const { @@ -121,7 +130,9 @@ export class UnifiedSwapClient { /** * Get a swap transaction from the underlying client */ - public async getSwap(params: SwapTransactionParams): Promise { + public async getSwap( + params: SwapTransactionParams + ): Promise { if (this.clientType === 'jupiter') { const jupiterClient = this.client as JupiterClient; // Cast the quote to Jupiter's QuoteResponse type @@ -134,7 +145,7 @@ export class UnifiedSwapClient { } else { const titanClient = this.client as TitanClient; const { quote, userPublicKey, slippageBps } = params; - + // For Titan, we need to reconstruct the parameters from the quote const titanQuote = quote as TitanQuoteResponse; const result = await titanClient.getSwap({ @@ -187,7 +198,7 @@ export class UnifiedSwapClient { if (this.clientType === 'jupiter') { const jupiterClient = this.client as JupiterClient; - + // Get quote if not provided let finalQuote = quote as JupiterQuoteResponse; if (!finalQuote) { @@ -226,18 +237,19 @@ export class UnifiedSwapClient { lookupTables = jupiterLookupTables; } else { const titanClient = this.client as TitanClient; - + // For Titan, get swap directly (it handles quote internally) - const { transactionMessage, lookupTables: titanLookupTables } = await titanClient.getSwap({ - inputMint, - outputMint, - amount, - userPublicKey, - slippageBps, - swapMode: isExactOut ? TitanSwapMode.ExactOut : TitanSwapMode.ExactIn, - onlyDirectRoutes, - sizeConstraint: sizeConstraint || (1280 - 375), // MAX_TX_BYTE_SIZE - buffer for drift instructions - }); + const { transactionMessage, lookupTables: titanLookupTables } = + await titanClient.getSwap({ + inputMint, + outputMint, + amount, + userPublicKey, + slippageBps, + swapMode: isExactOut ? TitanSwapMode.ExactOut : TitanSwapMode.ExactIn, + onlyDirectRoutes, + sizeConstraint: sizeConstraint || 1280 - 375, // MAX_TX_BYTE_SIZE - buffer for drift instructions + }); swapInstructions = titanClient.getTitanInstructions({ transactionMessage, @@ -278,4 +290,4 @@ export class UnifiedSwapClient { public isTitan(): boolean { return this.clientType === 'titan'; } -} \ No newline at end of file +} From 2eedeac2d956113a0b12f40469b50e7117e14f99 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:12:33 +0000 Subject: [PATCH 205/247] sdk: release v2.145.0-beta.1 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 0033687b63..ca74bb6dd0 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.145.0-beta.0 \ No newline at end of file +2.145.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 2b170cb477..14e68cc9fe 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.145.0-beta.0", + "version": "2.145.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From a9658e6fdaa97ee1da41d2fc091eeef841d8506c Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Tue, 28 Oct 2025 08:58:22 -0600 Subject: [PATCH 206/247] feat: update getPlaceSignedMsgTakerPerpOrderIxs for iso position deposit (#1976) * feat: update getPlaceSignedMsgTakerPerpOrderIxs for iso position deposit * fix: prettier * feat: trim down idl diff to only iso deposit fields * feat: coalesce undefined into null for swift iso deposit param * fix: formatting + cleanup --- sdk/src/driftClient.ts | 23 +++++++++++++++++------ sdk/src/idl/drift.json | 12 ++++++++++++ sdk/src/types.ts | 2 ++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 87681bf13c..fd464aa324 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -7253,6 +7253,9 @@ export class DriftClient { if (orderParamsMessage.maxMarginRatio === undefined) { orderParamsMessage.maxMarginRatio = null; } + if (orderParamsMessage.isolatedPositionDeposit === undefined) { + orderParamsMessage.isolatedPositionDeposit = null; + } const anchorIxName = delegateSigner ? 'global' + ':' + 'SignedMsgOrderParamsDelegateMessage' @@ -7355,12 +7358,6 @@ export class DriftClient { precedingIxs: TransactionInstruction[] = [], overrideCustomIxIndex?: number ): Promise { - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [takerInfo.takerUserAccount], - useMarketLastSlotCache: false, - readablePerpMarketIndex: marketIndex, - }); - const isDelegateSigner = takerInfo.signingAuthority.equals( takerInfo.takerUserAccount.delegate ); @@ -7374,6 +7371,20 @@ export class DriftClient { borshBuf, isDelegateSigner ); + + const writableSpotMarketIndexes = signedMessage.isolatedPositionDeposit?.gt( + ZERO + ) + ? [QUOTE_SPOT_MARKET_INDEX] + : undefined; + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [takerInfo.takerUserAccount], + useMarketLastSlotCache: false, + readablePerpMarketIndex: marketIndex, + writableSpotMarketIndexes, + }); + if (isUpdateHighLeverageMode(signedMessage.signedMsgOrderParams.bitFlags)) { remainingAccounts.push({ pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index ed9c2538a6..a358214ea2 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -10334,6 +10334,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -10399,6 +10405,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index e2801414ed..42ddce507d 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1337,6 +1337,7 @@ export type SignedMsgOrderParamsMessage = { maxMarginRatio?: number | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; + isolatedPositionDeposit?: BN | null; }; export type SignedMsgOrderParamsDelegateMessage = { @@ -1349,6 +1350,7 @@ export type SignedMsgOrderParamsDelegateMessage = { maxMarginRatio?: number | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; + isolatedPositionDeposit?: BN | null; }; export type SignedMsgTriggerOrderParams = { From 35586ec4788e1f60bd83517e924de86ad1fb52ff Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:03:52 +0000 Subject: [PATCH 207/247] sdk: release v2.145.0-beta.2 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index ca74bb6dd0..782892d821 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.145.0-beta.1 \ No newline at end of file +2.145.0-beta.2 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 14e68cc9fe..3b79f17ffe 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.145.0-beta.1", + "version": "2.145.0-beta.2", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From c6b9263c5d094233c29178510bded26e65e5bb24 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 28 Oct 2025 14:41:49 -0600 Subject: [PATCH 208/247] chore: re organizing some user sdk funcs --- sdk/src/user.ts | 1023 ++++++++++++++++++++++++----------------------- 1 file changed, 512 insertions(+), 511 deletions(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index a5bb629359..a3cc119616 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -125,282 +125,6 @@ export class User { return this._isSubscribed && this.accountSubscriber.isSubscribed; } - /** - * Compute a consolidated margin snapshot once, without caching. - * Consumers can use this to avoid duplicating work across separate calls. - */ - // TODO: need another param to tell it give it back leverage compnents - // TODO: change get leverage functions need to pull the right values from - public getMarginCalculation( - marginCategory: MarginCategory = 'Initial', - opts?: { - strict?: boolean; // mirror StrictOraclePrice application - includeOpenOrders?: boolean; - enteringHighLeverage?: boolean; - liquidationBuffer?: BN; // margin_buffer analog for buffer mode - marginRatioOverride?: number; // mirrors context.margin_ratio_override - } - ): MarginCalculation { - const strict = opts?.strict ?? false; - const enteringHighLeverage = opts?.enteringHighLeverage ?? false; - const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? - const marginBuffer = opts?.liquidationBuffer; // treat as MARGIN_BUFFER ratio if provided - const marginRatioOverride = opts?.marginRatioOverride; - - // Equivalent to on-chain user_custom_margin_ratio - let userCustomMarginRatio = - marginCategory === 'Initial' ? this.getUserAccount().maxMarginRatio : 0; - if (marginRatioOverride !== undefined) { - userCustomMarginRatio = Math.max( - userCustomMarginRatio, - marginRatioOverride - ); - } - - // Initialize calc via JS mirror of Rust MarginCalculation - const ctx = MarginContext.standard(marginCategory) - .strictMode(strict) - .setMarginBuffer(marginBuffer) - .setMarginRatioOverride(userCustomMarginRatio); - const calc = new MarginCalculation(ctx); - - // SPOT POSITIONS - // TODO: include open orders in the worst-case simulation in the same way on both spot and perp positions - for (const spotPosition of this.getUserAccount().spotPositions) { - if (isSpotPositionAvailable(spotPosition)) continue; - - const spotMarket = this.driftClient.getSpotMarketAccount( - spotPosition.marketIndex - ); - const oraclePriceData = this.getOracleDataForSpotMarket( - spotPosition.marketIndex - ); - const twap5 = strict - ? calculateLiveOracleTwap( - spotMarket.historicalOracleData, - oraclePriceData, - new BN(Math.floor(Date.now() / 1000)), - FIVE_MINUTE - ) - : undefined; - const strictOracle = new StrictOraclePrice(oraclePriceData.price, twap5); - - if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX) { - const tokenAmount = getSignedTokenAmount( - getTokenAmount( - spotPosition.scaledBalance, - spotMarket, - spotPosition.balanceType - ), - spotPosition.balanceType - ); - if (isVariant(spotPosition.balanceType, 'deposit')) { - // add deposit value to total collateral - const tokenValue = getStrictTokenValue( - tokenAmount, - spotMarket.decimals, - strictOracle - ); - calc.addCrossMarginTotalCollateral(tokenValue); - } else { - // borrow on quote contributes to margin requirement - const tokenValueAbs = getStrictTokenValue( - tokenAmount, - spotMarket.decimals, - strictOracle - ).abs(); - calc.addCrossMarginRequirement(tokenValueAbs, tokenValueAbs); - calc.addSpotLiability(); - } - continue; - } - - // Non-quote spot: worst-case simulation - const { - tokenAmount: worstCaseTokenAmount, - ordersValue: worstCaseOrdersValue, - tokenValue: worstCaseTokenValue, - weightedTokenValue: worstCaseWeightedTokenValue, - } = getWorstCaseTokenAmounts( - spotPosition, - spotMarket, - strictOracle, - marginCategory, - userCustomMarginRatio, - includeOpenOrders - ); - - // open order IM - calc.addCrossMarginRequirement( - new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), - ZERO - ); - - if (worstCaseTokenAmount.gt(ZERO)) { - // asset side increases total collateral (weighted) - calc.addCrossMarginTotalCollateral(worstCaseWeightedTokenValue); - } else if (worstCaseTokenAmount.lt(ZERO)) { - // liability side increases margin requirement (weighted >= abs(token_value)) - const liabilityWeighted = worstCaseWeightedTokenValue.abs(); - calc.addCrossMarginRequirement( - liabilityWeighted, - worstCaseTokenValue.abs() - ); - calc.addSpotLiability(); - calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); - } else if (spotPosition.openOrders !== 0) { - calc.addSpotLiability(); - calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); - } - - // orders value contributes to collateral or requirement - if (worstCaseOrdersValue.gt(ZERO)) { - calc.addCrossMarginTotalCollateral(worstCaseOrdersValue); - } else if (worstCaseOrdersValue.lt(ZERO)) { - const absVal = worstCaseOrdersValue.abs(); - calc.addCrossMarginRequirement(absVal, absVal); - } - } - - // PERP POSITIONS - for (const marketPosition of this.getActivePerpPositions()) { - const market = this.driftClient.getPerpMarketAccount( - marketPosition.marketIndex - ); - const quoteSpotMarket = this.driftClient.getSpotMarketAccount( - market.quoteSpotMarketIndex - ); - const quoteOraclePriceData = this.getOracleDataForSpotMarket( - market.quoteSpotMarketIndex - ); - const oraclePriceData = this.getOracleDataForPerpMarket( - market.marketIndex - ); - - // Worst-case perp liability and weighted pnl - const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = - calculateWorstCasePerpLiabilityValue( - marketPosition, - market, - oraclePriceData.price, - includeOpenOrders - ); - - // margin ratio for this perp - const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.maxMarginRatio); - let marginRatio = new BN( - calculateMarketMarginRatio( - market, - worstCaseBaseAssetAmount.abs(), - marginCategory, - customMarginRatio, - this.isHighLeverageMode(marginCategory) || enteringHighLeverage - ) - ); - if (isVariant(market.status, 'settlement')) { - marginRatio = ZERO; - } - - // convert liability to quote value and apply margin ratio - const quotePrice = strict - ? BN.max( - quoteOraclePriceData.price, - quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min - ) - : quoteOraclePriceData.price; - let perpMarginRequirement = worstCaseLiabilityValue - .mul(quotePrice) - .div(PRICE_PRECISION) - .mul(marginRatio) - .div(MARGIN_PRECISION); - // add open orders IM - perpMarginRequirement = perpMarginRequirement.add( - new BN(marketPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) - ); - - // weighted unrealized pnl - let positionUnrealizedPnl = calculatePositionPNL( - market, - marketPosition, - true, - oraclePriceData - ); - let pnlQuotePrice: BN; - if (strict && positionUnrealizedPnl.gt(ZERO)) { - pnlQuotePrice = BN.min( - quoteOraclePriceData.price, - quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min - ); - } else if (strict && positionUnrealizedPnl.lt(ZERO)) { - pnlQuotePrice = BN.max( - quoteOraclePriceData.price, - quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min - ); - } else { - pnlQuotePrice = quoteOraclePriceData.price; - } - positionUnrealizedPnl = positionUnrealizedPnl - .mul(pnlQuotePrice) - .div(PRICE_PRECISION); - - // Add perp contribution: isolated vs cross - const isIsolated = this.isPerpPositionIsolated(marketPosition); - if (isIsolated) { - // derive isolated quote deposit value, mirroring on-chain logic - let depositValue = ZERO; - if (marketPosition.isolatedPositionScaledBalance.gt(ZERO)) { - const quoteSpotMarket = this.driftClient.getSpotMarketAccount( - market.quoteSpotMarketIndex - ); - const quoteOraclePriceData = this.getOracleDataForSpotMarket( - market.quoteSpotMarketIndex - ); - const strictQuote = new StrictOraclePrice( - quoteOraclePriceData.price, - strict - ? quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min - : undefined - ); - const quoteTokenAmount = getTokenAmount( - marketPosition.isolatedPositionScaledBalance, - quoteSpotMarket, - SpotBalanceType.DEPOSIT - ); - depositValue = getStrictTokenValue( - quoteTokenAmount, - quoteSpotMarket.decimals, - strictQuote - ); - } - calc.addIsolatedMarginCalculation( - market.marketIndex, - depositValue, - positionUnrealizedPnl, - worstCaseLiabilityValue, - perpMarginRequirement - ); - calc.addPerpLiability(); - calc.addPerpLiabilityValue(worstCaseLiabilityValue); - } else { - // cross: add to global requirement and collateral - calc.addCrossMarginRequirement( - perpMarginRequirement, - worstCaseLiabilityValue - ); - calc.addCrossMarginTotalCollateral(positionUnrealizedPnl); - const hasPerpLiability = - !marketPosition.baseAssetAmount.eq(ZERO) || - marketPosition.quoteAssetAmount.lt(ZERO) || - marketPosition.openOrders !== 0; - if (hasPerpLiability) { - calc.addPerpLiability(); - } - } - } - - return calc; - } - public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -4145,287 +3869,564 @@ export class User { ); } - const userCustomMargin = Math.max( - perpPosition.maxMarginRatio, - this.getUserAccount().maxMarginRatio - ); - const marginRatio = new BN( - calculateMarketMarginRatio( - perpMarket, - worstCaseBaseAmount.abs(), - marginCategory, - userCustomMargin, - this.isHighLeverageMode(marginCategory) - ) - ); + const userCustomMargin = Math.max( + perpPosition.maxMarginRatio, + this.getUserAccount().maxMarginRatio + ); + const marginRatio = new BN( + calculateMarketMarginRatio( + perpMarket, + worstCaseBaseAmount.abs(), + marginCategory, + userCustomMargin, + this.isHighLeverageMode(marginCategory) + ) + ); + + const _quoteOraclePriceData = + quoteOraclePriceData || + this.driftClient.getOracleDataForSpotMarket(QUOTE_SPOT_MARKET_INDEX); + + let marginRequirement = worstCaseLiabilityValue + .mul(_quoteOraclePriceData.price) + .div(PRICE_PRECISION) + .mul(marginRatio) + .div(MARGIN_PRECISION); + + marginRequirement = marginRequirement.add( + new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) + ); + + return { + marketIndex: perpMarket.marketIndex, + size: worstCaseBaseAmount, + value: worstCaseLiabilityValue, + weight: marginRatio, + weightedValue: marginRequirement, + }; + } + + public getHealthComponents({ + marginCategory, + }: { + marginCategory: MarginCategory; + }): HealthComponents { + const healthComponents: HealthComponents = { + deposits: [], + borrows: [], + perpPositions: [], + perpPnl: [], + }; + + for (const perpPosition of this.getActivePerpPositions()) { + const perpMarket = this.driftClient.getPerpMarketAccount( + perpPosition.marketIndex + ); + + const oraclePriceData = this.driftClient.getOracleDataForPerpMarket( + perpMarket.marketIndex + ); + + const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket( + QUOTE_SPOT_MARKET_INDEX + ); + + healthComponents.perpPositions.push( + this.getPerpPositionHealth({ + marginCategory, + perpPosition, + oraclePriceData, + quoteOraclePriceData, + }) + ); + + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + perpMarket.quoteSpotMarketIndex + ); + + const positionUnrealizedPnl = calculatePositionPNL( + perpMarket, + perpPosition, + true, + oraclePriceData + ); + + let pnlWeight; + if (positionUnrealizedPnl.gt(ZERO)) { + pnlWeight = calculateUnrealizedAssetWeight( + perpMarket, + quoteSpotMarket, + positionUnrealizedPnl, + marginCategory, + oraclePriceData + ); + } else { + pnlWeight = SPOT_MARKET_WEIGHT_PRECISION; + } + + const pnlValue = positionUnrealizedPnl + .mul(quoteOraclePriceData.price) + .div(PRICE_PRECISION); + + const wegithedPnlValue = pnlValue + .mul(pnlWeight) + .div(SPOT_MARKET_WEIGHT_PRECISION); + + healthComponents.perpPnl.push({ + marketIndex: perpMarket.marketIndex, + size: positionUnrealizedPnl, + value: pnlValue, + weight: pnlWeight, + weightedValue: wegithedPnlValue, + }); + } + + let netQuoteValue = ZERO; + for (const spotPosition of this.getActiveSpotPositions()) { + const spotMarketAccount: SpotMarketAccount = + this.driftClient.getSpotMarketAccount(spotPosition.marketIndex); + + const oraclePriceData = this.getOracleDataForSpotMarket( + spotPosition.marketIndex + ); + + const strictOraclePrice = new StrictOraclePrice(oraclePriceData.price); + + if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX) { + const tokenAmount = getSignedTokenAmount( + getTokenAmount( + spotPosition.scaledBalance, + spotMarketAccount, + spotPosition.balanceType + ), + spotPosition.balanceType + ); + + netQuoteValue = netQuoteValue.add(tokenAmount); + continue; + } + + const { + tokenAmount: worstCaseTokenAmount, + tokenValue: tokenValue, + weight, + weightedTokenValue: weightedTokenValue, + ordersValue: ordersValue, + } = getWorstCaseTokenAmounts( + spotPosition, + spotMarketAccount, + strictOraclePrice, + marginCategory, + this.getUserAccount().maxMarginRatio + ); + + netQuoteValue = netQuoteValue.add(ordersValue); + + const baseAssetValue = tokenValue.abs(); + const weightedValue = weightedTokenValue.abs(); + + if (weightedTokenValue.lt(ZERO)) { + healthComponents.borrows.push({ + marketIndex: spotMarketAccount.marketIndex, + size: worstCaseTokenAmount, + value: baseAssetValue, + weight: weight, + weightedValue: weightedValue, + }); + } else { + healthComponents.deposits.push({ + marketIndex: spotMarketAccount.marketIndex, + size: worstCaseTokenAmount, + value: baseAssetValue, + weight: weight, + weightedValue: weightedValue, + }); + } + } + + if (!netQuoteValue.eq(ZERO)) { + const spotMarketAccount = this.driftClient.getQuoteSpotMarketAccount(); + const oraclePriceData = this.getOracleDataForSpotMarket( + QUOTE_SPOT_MARKET_INDEX + ); + + const baseAssetValue = getTokenValue( + netQuoteValue, + spotMarketAccount.decimals, + oraclePriceData + ); + + const { weight, weightedTokenValue } = calculateWeightedTokenValue( + netQuoteValue, + baseAssetValue, + oraclePriceData.price, + spotMarketAccount, + marginCategory, + this.getUserAccount().maxMarginRatio + ); + + if (netQuoteValue.lt(ZERO)) { + healthComponents.borrows.push({ + marketIndex: spotMarketAccount.marketIndex, + size: netQuoteValue, + value: baseAssetValue.abs(), + weight: weight, + weightedValue: weightedTokenValue.abs(), + }); + } else { + healthComponents.deposits.push({ + marketIndex: spotMarketAccount.marketIndex, + size: netQuoteValue, + value: baseAssetValue, + weight: weight, + weightedValue: weightedTokenValue, + }); + } + } + + return healthComponents; + } + + /** + * Get the total position value, excluding any position coming from the given target market + * @param marketToIgnore + * @returns positionValue : Precision QUOTE_PRECISION + */ + private getTotalPerpPositionValueExcludingMarket( + marketToIgnore: number, + marginCategory?: MarginCategory, + liquidationBuffer?: BN, + includeOpenOrders?: boolean + ): BN { + const currentPerpPosition = this.getPerpPositionOrEmpty(marketToIgnore); + + const oracleData = this.getOracleDataForPerpMarket(marketToIgnore); + + let currentPerpPositionValueUSDC = ZERO; + if (currentPerpPosition) { + currentPerpPositionValueUSDC = this.getPerpLiabilityValue( + marketToIgnore, + oracleData, + includeOpenOrders + ); + } + + return this.getTotalPerpPositionLiability( + marginCategory, + liquidationBuffer, + includeOpenOrders + ).sub(currentPerpPositionValueUSDC); + } - const _quoteOraclePriceData = - quoteOraclePriceData || - this.driftClient.getOracleDataForSpotMarket(QUOTE_SPOT_MARKET_INDEX); + private getMMOracleDataForPerpMarket(marketIndex: number): MMOraclePriceData { + return this.driftClient.getMMOracleDataForPerpMarket(marketIndex); + } - let marginRequirement = worstCaseLiabilityValue - .mul(_quoteOraclePriceData.price) - .div(PRICE_PRECISION) - .mul(marginRatio) - .div(MARGIN_PRECISION); + private getOracleDataForPerpMarket(marketIndex: number): OraclePriceData { + return this.driftClient.getOracleDataForPerpMarket(marketIndex); + } - marginRequirement = marginRequirement.add( - new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) + private getOracleDataForSpotMarket(marketIndex: number): OraclePriceData { + return this.driftClient.getOracleDataForSpotMarket(marketIndex); + } + + /** + * Get the active perp and spot positions of the user. + */ + public getActivePositions(): { + activePerpPositions: number[]; + activeSpotPositions: number[]; + } { + const activePerpMarkets = this.getActivePerpPositions().map( + (position) => position.marketIndex + ); + + const activeSpotMarkets = this.getActiveSpotPositions().map( + (position) => position.marketIndex ); return { - marketIndex: perpMarket.marketIndex, - size: worstCaseBaseAmount, - value: worstCaseLiabilityValue, - weight: marginRatio, - weightedValue: marginRequirement, + activePerpPositions: activePerpMarkets, + activeSpotPositions: activeSpotMarkets, }; } - public getHealthComponents({ - marginCategory, - }: { - marginCategory: MarginCategory; - }): HealthComponents { - const healthComponents: HealthComponents = { - deposits: [], - borrows: [], - perpPositions: [], - perpPnl: [], - }; - - for (const perpPosition of this.getActivePerpPositions()) { - const perpMarket = this.driftClient.getPerpMarketAccount( - perpPosition.marketIndex - ); - - const oraclePriceData = this.driftClient.getOracleDataForPerpMarket( - perpMarket.marketIndex - ); + /** + * Compute a consolidated margin snapshot once, without caching. + * Consumers can use this to avoid duplicating work across separate calls. + */ + // TODO: need another param to tell it give it back leverage compnents + // TODO: change get leverage functions need to pull the right values from + public getMarginCalculation( + marginCategory: MarginCategory = 'Initial', + opts?: { + strict?: boolean; // mirror StrictOraclePrice application + includeOpenOrders?: boolean; + enteringHighLeverage?: boolean; + liquidationBuffer?: BN; // margin_buffer analog for buffer mode + marginRatioOverride?: number; // mirrors context.margin_ratio_override + } + ): MarginCalculation { + const strict = opts?.strict ?? false; + const enteringHighLeverage = opts?.enteringHighLeverage ?? false; + const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? + const marginBuffer = opts?.liquidationBuffer; // treat as MARGIN_BUFFER ratio if provided + const marginRatioOverride = opts?.marginRatioOverride; - const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket( - QUOTE_SPOT_MARKET_INDEX + // Equivalent to on-chain user_custom_margin_ratio + let userCustomMarginRatio = + marginCategory === 'Initial' ? this.getUserAccount().maxMarginRatio : 0; + if (marginRatioOverride !== undefined) { + userCustomMarginRatio = Math.max( + userCustomMarginRatio, + marginRatioOverride ); + } - healthComponents.perpPositions.push( - this.getPerpPositionHealth({ - marginCategory, - perpPosition, - oraclePriceData, - quoteOraclePriceData, - }) - ); + // Initialize calc via JS mirror of Rust MarginCalculation + const ctx = MarginContext.standard(marginCategory) + .strictMode(strict) + .setMarginBuffer(marginBuffer) + .setMarginRatioOverride(userCustomMarginRatio); + const calc = new MarginCalculation(ctx); - const quoteSpotMarket = this.driftClient.getSpotMarketAccount( - perpMarket.quoteSpotMarketIndex - ); + // SPOT POSITIONS + // TODO: include open orders in the worst-case simulation in the same way on both spot and perp positions + for (const spotPosition of this.getUserAccount().spotPositions) { + if (isSpotPositionAvailable(spotPosition)) continue; - const positionUnrealizedPnl = calculatePositionPNL( - perpMarket, - perpPosition, - true, - oraclePriceData + const spotMarket = this.driftClient.getSpotMarketAccount( + spotPosition.marketIndex ); - - let pnlWeight; - if (positionUnrealizedPnl.gt(ZERO)) { - pnlWeight = calculateUnrealizedAssetWeight( - perpMarket, - quoteSpotMarket, - positionUnrealizedPnl, - marginCategory, - oraclePriceData - ); - } else { - pnlWeight = SPOT_MARKET_WEIGHT_PRECISION; - } - - const pnlValue = positionUnrealizedPnl - .mul(quoteOraclePriceData.price) - .div(PRICE_PRECISION); - - const wegithedPnlValue = pnlValue - .mul(pnlWeight) - .div(SPOT_MARKET_WEIGHT_PRECISION); - - healthComponents.perpPnl.push({ - marketIndex: perpMarket.marketIndex, - size: positionUnrealizedPnl, - value: pnlValue, - weight: pnlWeight, - weightedValue: wegithedPnlValue, - }); - } - - let netQuoteValue = ZERO; - for (const spotPosition of this.getActiveSpotPositions()) { - const spotMarketAccount: SpotMarketAccount = - this.driftClient.getSpotMarketAccount(spotPosition.marketIndex); - const oraclePriceData = this.getOracleDataForSpotMarket( spotPosition.marketIndex ); - - const strictOraclePrice = new StrictOraclePrice(oraclePriceData.price); + const twap5 = strict + ? calculateLiveOracleTwap( + spotMarket.historicalOracleData, + oraclePriceData, + new BN(Math.floor(Date.now() / 1000)), + FIVE_MINUTE + ) + : undefined; + const strictOracle = new StrictOraclePrice(oraclePriceData.price, twap5); if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX) { const tokenAmount = getSignedTokenAmount( getTokenAmount( spotPosition.scaledBalance, - spotMarketAccount, + spotMarket, spotPosition.balanceType ), spotPosition.balanceType ); - - netQuoteValue = netQuoteValue.add(tokenAmount); + if (isVariant(spotPosition.balanceType, 'deposit')) { + // add deposit value to total collateral + const tokenValue = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ); + calc.addCrossMarginTotalCollateral(tokenValue); + } else { + // borrow on quote contributes to margin requirement + const tokenValueAbs = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ).abs(); + calc.addCrossMarginRequirement(tokenValueAbs, tokenValueAbs); + calc.addSpotLiability(); + } continue; } - const { - tokenAmount: worstCaseTokenAmount, - tokenValue: tokenValue, - weight, - weightedTokenValue: weightedTokenValue, - ordersValue: ordersValue, - } = getWorstCaseTokenAmounts( - spotPosition, - spotMarketAccount, - strictOraclePrice, - marginCategory, - this.getUserAccount().maxMarginRatio + // Non-quote spot: worst-case simulation + const { + tokenAmount: worstCaseTokenAmount, + ordersValue: worstCaseOrdersValue, + tokenValue: worstCaseTokenValue, + weightedTokenValue: worstCaseWeightedTokenValue, + } = getWorstCaseTokenAmounts( + spotPosition, + spotMarket, + strictOracle, + marginCategory, + userCustomMarginRatio, + includeOpenOrders + ); + + // open order IM + calc.addCrossMarginRequirement( + new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), + ZERO + ); + + if (worstCaseTokenAmount.gt(ZERO)) { + // asset side increases total collateral (weighted) + calc.addCrossMarginTotalCollateral(worstCaseWeightedTokenValue); + } else if (worstCaseTokenAmount.lt(ZERO)) { + // liability side increases margin requirement (weighted >= abs(token_value)) + const liabilityWeighted = worstCaseWeightedTokenValue.abs(); + calc.addCrossMarginRequirement( + liabilityWeighted, + worstCaseTokenValue.abs() + ); + calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); + } else if (spotPosition.openOrders !== 0) { + calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); + } + + // orders value contributes to collateral or requirement + if (worstCaseOrdersValue.gt(ZERO)) { + calc.addCrossMarginTotalCollateral(worstCaseOrdersValue); + } else if (worstCaseOrdersValue.lt(ZERO)) { + const absVal = worstCaseOrdersValue.abs(); + calc.addCrossMarginRequirement(absVal, absVal); + } + } + + // PERP POSITIONS + for (const marketPosition of this.getActivePerpPositions()) { + const market = this.driftClient.getPerpMarketAccount( + marketPosition.marketIndex + ); + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const oraclePriceData = this.getOracleDataForPerpMarket( + market.marketIndex ); - netQuoteValue = netQuoteValue.add(ordersValue); - - const baseAssetValue = tokenValue.abs(); - const weightedValue = weightedTokenValue.abs(); + // Worst-case perp liability and weighted pnl + const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = + calculateWorstCasePerpLiabilityValue( + marketPosition, + market, + oraclePriceData.price, + includeOpenOrders + ); - if (weightedTokenValue.lt(ZERO)) { - healthComponents.borrows.push({ - marketIndex: spotMarketAccount.marketIndex, - size: worstCaseTokenAmount, - value: baseAssetValue, - weight: weight, - weightedValue: weightedValue, - }); - } else { - healthComponents.deposits.push({ - marketIndex: spotMarketAccount.marketIndex, - size: worstCaseTokenAmount, - value: baseAssetValue, - weight: weight, - weightedValue: weightedValue, - }); + // margin ratio for this perp + const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.maxMarginRatio); + let marginRatio = new BN( + calculateMarketMarginRatio( + market, + worstCaseBaseAssetAmount.abs(), + marginCategory, + customMarginRatio, + this.isHighLeverageMode(marginCategory) || enteringHighLeverage + ) + ); + if (isVariant(market.status, 'settlement')) { + marginRatio = ZERO; } - } - if (!netQuoteValue.eq(ZERO)) { - const spotMarketAccount = this.driftClient.getQuoteSpotMarketAccount(); - const oraclePriceData = this.getOracleDataForSpotMarket( - QUOTE_SPOT_MARKET_INDEX + // convert liability to quote value and apply margin ratio + const quotePrice = strict + ? BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ) + : quoteOraclePriceData.price; + let perpMarginRequirement = worstCaseLiabilityValue + .mul(quotePrice) + .div(PRICE_PRECISION) + .mul(marginRatio) + .div(MARGIN_PRECISION); + // add open orders IM + perpMarginRequirement = perpMarginRequirement.add( + new BN(marketPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) ); - const baseAssetValue = getTokenValue( - netQuoteValue, - spotMarketAccount.decimals, + // weighted unrealized pnl + let positionUnrealizedPnl = calculatePositionPNL( + market, + marketPosition, + true, oraclePriceData ); - - const { weight, weightedTokenValue } = calculateWeightedTokenValue( - netQuoteValue, - baseAssetValue, - oraclePriceData.price, - spotMarketAccount, - marginCategory, - this.getUserAccount().maxMarginRatio - ); - - if (netQuoteValue.lt(ZERO)) { - healthComponents.borrows.push({ - marketIndex: spotMarketAccount.marketIndex, - size: netQuoteValue, - value: baseAssetValue.abs(), - weight: weight, - weightedValue: weightedTokenValue.abs(), - }); + let pnlQuotePrice: BN; + if (strict && positionUnrealizedPnl.gt(ZERO)) { + pnlQuotePrice = BN.min( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); + } else if (strict && positionUnrealizedPnl.lt(ZERO)) { + pnlQuotePrice = BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); } else { - healthComponents.deposits.push({ - marketIndex: spotMarketAccount.marketIndex, - size: netQuoteValue, - value: baseAssetValue, - weight: weight, - weightedValue: weightedTokenValue, - }); + pnlQuotePrice = quoteOraclePriceData.price; } - } - - return healthComponents; - } - - /** - * Get the total position value, excluding any position coming from the given target market - * @param marketToIgnore - * @returns positionValue : Precision QUOTE_PRECISION - */ - private getTotalPerpPositionValueExcludingMarket( - marketToIgnore: number, - marginCategory?: MarginCategory, - liquidationBuffer?: BN, - includeOpenOrders?: boolean - ): BN { - const currentPerpPosition = this.getPerpPositionOrEmpty(marketToIgnore); - - const oracleData = this.getOracleDataForPerpMarket(marketToIgnore); + positionUnrealizedPnl = positionUnrealizedPnl + .mul(pnlQuotePrice) + .div(PRICE_PRECISION); - let currentPerpPositionValueUSDC = ZERO; - if (currentPerpPosition) { - currentPerpPositionValueUSDC = this.getPerpLiabilityValue( - marketToIgnore, - oracleData, - includeOpenOrders - ); + // Add perp contribution: isolated vs cross + const isIsolated = this.isPerpPositionIsolated(marketPosition); + if (isIsolated) { + // derive isolated quote deposit value, mirroring on-chain logic + let depositValue = ZERO; + if (marketPosition.isolatedPositionScaledBalance.gt(ZERO)) { + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const strictQuote = new StrictOraclePrice( + quoteOraclePriceData.price, + strict + ? quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + : undefined + ); + const quoteTokenAmount = getTokenAmount( + marketPosition.isolatedPositionScaledBalance, + quoteSpotMarket, + SpotBalanceType.DEPOSIT + ); + depositValue = getStrictTokenValue( + quoteTokenAmount, + quoteSpotMarket.decimals, + strictQuote + ); + } + calc.addIsolatedMarginCalculation( + market.marketIndex, + depositValue, + positionUnrealizedPnl, + worstCaseLiabilityValue, + perpMarginRequirement + ); + calc.addPerpLiability(); + calc.addPerpLiabilityValue(worstCaseLiabilityValue); + } else { + // cross: add to global requirement and collateral + calc.addCrossMarginRequirement( + perpMarginRequirement, + worstCaseLiabilityValue + ); + calc.addCrossMarginTotalCollateral(positionUnrealizedPnl); + const hasPerpLiability = + !marketPosition.baseAssetAmount.eq(ZERO) || + marketPosition.quoteAssetAmount.lt(ZERO) || + marketPosition.openOrders !== 0; + if (hasPerpLiability) { + calc.addPerpLiability(); + } + } } - return this.getTotalPerpPositionLiability( - marginCategory, - liquidationBuffer, - includeOpenOrders - ).sub(currentPerpPositionValueUSDC); - } - - private getMMOracleDataForPerpMarket(marketIndex: number): MMOraclePriceData { - return this.driftClient.getMMOracleDataForPerpMarket(marketIndex); - } - - private getOracleDataForPerpMarket(marketIndex: number): OraclePriceData { - return this.driftClient.getOracleDataForPerpMarket(marketIndex); - } - - private getOracleDataForSpotMarket(marketIndex: number): OraclePriceData { - return this.driftClient.getOracleDataForSpotMarket(marketIndex); + return calc; } - /** - * Get the active perp and spot positions of the user. - */ - public getActivePositions(): { - activePerpPositions: number[]; - activeSpotPositions: number[]; - } { - const activePerpMarkets = this.getActivePerpPositions().map( - (position) => position.marketIndex - ); - - const activeSpotMarkets = this.getActiveSpotPositions().map( - (position) => position.marketIndex - ); - - return { - activePerpPositions: activePerpMarkets, - activeSpotPositions: activeSpotMarkets, - }; - } private isPerpPositionIsolated(perpPosition: PerpPosition): boolean { return (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0; } From a1a015775f9bc741809aa6379934b20423b9c90c Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 28 Oct 2025 15:40:48 -0600 Subject: [PATCH 209/247] fix: bug with max amount withdrawal for transfer iso perp --- sdk/src/driftClient.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 0263760696..036ff0015c 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -4170,17 +4170,18 @@ export class DriftClient { subAccountId?: number, txParams?: TxParams ): Promise { - const { txSig } = await this.sendTransaction( - await this.buildTransaction( - await this.getTransferIsolatedPerpPositionDepositIx( - amount, - perpMarketIndex, - subAccountId - ), - txParams + const tx =await this.buildTransaction( + await this.getTransferIsolatedPerpPositionDepositIx( + amount, + perpMarketIndex, + subAccountId ), + txParams + ) + const { txSig } = await this.sendTransaction( + tx, [], - this.opts + {...this.opts, skipPreflight: true} ); return txSig; } @@ -4207,9 +4208,9 @@ export class DriftClient { readablePerpMarketIndex: [perpMarketIndex], }); - const amountWithBuffer = noAmountBuffer + const amountWithBuffer = noAmountBuffer || amount.eq(BigNum.fromPrint('-9223372036854775808').val) ? amount - : amount.add(amount.div(new BN(1000))); // .1% buffer + : amount.add(amount.mul(new BN(1000))); // .1% buffer return await this.program.instruction.transferIsolatedPerpPositionDeposit( spotMarketIndex, @@ -4282,6 +4283,8 @@ export class DriftClient { const amountToWithdraw = amount.gt(depositAmountPlusUnrealizedPnl) ? BigNum.fromPrint('-9223372036854775808').val // min i64 : amount; + console.log("amountToWithdraw", amountToWithdraw.toString()); + console.log("amount", amount.toString()); let associatedTokenAccount = userTokenAccount; if (!associatedTokenAccount) { From 1fb6e523e23b5ca3d23c20883e843c766d518472 Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:02:23 -0700 Subject: [PATCH 210/247] DLP (#1885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bigz/init lp pool (#1884) * program: init lp pool * cargo fmt -- * add total fee fields * add update_target_weights math * program: use sparse matrix for constituent map and update tests * zero copy accounts, init ix (#1578) * update accounts (#1580) * zero copy + permissionless crank ixs (#1581) * program: support negative target weights for borrow-lend * fix tests to work with zero copy * few comment changes * remove discriminator from impl macro * add get_swap_amount, get_swap_fees, get_weight (#1579) * add get_swap_amount, get_swap_fees, get_weight * update accounts * add back ts * rebase * add constituent swap fees * fix swap fee calc (#1582) * add init amm mapping to lp context (#1583) * init constituent * add initializeLpPool test (#1585) * add initializeLpPool test * add check for constituent target weights * add add datum ix * add init tests and invariant checks * rename data to more useful names * dlp use spl token program (#1588) * add crank ix * update total_weight for validation_flags check * push test so far * overriding perp position works * remove message * fix dup total_weight add * constituent map remaining accounts * compiles * bankrun tests pass * compiles but casting failure in overflow protection test * address comment and change token arguments from u64 to u128 * bankrun tests pass * init constituent token account (#1596) * update aum calc * add update /remove mapping ixs * fix test - init constituent spot market * add crank improvements * passes tests * precision fix crank aum * precision fixes and constituent map check for account owner * add passthrough account logic (#1602) * add passthrough account logic * cant read yet * fix all zc alignment issues * make oracle source a u8 on zc struct * Wphan/dlp-swap-ixs (#1592) * add lp_swap ix * rebase * test helpers * swap works * fix swaps, add more cargo tests for fees n swap amt * remove console.logs * address PR comments * merge upstream * post-merge fixes * store bumps on accounts (#1604) * store bumps on accounts * do pda check in constituent map * address comments * Wphan/add liquidity (#1607) * add add remove liquidity fees calc * add liquidity ix * fix init mint and lppool token account, refactor test fees * add removeLiquidity bankrun test * merge upstream * add LPPool.next_mint_redeem_id * program: lp-pool-to-use-target-base-vector (#1615) * init lp pool target-base matrix * working target-base logic * add todos for add/remove liquidity aum * add renames + fix test * add beta and cost to trade in bps to target datum * add more tests * add fields to LP events, fix tests (#1620) * add fields to LP events, fix tests * revert target weight calc * add constituent.next_swap_id, fix cost_to_trade math * dlp jup swap (#1636) * dlp jup swap * add admin client ixs * almost fixed * test working? * update begin and end swap * tweaks * fix math on how much was swapped * remove unnecessary lp pool args * extra account validation * added token account pda checks in other ixs * stablecoin targets (#1638) * is stablecoin * address comments --------- Co-authored-by: Chris Heaney * cleanup * transfer oracle data ix to constituent (#1643) * transfer oracle data ix to constituent * add lib entrypoint * simplify more * add spot market constraint * big cargo test (#1644) * derivative constituents + better testing + bug fixes (#1657) * all tests technically pass * update tests + prettify * bug fixes and tests pass * fix many bugs and finalize logic * deposit/borrow working and changing positions (#1652) * sdk: allow custom coder * program: dlp add upnl for settles to amm cache (#1659) * program: dlp add-upnl-for-settles-to-amm-cache * finish up lp pool transfer from perp market * add amount_to_transfer using diff * merge * add pnl and fee pool accounting + transfer from dlp to perp market --------- Co-authored-by: Nour Alharithi * remove unused accounts coder * move customCoder into sdk, lint * testing: ix: settle perp to dlp, insufficient balance edge case and improvements (#1688) * finish edge case test * aum check also passes * prettify * added more settle test coverage and squash bugs (#1689) * dlp: add constituentMap (#1699) * Nour/gauntlet fee impl (#1698) * added correlation matrix infra * refactor builds * mint redeem handled for usdc * remove liquidity also should work * all tests pass * bankrun tests pass too * update aum considers amm cache (#1701) * prettify (#1702) * Wphan/merge master dlp (#1703) * feat: init swift user orders on user account creation if needed * fix: wrong pushing of swift user orders ixs * fix: broken swift tests * fix: swift -> signed msg * refactor(sdk): update jupiter's api url * fix(sdk): remove error thrown * indicative qutoes server changes * sdk: release v2.121.0-beta.7 * sdK: update market index 33 oracle rr (#1606) * sdk: add to spot constants market index 34 * revert adminClient.ts change * sdk: update spot market constants oracle index 33 * sdk: release v2.121.0-beta.8 * sdk: high leverage mode updates (#1605) * sdk: high leverage mode updates * add optional param for fee calc * update changelog * sdk: release v2.121.0-beta.9 * getPlaceSignedMsgTakerPerpOrderIxs infer HLM mode from bitflags (#1608) * sdk: release v2.121.0-beta.10 * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs (#1610) * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs * bankrun test * sdk: release v2.121.0-beta.11 * sdk: round tick/step size for getVammL2Generateor (#1612) * sdk: round tick/step size for etVammL2Generateor * use standard functions, include in all fcns * fix const declare, rm whitespace * fix posdir sign * sdk: release v2.121.0-beta.12 * sdk: release v2.121.0-beta.13 * sdk: constants market-index-45-46 (#1618) * sdk: release v2.121.0-beta.14 * robustness check for indicative quotes sender (#1621) * robustness check for indicative quotes sender * delete quote from market index of bad quote * sdk: release v2.121.0-beta.15 * Added launchTs for ZEUS, zBTC * sdk: release v2.121.0-beta.16 * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622) * sdk: release v2.121.0-beta.17 * sdk: fix vamm l2 generator base swapped (#1623) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * sdk: release v2.121.0-beta.18 * sdk: revert vamm l2 gen (#1624) * Revert "sdk: fix vamm l2 generator base swapped (#1623)" This reverts commit 56bc78d70e82cb35a90f12f73162bffb640cb655. * Revert "sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622)" This reverts commit e49cfd554cc44cd8d7770184f02f6ddb0bfc92f1. * Revert "sdk: round tick/step size for getVammL2Generateor (#1612)" This reverts commit f932a4ea2afcae314e406b7c7ee35e55b36043ad. * sdk: release v2.121.0-beta.19 * sdk: show protected-asset have zero-borrow-limit (#1603) * sdk: show protected-asset have zero-borrow-limit * rm unused AssetTier import * sdk: release v2.121.0-beta.20 * sdk: market-constants-index-74 (#1629) * sdk: release v2.121.0-beta.21 * program: use saturating_sub for number_of_users (#1616) * program: use saturating_sub for number_of_users * update CHANGELOG.md * program: allow fixing hlm num users (#1630) * sdk: release v2.121.0-beta.22 * sdk: fix switchboard on demand client to use landed at * sdk: release v2.121.0-beta.23 * sdk: spot-market-poolid-4 constants (#1631) * sdk: release v2.121.0-beta.24 * fix high lev mode liq price (#1632) * sdk: release v2.121.0-beta.25 * replace deprecated solana install scripts (#1634) * sdk: release v2.121.0-beta.26 * refactor(sdk): use ReturnType for Timeout types (#1637) * sdk: release v2.121.0-beta.27 * auction price sdk fix * sdk: release v2.121.0-beta.28 * program: multi piecewise interest rate curve (#1560) * program: multi-piecewise-interest-rate-curve * update tests * widen out borrow limits/healthy util check * add break, use array of array for borrow slope segments * program: fix cargo test * sdk: add segmented IR curve to interest rate calc * clean up unusded var, make interest rate segment logic a const * incorp efficiency feedback points * test: add sol realistic market example * cargo fmt -- * CHANGELOG --------- Co-authored-by: Chris Heaney * sdk: release v2.121.0-beta.29 * program: allow hot admin to update market fuel params (#1640) * v2.121.0 * sdk: release v2.122.0-beta.0 * sdk: fix nullish coalescing * sdk: release v2.122.0-beta.1 * program: add logging for wrong perp market mutability * sdk: check free collateral change in maxTradeSizeUsdcForPerp (#1645) * sdk: check free collateral change in maxTradeSizeUsdcForPerp * update changelog * sdk: release v2.122.0-beta.2 * refactor(sdk): emit newSlot event on initial subscribe call (#1646) * sdk: release v2.122.0-beta.3 * sdk: spot-market-constants-pool-id-2 (#1647) * sdk: release v2.122.0-beta.4 * sdk: add-spot-market-index-52-constants (#1649) * sdk: release v2.122.0-beta.5 * program: add existing position fields to order records (#1614) * program: add quote entry amount to order records * fix cargo fmt and test * more reusable code * more reusable code * add another comment * fix math * account for pos flip * fix typo * missed commit * more fixes * align naming * fix typo * CHANGELOG * program: check limit price after applying buffer in trigger limit ord… (#1648) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * fix tests * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: fix cargo tests * program: check limit price when setting auction for limit order (#1650) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * program: check limit price when setting limit auction params * cargo fmt -- * fix CHANGELOG * tests: updates switchboardTxCus.ts * program: try to fix iteration for max order size (#1651) * Revert "program: try to fix iteration for max order size (#1651)" This reverts commit 3f0eab39ed23fa4a9c41cbab9af793c60b50a239. * disable debug logging in bankrun tests * v2.122.0 * sdk: release v2.123.0-beta.0 * sdk: constants-spot-market-index-53 (#1655) * sdk: release v2.123.0-beta.1 * sdk: idl for new existing position order action records * fix: protocol test prettier fix * make ci lut checks not shit * sdk: release v2.123.0-beta.2 * sdk: fix vamm l2 generator base swapped and add new top of book (#1626) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * use proper quoteAmount with baseSwap for top of book orders * clean up console.log * sdk: getVammL2Generator reduce loc (#1628) * sdk: getVammL2Generator-reduce-loc * add MAJORS_TOP_OF_BOOK_QUOTE_AMOUNTS * add marketindex check topOfBookAmounts * yarn lint/prettier * sdk: release v2.123.0-beta.3 * program: allow all limit orders to go through swift (#1661) * program: allow all limit orders to go through swift * add anchor test * CHANGELOG * sdk: add optional initSwiftAccount on existing account deposits (#1660) * sdk: release v2.123.0-beta.4 * program: add taker_speed_bump_override and amm_spread_adjustment * Revert "program: add taker_speed_bump_override and amm_spread_adjustment" This reverts commit 1e19b7e7a6c5cecebdbfb3a9e224a0d4471ba6d2. * program: tests-fee-adjustment-neg-100 (#1656) * program: tests-fee-adjustment-neg-100 * add HLM field to test * cargo fmt -- --------- Co-authored-by: Chris Heaney * program: simplify user can skip duration (#1668) * program: simplify user can skip duration * update context * CHANGELOG * fix test * fix pmm tests --------- Co-authored-by: Chris Heaney * program: add taker_speed_bump_override and amm_spread_adjustment (#1665) * program: add taker_speed_bump_override and amm_spread_adjustment * add admin client * cargo test * add impl for amm_spread_adjustment * ensure no overflows * CHANGELOG * cargo fmt -- * sdk types * prettify --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: update-amm-spread-and-availability-constraints (#1663) * program: update-amm-spread-and-availability-constraints * fix cargo tests * program: use saturating mul for amm spread adj * nour/indic-quotes-sender-v2 (#1667) * nour/indic-quotes-sender-v2 * prettify * pass margin category into calculateEntriesEffectOnFreeCollateral (#1669) * fix cargo test * tests: fix oracle guardrail test * sdk: update idl * yarn prettify:fix * tests: fix a few more place and make tests * prettify fix * whitespace readme change * sdk: release v2.123.0-beta.5 * v2.123.0 * sdk: release v2.124.0-beta.0 * v2.123.0-1 * sdk: calculateVolSpreadBN-sync (#1671) * sdk: release v2.124.0-beta.1 * sdk: calculate-spread-bn-add-amm-spread-adjustment (#1672) * sdk: calculate-spread-bn-add-amm-spread-adjustment * corect sign * add math max 1 * prettify * sdk: release v2.124.0-beta.2 * sdk: correct calculateVolSpreadBN reversion * sdk: release v2.124.0-beta.3 * sdk: add getTriggerAuctionStartPrice (#1654) * sdk: add getTriggerAuctionStartPrice * updates * precisions * remove startBuffer param --------- Co-authored-by: Chris Heaney * sdk: release v2.124.0-beta.4 * feat: customized cadence account loader (#1666) * feat: customized cadence account loader bby * feat: method to read account cadence on custom cadence account loader * feat: PR feedback on customized loader cleaup code and better naming * fix: lint and prettify * feat: more efficient rpc polling on custom polling intervals * feat: custom cadence acct loader override load * chore: prettify * sdk: release v2.124.0-beta.5 * sdk: sync-user-trade-tier-calcs (#1673) * sdk: sync-user-trade-tier-calcs * prettify --------- Co-authored-by: Nick Caradonna * sdk: release v2.124.0-beta.6 * sdk: add new admin client fn * Revert "sdk: add new admin client fn" This reverts commit c7a4f0b174858048bd379f2f2bb0e63595949921. * sdk: release v2.124.0-beta.7 * refactor(ui): add callback logic, fix polling frequency update * sdk: release v2.124.0-beta.8 * program: less order param sanitization for long tail perps (#1680) * program: allow-auction-start-buffer-on-tail-mkt * fix test * cargo fmt -- * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * Wphan/custom coder (#1682) * sdk: allow custom coder * remove unused accounts coder * linter * move customCoder into sdk, lint * update test helpers * update testhelpers.ts * sdk: release v2.124.0-beta.9 * update sdk exports * sdk: release v2.124.0-beta.10 * sdk: safer-calculate-spread-reserve-math (#1681) * sdk: release v2.124.0-beta.11 * update getMaxLeverageForPerp to use usdc logic (#1678) * sdk: release v2.124.0-beta.12 * program: override for oracle delay (#1679) * programy: override for oracle delay * update impl * switch to i8 * CHANGELOG * program: programmatic rebalance between protocol owned if holdings (#1653) * program: if swap * program: add initial config * add update * more * moar * moar * moar * program: update how swap epoch works * add test * add an invariant * cargo fmt -- * add transfer to rev pool * add mint validation * cargo fmt -- * track in amount between tranfsers * add to ci tests * separate key * program: always transfer max amount to rev pool * CHANGELOG * sdk: release v2.124.0-beta.13 * sdk: improve-aclient-accounts-logic (#1684) * sdk: release v2.124.0-beta.14 * program: improve-amm-spread-validates (#1685) * program: let hot wallet update amm jit intensity * sdk: hot wallet can update amm jit intensity * program: hot wallet can update curve intensity * program: fix build * sdk: update idl * sdk: release v2.124.0-beta.15 * v2.124.0 * sdk: release v2.125.0-beta.0 * program: three-point-std-estimator (#1686) * program: three-point-std-estimator * update tests and add sdk * update changelog * sdk: add-updatePerpMarketOracleSlotDelayOverride (#1691) * sdk: release v2.125.0-beta.1 * program: add-amm-inventory-spread-adjustment-param (#1690) * program: add-amm-inventory-spread-adjustment-param * cargo fmt -- * update sdk * prettier * fix syntax { --------- Co-authored-by: Chris Heaney * program: max-apr-rev-settle-by-spot-market (#1692) * program: max-apr-rev-settle-by-spot-market * update max * default to u128 to avoid casts * changelog * sdk: release v2.125.0-beta.2 * program: better account for imf in calculate_max_perp_order_size (#1693) * program: better account for imf in calculate_max_perp_order_size * CHANGELOG * v2.125.0 * sdk: release v2.126.0-beta.0 * sdk: only count taker fee in calculateEntriesEffectOnFreeCollateral for maintenance (#1694) * sdk: release v2.126.0-beta.1 * Separate getAddInsuranceFundStakeIxs (#1695) * sdk: release v2.126.0-beta.2 * idl: amm-inv-adj-latest-idl (#1697) * sdk: release v2.126.0-beta.3 * sdk: spot-market-index-54 constants (#1696) * sdk: release v2.126.0-beta.4 * sdk: update spot market index 54 pythlazer id * sdk: release v2.126.0-beta.5 * Update spotMarkets.ts * sdk: release v2.126.0-beta.6 * prettify --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: Nour Alharithi Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: lil perp Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> * slot staleness checks (#1705) * slot staleness checks * update aum ix to use constituent oracles * Nour/derivative constituent testing (#1708) * slot staleness checks * update aum ix to use constituent oracles * constituent test works when adjusting derivative index * constituent depeg kill switch works * works with multiple derivatives on the same parent * remove incorrect usage of nav * fix adminClient and tests * Nour/fee grid search testing (#1714) * grid search * grid search swap test * Nour/address comments (#1715) * low hanging fruit comments * remove pda checks and store lp pool on zero copy accounts * parameterize depeg threshold * make description in lp pool event * update idl for event change * add swap fee unit tests (#1713) * add swap fee unit tests * remove linear inventory fee component * Nour/settle accounting (#1723) * fixing the main settle test and settle function * all current tests pass * update msg occurrences * dont update lp quote owed unless collateralized * Nour/settle testing (#1725) * refactor settle pnl to modularize and add tests * more cargo tests * prettify * Nour/address more comments (#1726) * use oracle staleness threshold for staleness * add spot market vault invariant * refactor update_aum, add unit tests (#1727) * refactor update_aum, add unit tests * add constituent target base tests * update doc * Nour/parameterize dlp (#1731) * add validates and test for withdraw limit * settlement max * update idl * merge conflicts * fixes * update idl * bug fixes * mostly sdk fixes * bug fixes * bug fix and deploy script * program: new amm oracle (#1738) * zero unused amm fields * cargo fmt * bare bones ix * minimal anchor mm oracle impl * update test file * only do admin validate when not anchor test * updates * generalize native entry * fix weird function name chop off * make it compile for --feature cpi (#1748) Co-authored-by: jordy25519 * more efficeint clock and state bit flags check * vamm uses mm oracle (#1747) * add offset * working tests * refactor to use MM oracle as its own type * remove weird preface * sdk updates * bankrun tests all pass * fix test * changes and fixes * widen confidence if mm oracle too diff * sdk side for confidence adjust * changelog * fix lint * fix cargo tests * address comments * add conf check * remove anchor ix and cache oracle confidence * only state admin can reenable mm oracle kill switch * cargo fmt --------- Co-authored-by: jordy25519 * fix tests (#1764) * Nour/move ixs around (#1766) * move around ixs * remove message * add devnet oracle crank wallet * refactored mm oracle * sdk changes + cargo fmt * fix tests * validate price bands with fill fix * normalize fill within price bands * add sdk warning * updated type * undefined guard so anchor tests pass * accept vec for update amm and view amm * adjust test to work with new price bands * Revert "adjust test to work with new price bands" This reverts commit ee40ac8799fa2f6222ea7d0e9b3e07014346a699. * remove price bands logic * add zero ix for mm oracle for reset * add new drift client ix grouping * v1 safety improvements * isolate funding from MM oracle * add cargo tests for amm availability * change oracle validity log bool to enum * address comment * make validate fill direction agnostic * fix liquidate borrow for perp pnl test * fix tests and address comments * commit constituent map to barrel file * add lp fields to perp market account * rearrange perp market struct for lp fields * bug fix for notional position tracking * view function * fee view functions * max aum + whitelist check and removing get_mint_redeem_fee for now * add wsol support for add liquidity * fix sdk and typing bugs * update lp pool params ix * admin override cache and disable settle functions * devnet swap working * dlp taker discovered bug fixes and sdk changes * refactor last settle ts to last settle slot * Nour/settle pnl fix (#1817) * settle perp to lp pool bug fixes * update bankrun test to not use admin fee pool deposit * fix tests using update spot market balances too * add log msgs for withdraw and fix casting bug * check in for z (#1823) * feat: option for custom oracle ws subscriber * fix: pass custom oracle ws sub option in dc constructor * sdk: add spot-market-index-57 to constants (#1815) * sdk: release v2.134.0-beta.2 * lazer oracle migration (#1813) * lazer oracle migration * spot markets too * sdk: release v2.134.0-beta.3 * sdk: release v2.134.0-beta.4 * program: settle pnl invariants (#1812) * program: settle pnl invariants * add test * fix lint * lints * add msg * CHANGELOG * cargo fmt -- * program: add_update_perp_pnl_pool (#1810) * program: add_update_perp_pnl_pool * test * CHANGELOG * sdk: release v2.134.0-beta.5 * program: update-mark-twap-integer-bias (#1783) * program: update-mark-twap-integer-bias * changelog update * program: update-fee-tier-determine-fix5 (#1800) * program: update-fee-tier-determine-fix5 * update changelog * program: update-mark-twap-crank-use-5min-basis (#1769) * program: update-mark-twap-crank-use-5min-basis * changelog * program: update-min-margin-const-limit (#1802) * program: update-min-margin-const-limit * add CHANGELOG.md * sdk: release v2.134.0-beta.6 * program: rm-burn-lp-shares-invariant (#1816) * program: rm-burn-lp-shares-invariant * update changelog * fix test and cargo fmt * fix anchor tests * yarn prettify:fix * reenable settle_pnl mode test * v2.134.0 * sdk: release v2.135.0-beta.0 * Merge pull request #1820 from drift-labs/chester/fix-zod * sdk: release v2.135.0-beta.1 * mm oracle sdk change (#1806) * mm oracle sdk change * better conditional typing * DLOB bug fix * updated idl * rm getAmmBidAskPrice * sdk: release v2.135.0-beta.2 * sdk: fix isHighLeverageMode * sdk: release v2.135.0-beta.3 * refactor(sdk): add update delegate ix method, ovrride authority for settle multiple pnl (#1822) * check in for z * more logging changes * mm oracle sdk additions (#1824) * strict typing for more MM oracle contact points * add comments to auction.ts * prettify * sdk: release v2.135.0-beta.4 * init constituent bug fix and type change * add in invariant to be within 1 bp of balance before after * add strict typing for getPrice and new auction trigger function (#1826) * add strict typing for getPrice and new auction trigger function * refactor getTriggerAuctionStartAndExecutionPrice * sdk: release v2.135.0-beta.5 * update tests and enforce atomic settles for withdraw * add failing withdraw test * withdraw fix * bring diff in validate back to 1 * make lp pool test fail * better failed test * only check after < before, do to spot precision limits * add balance check to be < 1 cent --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: LukasDeco Co-authored-by: lil perp Co-authored-by: wphan Co-authored-by: Chester Sim * add price for lp validates (#1833) * add missing token account reloads and syncs * add disabled lp pool swaps by default * more extensive aum logging * Wphan/merge-builder-codes (#1842) * add RevenueShare and RevenueShareEscrow accounts an init ixs * fix multiple array zc account, and handling different message types in place_signed_msg_taker_order * decoding error * recording orders in RevenueShareEscrow workin * cancel and fill orders * idl * fix sdk build * fix math * update RevenueShareOrder bitflags, store builder_idx instead of pubkey * merge RevenueShareOrders on add * remove builder accounts from cancel ixs, wip settle impl * dont fail settlpnl if no builder users provided * finish settle, rename RevenueShare->Builder, RevenueShareEscrow->BuilderEscrow * feat: option for custom oracle ws subscriber * fix: pass custom oracle ws sub option in dc constructor * sdk: add spot-market-index-57 to constants (#1815) * sdk: release v2.134.0-beta.2 * lazer oracle migration (#1813) * lazer oracle migration * spot markets too * sdk: release v2.134.0-beta.3 * sdk: release v2.134.0-beta.4 * program: settle pnl invariants (#1812) * program: settle pnl invariants * add test * fix lint * lints * add msg * CHANGELOG * cargo fmt -- * program: add_update_perp_pnl_pool (#1810) * program: add_update_perp_pnl_pool * test * CHANGELOG * sdk: release v2.134.0-beta.5 * program: update-mark-twap-integer-bias (#1783) * program: update-mark-twap-integer-bias * changelog update * program: update-fee-tier-determine-fix5 (#1800) * program: update-fee-tier-determine-fix5 * update changelog * program: update-mark-twap-crank-use-5min-basis (#1769) * program: update-mark-twap-crank-use-5min-basis * changelog * program: update-min-margin-const-limit (#1802) * program: update-min-margin-const-limit * add CHANGELOG.md * sdk: release v2.134.0-beta.6 * program: rm-burn-lp-shares-invariant (#1816) * program: rm-burn-lp-shares-invariant * update changelog * fix test and cargo fmt * fix anchor tests * yarn prettify:fix * reenable settle_pnl mode test * v2.134.0 * sdk: release v2.135.0-beta.0 * add more bankrun tests, clean up * clean up, fix tests * why test fail * add subaccountid to BuilderOrder * reduce diff * add referrals * add test can fill settle user with no builderescrow * add referral builder feature flag and referral migration method * fix cargo tests, try fix bankrun test timing issue * Merge pull request #1820 from drift-labs/chester/fix-zod * sdk: release v2.135.0-beta.1 * mm oracle sdk change (#1806) * mm oracle sdk change * better conditional typing * DLOB bug fix * updated idl * rm getAmmBidAskPrice * sdk: release v2.135.0-beta.2 * sdk: fix isHighLeverageMode * sdk: release v2.135.0-beta.3 * refactor(sdk): add update delegate ix method, ovrride authority for settle multiple pnl (#1822) * mm oracle sdk additions (#1824) * strict typing for more MM oracle contact points * add comments to auction.ts * prettify * sdk: release v2.135.0-beta.4 * add strict typing for getPrice and new auction trigger function (#1826) * add strict typing for getPrice and new auction trigger function * refactor getTriggerAuctionStartAndExecutionPrice * sdk: release v2.135.0-beta.5 * sdk: handle unfillable reduce only orders (#1790) * sdk: handle unfillable reduce only orders * fix dlob tests build errors * fix some test build errors * sdk: release v2.135.0-beta.6 * ref price offset amm math fix (#1828) * ref price offset amm math fix * add latest slot optional var to callers of update amm spread * sdk: release v2.135.0-beta.7 * latest slot as argument to getL2 (#1829) * latest slot as argument to getL2 * add comment * update BN import * sdk: release v2.135.0-beta.8 * add SignedMsgOrderParamsMessageV2 * program: trigger price use 5min mark price (#1830) * program: trigger price use 5min mark price * cargo fmt -- --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * v2.135.0 * sdk: release v2.136.0-beta.0 * zero pad swift messages to make backwards compatible * PR feedback * update tests/placeAndMakeSignedMsgBankrun.ts to handle client side errors * lukas/websocket improvements (#1807) * feat: initial implementation for users and markets WS improvements * lukas/gill websocket sub (#1781) * websockets gill temp * feat: feature parity between gill version ws acct sub and reg one + optional passing into driftClient * fix: post rebase bugs and cleanup * chore: websocket account subscriber export * feat: logging string update on ws acct v2 * rm: useless logging * chore: cleanup ws subscriber v2 docs * chore: specific name on custom ws acct sub param * fix: post rebase again cleanup * fix: prettier fixed * feat: initial implementation for users and markets WS improvements * feat: polling check on websocket acct subscriber v2 + naming * fix: lint * fix: non-hanging WS subscription async loop handling * fix: bugs with program ws subs hanging on asynciter * fix: goofy self imports * feat: initial batch fetching temp * temp: sub second WS subscribe time * fix: ws program account subscriber v2 bugs and optimizations * feat: chunk stuff account requests * feat: more subscribe optimizations ws driftclient sub v2 * chore: cleanup ws sub v2 logs * feat: conditional check on using ws account subscriber + unused * fix: bad import * chore: add export of WebSocketProgramAccountSubscriberV2 * fix: unneeded drift idl export messing up common build * fix: consolidate rpc ws subscriptions for oracles * feat: docs for ws v2 and cleanup * chore: more docs on ws acct susbcriber v2 * feat: PR feedback round 2 * fix: default timeout for ws v2 susbcribers * feat: PR feedback on resubOpts and simplify logic * fix: prettier * sdk: release v2.136.0-beta.1 * refactor(sdk): add decimal override for bignum prettyPrint * sdk: release v2.136.0-beta.2 * sdk: while valid tx sender memory leak fix * sdk: release v2.136.0-beta.3 * refactor account logic for borrows * remove double fee count, update tests to check filled position and quote amounts fda * rename Builder -> RevenueShare * add test check accumulated builder/ref fees * fix settle multiple pnl accounts, test ref rewards in multiple markets * [ FIX ] `posaune0423/fix tx fee payer` (#1837) * sdk: release v2.136.0-beta.4 * sdk: add constant for spot market index 58 (#1840) * sdk: add spot market constant 58 * revert .sh * sdk: release v2.136.0-beta.5 * Revert "[ FIX ] `posaune0423/fix tx fee payer` (#1837)" (#1841) This reverts commit 8cc07e0e179d4335fbb47f8aef5ae022b7143550. * sdk: release v2.136.0-beta.6 * express builder fees in tenth of bps * update referral migration params * PR feedback * add builder code feature gate * fix tests * add referral fields * run all tests * kickoff build * disable extra instructions, fix builder code feature flag selection --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: moosecat Co-authored-by: LukasDeco Co-authored-by: lil perp Co-authored-by: Chester Sim Co-authored-by: asuma * calc aum bug fix for borrows * idl changes and drift client working * Wphan/dlp revert builder codes (#1854) * Revert "Wphan/merge-builder-codes (#1842)" This reverts commit c999f83e000436e34ce4cde17700521d24057208. * fix conflicts * fix incorrect merges * address some perp comments * respond to more comments * pda efficiency changes * Revert "pda efficiency changes" This reverts commit 578b957fe9dc6caa6dd1221e357cfc9ddfee0170. * Revert "respond to more comments" This reverts commit 27600a179f57287aeed860a6ae6c6451eb9a62ba. * better wsol handling * subtract exchange fees from amount settled (#1849) * subtract exchange fees from amount settled * add exchange fee scalar to settling * use percents isntead of scalars * re-introduce breaking bchanges * pda efficiency changes * more pda changes * fix tests * merge in crisp token authority changes * address more comments * amm cache rework (#1863) * Crispheaney/lp whitelist mint (#1866) * lp whitelist mint * test * prettify * Crispheaney/zero copy oracle validity (#1865) * amm cache zero copy validity * remove unnecessary fields from amm cache * update pda --------- Co-authored-by: Nour Alharithi * address renaming comments * Nour/cu profiling (#1870) * add CU profiling test * reduce CUs for target base * cache robustness and limit testing CUs * pass through trade ratio in fee calcs * Crispheaney/withdraw in rm liquidity (#1871) * init * fail transfer_from_program_vault if withdraw too big * test * Nour/expand lp status (#1867) * constituent status and paused operations * add admin functions and tests * add lp status checks * testing expanded to lp pool paushed operations on perp markets * make new wallet for lp taker swaps rather than hot wallet * idl changes and bug fixes * improve CUs for target base * more CU opts * more CU reduction in target crank * lp/init lp-settle-records (#1872) * init lp-settle-records * update pr to emit event * add in last settle ts --------- Co-authored-by: Nour Alharithi * remove more unused lp pool params * update idl * update constituent target params vals * update idl * Crispheaney/rm mint (#1875) * rm unnecessary mints * sdk updates --------- Co-authored-by: Nour Alharithi * add constituent map memcmp and lp status on cache * update tests and amm cache iteration method * change target base ix ordering * update admin client whitelistdlp token ix and other things --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> Co-authored-by: wphan Co-authored-by: Chris Heaney Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> Co-authored-by: asuma * add vamm cache percent scalar (default is 100) * aum cant go below zero (#1890) * aum cant go below zero * keep it signed * reassing dlp taker bot wallet * add event subscriber changes for dlp events * give permission for dlp taker bot to deposit withdraw from program vault to dlp * fix max withdrawals bug * get max transfer bug fix * Wphan/merge-master (#1915) * fix comments (#1844) * chore: update laser 0.1.8 * chore: remove logging * program: tweak ResizeSignedMsgUserOrders (#1898) * fix linter and cargo test * fix cargo build errors * v2.138.0 * sdk: release v2.139.0-beta.0 * program: init-delegated-if-stake (#1859) * program: init-delegated-if-stake * add sdk * CHANGELOG --------- Co-authored-by: Chris Heaney * program: auction-order-params-on-slow-fast-twap-divergence (#1882) * program: auction-order-params-on-slow-fast-twap-divergence * change tests * rm dlog * CHANGELOG * cargo fmt -- --------- Co-authored-by: Chris Heaney * program: add invariant for max in amount for if swap (#1825) * sdk: release v2.139.0-beta.1 * chore: add grpc client to order subscriber * sdk: release v2.139.0-beta.2 * sdk: add market index 76 to constant (#1901) * sdk: release v2.139.0-beta.3 * fix ui build (#1902) * sdk: release v2.139.0-beta.4 * sdk: update aster config (#1903) * update aster config * add pythLazerId * sdk: release v2.139.0-beta.5 * Revert "Revert "Crispeaney/revert swift max margin ratio" (#1877)" (#1907) This reverts commit 0a8e15349f45e135df3eb2341f163d70ef09fe64. * sdk: release v2.139.0-beta.6 * Revert "Revert "Revert "Crispeaney/revert swift max margin ratio" (#1877)" (#…" (#1910) * sdk: release v2.139.0-beta.7 * more robust isDelegateSigner for swift orders * sdk: release v2.139.0-beta.8 * program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit (#1909) * program: update-resolve-perp-pnl-pool-validate * CHANGELOG --------- Co-authored-by: Chris Heaney * program: add immutable owner support for token 22 vaults (#1904) * program: add immutable owner support for token 22 vaults * cargo fmt -- * CHANGELOG * sdk: tweak math for filling triggers (#1880) * sdk: tweak math for filling triggers * add back line * sdk: release v2.139.0-beta.9 * program: allow delegate to update user position max margin ratio (#1913) * Revert "more robust isDelegateSigner for swift orders" This reverts commit 2d4e30b5bfac835c2251b8640b898408714a7c13. * sdk: release v2.139.0-beta.10 * update SwiftOrderMessage type for missing fields (#1908) * sdk: release v2.139.0-beta.11 * sdk: add getUpdateFeatureBitFlagsMedianTriggerPriceIx * sdk: release v2.139.0-beta.12 * update devnet market constants (#1914) * sdk: release v2.139.0-beta.13 * program: deposit into if stake from admin (#1899) * program: deposit into if stake from admin * add test * change action * cargo fmt -- * move depositIntoInsuranceFundStake to adminClient --------- Co-authored-by: wphan * sdk: release v2.139.0-beta.14 * program: comment out unused ix (#1911) * program: raise MAX_BASE_ASSET_AMOUNT_WITH_AMM numerical invariant * v2.139.0 * sdk: release v2.140.0-beta.0 --------- Co-authored-by: jordy25519 Co-authored-by: Jack Waller Co-authored-by: lil perp Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> * add new bulk instruction packaging * logging changes * Wphan/master-dlp (#1918) * fix comments (#1844) * chore: update laser 0.1.8 * chore: remove logging * program: tweak ResizeSignedMsgUserOrders (#1898) * fix linter and cargo test * fix cargo build errors * v2.138.0 * sdk: release v2.139.0-beta.0 * program: init-delegated-if-stake (#1859) * program: init-delegated-if-stake * add sdk * CHANGELOG --------- Co-authored-by: Chris Heaney * program: auction-order-params-on-slow-fast-twap-divergence (#1882) * program: auction-order-params-on-slow-fast-twap-divergence * change tests * rm dlog * CHANGELOG * cargo fmt -- --------- Co-authored-by: Chris Heaney * program: add invariant for max in amount for if swap (#1825) * sdk: release v2.139.0-beta.1 * chore: add grpc client to order subscriber * sdk: release v2.139.0-beta.2 * sdk: add market index 76 to constant (#1901) * sdk: release v2.139.0-beta.3 * fix ui build (#1902) * sdk: release v2.139.0-beta.4 * sdk: update aster config (#1903) * update aster config * add pythLazerId * sdk: release v2.139.0-beta.5 * Revert "Revert "Crispeaney/revert swift max margin ratio" (#1877)" (#1907) This reverts commit 0a8e15349f45e135df3eb2341f163d70ef09fe64. * sdk: release v2.139.0-beta.6 * Revert "Revert "Revert "Crispeaney/revert swift max margin ratio" (#1877)" (#…" (#1910) * sdk: release v2.139.0-beta.7 * more robust isDelegateSigner for swift orders * sdk: release v2.139.0-beta.8 * program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit (#1909) * program: update-resolve-perp-pnl-pool-validate * CHANGELOG --------- Co-authored-by: Chris Heaney * program: add immutable owner support for token 22 vaults (#1904) * program: add immutable owner support for token 22 vaults * cargo fmt -- * CHANGELOG * sdk: tweak math for filling triggers (#1880) * sdk: tweak math for filling triggers * add back line * sdk: release v2.139.0-beta.9 * program: allow delegate to update user position max margin ratio (#1913) * Revert "more robust isDelegateSigner for swift orders" This reverts commit 2d4e30b5bfac835c2251b8640b898408714a7c13. * sdk: release v2.139.0-beta.10 * update SwiftOrderMessage type for missing fields (#1908) * sdk: release v2.139.0-beta.11 * sdk: add getUpdateFeatureBitFlagsMedianTriggerPriceIx * sdk: release v2.139.0-beta.12 * update devnet market constants (#1914) * sdk: release v2.139.0-beta.13 * program: deposit into if stake from admin (#1899) * program: deposit into if stake from admin * add test * change action * cargo fmt -- * move depositIntoInsuranceFundStake to adminClient --------- Co-authored-by: wphan * sdk: release v2.139.0-beta.14 * program: comment out unused ix (#1911) * program: raise MAX_BASE_ASSET_AMOUNT_WITH_AMM numerical invariant * v2.139.0 * sdk: release v2.140.0-beta.0 * sdk: update constants market index 77 (#1916) * sdk: release v2.140.0-beta.1 * Wphan/builder codes (#1805) * program: init lp pool * cargo fmt -- * add total fee fields * add update_target_weights math * program: use sparse matrix for constituent map and update tests * zero copy accounts, init ix (#1578) * update accounts (#1580) * zero copy + permissionless crank ixs (#1581) * program: support negative target weights for borrow-lend * fix tests to work with zero copy * few comment changes * remove discriminator from impl macro * add get_swap_amount, get_swap_fees, get_weight (#1579) * add get_swap_amount, get_swap_fees, get_weight * update accounts * add back ts * rebase * add constituent swap fees * fix swap fee calc (#1582) * add init amm mapping to lp context (#1583) * init constituent * add initializeLpPool test (#1585) * add initializeLpPool test * add check for constituent target weights * add add datum ix * add init tests and invariant checks * rename data to more useful names * dlp use spl token program (#1588) * add crank ix * update total_weight for validation_flags check * push test so far * overriding perp position works * remove message * fix dup total_weight add * constituent map remaining accounts * compiles * bankrun tests pass * compiles but casting failure in overflow protection test * address comment and change token arguments from u64 to u128 * bankrun tests pass * init constituent token account (#1596) * update aum calc * add update /remove mapping ixs * fix test - init constituent spot market * add crank improvements * passes tests * precision fix crank aum * precision fixes and constituent map check for account owner * add passthrough account logic (#1602) * add passthrough account logic * cant read yet * fix all zc alignment issues * make oracle source a u8 on zc struct * Wphan/dlp-swap-ixs (#1592) * add lp_swap ix * rebase * test helpers * swap works * fix swaps, add more cargo tests for fees n swap amt * remove console.logs * address PR comments * merge upstream * post-merge fixes * store bumps on accounts (#1604) * store bumps on accounts * do pda check in constituent map * address comments * Wphan/add liquidity (#1607) * add add remove liquidity fees calc * add liquidity ix * fix init mint and lppool token account, refactor test fees * add removeLiquidity bankrun test * merge upstream * add LPPool.next_mint_redeem_id * program: lp-pool-to-use-target-base-vector (#1615) * init lp pool target-base matrix * working target-base logic * add todos for add/remove liquidity aum * add renames + fix test * add beta and cost to trade in bps to target datum * add more tests * add fields to LP events, fix tests (#1620) * add fields to LP events, fix tests * revert target weight calc * add constituent.next_swap_id, fix cost_to_trade math * dlp jup swap (#1636) * dlp jup swap * add admin client ixs * almost fixed * test working? * update begin and end swap * tweaks * fix math on how much was swapped * remove unnecessary lp pool args * extra account validation * added token account pda checks in other ixs * stablecoin targets (#1638) * is stablecoin * address comments --------- Co-authored-by: Chris Heaney * cleanup * transfer oracle data ix to constituent (#1643) * transfer oracle data ix to constituent * add lib entrypoint * simplify more * add spot market constraint * big cargo test (#1644) * derivative constituents + better testing + bug fixes (#1657) * all tests technically pass * update tests + prettify * bug fixes and tests pass * fix many bugs and finalize logic * deposit/borrow working and changing positions (#1652) * sdk: allow custom coder * program: dlp add upnl for settles to amm cache (#1659) * program: dlp add-upnl-for-settles-to-amm-cache * finish up lp pool transfer from perp market * add amount_to_transfer using diff * merge * add pnl and fee pool accounting + transfer from dlp to perp market --------- Co-authored-by: Nour Alharithi * remove unused accounts coder * move customCoder into sdk, lint * testing: ix: settle perp to dlp, insufficient balance edge case and improvements (#1688) * finish edge case test * aum check also passes * prettify * added more settle test coverage and squash bugs (#1689) * dlp: add constituentMap (#1699) * Nour/gauntlet fee impl (#1698) * added correlation matrix infra * refactor builds * mint redeem handled for usdc * remove liquidity also should work * all tests pass * bankrun tests pass too * update aum considers amm cache (#1701) * prettify (#1702) * Wphan/merge master dlp (#1703) * feat: init swift user orders on user account creation if needed * fix: wrong pushing of swift user orders ixs * fix: broken swift tests * fix: swift -> signed msg * refactor(sdk): update jupiter's api url * fix(sdk): remove error thrown * indicative qutoes server changes * sdk: release v2.121.0-beta.7 * sdK: update market index 33 oracle rr (#1606) * sdk: add to spot constants market index 34 * revert adminClient.ts change * sdk: update spot market constants oracle index 33 * sdk: release v2.121.0-beta.8 * sdk: high leverage mode updates (#1605) * sdk: high leverage mode updates * add optional param for fee calc * update changelog * sdk: release v2.121.0-beta.9 * getPlaceSignedMsgTakerPerpOrderIxs infer HLM mode from bitflags (#1608) * sdk: release v2.121.0-beta.10 * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs (#1610) * fix: dehexify in getPlaceSignedMsgTakerPerpOrderIxs * bankrun test * sdk: release v2.121.0-beta.11 * sdk: round tick/step size for getVammL2Generateor (#1612) * sdk: round tick/step size for etVammL2Generateor * use standard functions, include in all fcns * fix const declare, rm whitespace * fix posdir sign * sdk: release v2.121.0-beta.12 * sdk: release v2.121.0-beta.13 * sdk: constants market-index-45-46 (#1618) * sdk: release v2.121.0-beta.14 * robustness check for indicative quotes sender (#1621) * robustness check for indicative quotes sender * delete quote from market index of bad quote * sdk: release v2.121.0-beta.15 * Added launchTs for ZEUS, zBTC * sdk: release v2.121.0-beta.16 * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622) * sdk: release v2.121.0-beta.17 * sdk: fix vamm l2 generator base swapped (#1623) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * sdk: release v2.121.0-beta.18 * sdk: revert vamm l2 gen (#1624) * Revert "sdk: fix vamm l2 generator base swapped (#1623)" This reverts commit 56bc78d70e82cb35a90f12f73162bffb640cb655. * Revert "sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign (#1622)" This reverts commit e49cfd554cc44cd8d7770184f02f6ddb0bfc92f1. * Revert "sdk: round tick/step size for getVammL2Generateor (#1612)" This reverts commit f932a4ea2afcae314e406b7c7ee35e55b36043ad. * sdk: release v2.121.0-beta.19 * sdk: show protected-asset have zero-borrow-limit (#1603) * sdk: show protected-asset have zero-borrow-limit * rm unused AssetTier import * sdk: release v2.121.0-beta.20 * sdk: market-constants-index-74 (#1629) * sdk: release v2.121.0-beta.21 * program: use saturating_sub for number_of_users (#1616) * program: use saturating_sub for number_of_users * update CHANGELOG.md * program: allow fixing hlm num users (#1630) * sdk: release v2.121.0-beta.22 * sdk: fix switchboard on demand client to use landed at * sdk: release v2.121.0-beta.23 * sdk: spot-market-poolid-4 constants (#1631) * sdk: release v2.121.0-beta.24 * fix high lev mode liq price (#1632) * sdk: release v2.121.0-beta.25 * replace deprecated solana install scripts (#1634) * sdk: release v2.121.0-beta.26 * refactor(sdk): use ReturnType for Timeout types (#1637) * sdk: release v2.121.0-beta.27 * auction price sdk fix * sdk: release v2.121.0-beta.28 * program: multi piecewise interest rate curve (#1560) * program: multi-piecewise-interest-rate-curve * update tests * widen out borrow limits/healthy util check * add break, use array of array for borrow slope segments * program: fix cargo test * sdk: add segmented IR curve to interest rate calc * clean up unusded var, make interest rate segment logic a const * incorp efficiency feedback points * test: add sol realistic market example * cargo fmt -- * CHANGELOG --------- Co-authored-by: Chris Heaney * sdk: release v2.121.0-beta.29 * program: allow hot admin to update market fuel params (#1640) * v2.121.0 * sdk: release v2.122.0-beta.0 * sdk: fix nullish coalescing * sdk: release v2.122.0-beta.1 * program: add logging for wrong perp market mutability * sdk: check free collateral change in maxTradeSizeUsdcForPerp (#1645) * sdk: check free collateral change in maxTradeSizeUsdcForPerp * update changelog * sdk: release v2.122.0-beta.2 * refactor(sdk): emit newSlot event on initial subscribe call (#1646) * sdk: release v2.122.0-beta.3 * sdk: spot-market-constants-pool-id-2 (#1647) * sdk: release v2.122.0-beta.4 * sdk: add-spot-market-index-52-constants (#1649) * sdk: release v2.122.0-beta.5 * program: add existing position fields to order records (#1614) * program: add quote entry amount to order records * fix cargo fmt and test * more reusable code * more reusable code * add another comment * fix math * account for pos flip * fix typo * missed commit * more fixes * align naming * fix typo * CHANGELOG * program: check limit price after applying buffer in trigger limit ord… (#1648) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * fix tests * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: fix cargo tests * program: check limit price when setting auction for limit order (#1650) * program: check limit price after applying buffer in trigger limit order auction * program: reduce duplicate code * program: check limit price when setting limit auction params * cargo fmt -- * fix CHANGELOG * tests: updates switchboardTxCus.ts * program: try to fix iteration for max order size (#1651) * Revert "program: try to fix iteration for max order size (#1651)" This reverts commit 3f0eab39ed23fa4a9c41cbab9af793c60b50a239. * disable debug logging in bankrun tests * v2.122.0 * sdk: release v2.123.0-beta.0 * sdk: constants-spot-market-index-53 (#1655) * sdk: release v2.123.0-beta.1 * sdk: idl for new existing position order action records * fix: protocol test prettier fix * make ci lut checks not shit * sdk: release v2.123.0-beta.2 * sdk: fix vamm l2 generator base swapped and add new top of book (#1626) * sdk: bigz/fix-vamm-l2-generator-baseSwapped var assign * fix ask book else baseSwapped calc * use proper quoteAmount with baseSwap for top of book orders * clean up console.log * sdk: getVammL2Generator reduce loc (#1628) * sdk: getVammL2Generator-reduce-loc * add MAJORS_TOP_OF_BOOK_QUOTE_AMOUNTS * add marketindex check topOfBookAmounts * yarn lint/prettier * sdk: release v2.123.0-beta.3 * program: allow all limit orders to go through swift (#1661) * program: allow all limit orders to go through swift * add anchor test * CHANGELOG * sdk: add optional initSwiftAccount on existing account deposits (#1660) * sdk: release v2.123.0-beta.4 * program: add taker_speed_bump_override and amm_spread_adjustment * Revert "program: add taker_speed_bump_override and amm_spread_adjustment" This reverts commit 1e19b7e7a6c5cecebdbfb3a9e224a0d4471ba6d2. * program: tests-fee-adjustment-neg-100 (#1656) * program: tests-fee-adjustment-neg-100 * add HLM field to test * cargo fmt -- --------- Co-authored-by: Chris Heaney * program: simplify user can skip duration (#1668) * program: simplify user can skip duration * update context * CHANGELOG * fix test * fix pmm tests --------- Co-authored-by: Chris Heaney * program: add taker_speed_bump_override and amm_spread_adjustment (#1665) * program: add taker_speed_bump_override and amm_spread_adjustment * add admin client * cargo test * add impl for amm_spread_adjustment * ensure no overflows * CHANGELOG * cargo fmt -- * sdk types * prettify --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * program: update-amm-spread-and-availability-constraints (#1663) * program: update-amm-spread-and-availability-constraints * fix cargo tests * program: use saturating mul for amm spread adj * nour/indic-quotes-sender-v2 (#1667) * nour/indic-quotes-sender-v2 * prettify * pass margin category into calculateEntriesEffectOnFreeCollateral (#1669) * fix cargo test * tests: fix oracle guardrail test * sdk: update idl * yarn prettify:fix * tests: fix a few more place and make tests * prettify fix * whitespace readme change * sdk: release v2.123.0-beta.5 * v2.123.0 * sdk: release v2.124.0-beta.0 * v2.123.0-1 * sdk: calculateVolSpreadBN-sync (#1671) * sdk: release v2.124.0-beta.1 * sdk: calculate-spread-bn-add-amm-spread-adjustment (#1672) * sdk: calculate-spread-bn-add-amm-spread-adjustment * corect sign * add math max 1 * prettify * sdk: release v2.124.0-beta.2 * sdk: correct calculateVolSpreadBN reversion * sdk: release v2.124.0-beta.3 * sdk: add getTriggerAuctionStartPrice (#1654) * sdk: add getTriggerAuctionStartPrice * updates * precisions * remove startBuffer param --------- Co-authored-by: Chris Heaney * sdk: release v2.124.0-beta.4 * feat: customized cadence account loader (#1666) * feat: customized cadence account loader bby * feat: method to read account cadence on custom cadence account loader * feat: PR feedback on customized loader cleaup code and better naming * fix: lint and prettify * feat: more efficient rpc polling on custom polling intervals * feat: custom cadence acct loader override load * chore: prettify * sdk: release v2.124.0-beta.5 * sdk: sync-user-trade-tier-calcs (#1673) * sdk: sync-user-trade-tier-calcs * prettify --------- Co-authored-by: Nick Caradonna * sdk: release v2.124.0-beta.6 * sdk: add new admin client fn * Revert "sdk: add new admin client fn" This reverts commit c7a4f0b174858048bd379f2f2bb0e63595949921. * sdk: release v2.124.0-beta.7 * refactor(ui): add callback logic, fix polling frequency update * sdk: release v2.124.0-beta.8 * program: less order param sanitization for long tail perps (#1680) * program: allow-auction-start-buffer-on-tail-mkt * fix test * cargo fmt -- * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> * Wphan/custom coder (#1682) * sdk: allow custom coder * remove unused accounts coder * linter * move customCoder into sdk, lint * update test helpers * update testhelpers.ts * sdk: release v2.124.0-beta.9 * update sdk exports * sdk: release v2.124.0-beta.10 * sdk: safer-calculate-spread-reserve-math (#1681) * sdk: release v2.124.0-beta.11 * update getMaxLeverageForPerp to use usdc logic (#1678) * sdk: release v2.124.0-beta.12 * program: override for oracle delay (#1679) * programy: override for oracle delay * update impl * switch to i8 * CHANGELOG * program: programmatic rebalance between protocol owned if holdings (#1653) * program: if swap * program: add initial config * add update * more * moar * moar * moar * program: update how swap epoch works * add test * add an invariant * cargo fmt -- * add transfer to rev pool * add mint validation * cargo fmt -- * track in amount between tranfsers * add to ci tests * separate key * program: always transfer max amount to rev pool * CHANGELOG * sdk: release v2.124.0-beta.13 * sdk: improve-aclient-accounts-logic (#1684) * sdk: release v2.124.0-beta.14 * program: improve-amm-spread-validates (#1685) * program: let hot wallet update amm jit intensity * sdk: hot wallet can update amm jit intensity * program: hot wallet can update curve intensity * program: fix build * sdk: update idl * sdk: release v2.124.0-beta.15 * v2.124.0 * sdk: release v2.125.0-beta.0 * program: three-point-std-estimator (#1686) * program: three-point-std-estimator * update tests and add sdk * update changelog * sdk: add-updatePerpMarketOracleSlotDelayOverride (#1691) * sdk: release v2.125.0-beta.1 * program: add-amm-inventory-spread-adjustment-param (#1690) * program: add-amm-inventory-spread-adjustment-param * cargo fmt -- * update sdk * prettier * fix syntax { --------- Co-authored-by: Chris Heaney * program: max-apr-rev-settle-by-spot-market (#1692) * program: max-apr-rev-settle-by-spot-market * update max * default to u128 to avoid casts * changelog * sdk: release v2.125.0-beta.2 * program: better account for imf in calculate_max_perp_order_size (#1693) * program: better account for imf in calculate_max_perp_order_size * CHANGELOG * v2.125.0 * sdk: release v2.126.0-beta.0 * sdk: only count taker fee in calculateEntriesEffectOnFreeCollateral for maintenance (#1694) * sdk: release v2.126.0-beta.1 * Separate getAddInsuranceFundStakeIxs (#1695) * sdk: release v2.126.0-beta.2 * idl: amm-inv-adj-latest-idl (#1697) * sdk: release v2.126.0-beta.3 * sdk: spot-market-index-54 constants (#1696) * sdk: release v2.126.0-beta.4 * sdk: update spot market index 54 pythlazer id * sdk: release v2.126.0-beta.5 * Update spotMarkets.ts * sdk: release v2.126.0-beta.6 * prettify --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: Nour Alharithi Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: lil perp Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> * slot staleness checks (#1705) * slot staleness checks * update aum ix to use constituent oracles * Nour/derivative constituent testing (#1708) * slot staleness checks * update aum ix to use constituent oracles * constituent test works when adjusting derivative index * constituent depeg kill switch works * works with multiple derivatives on the same parent * remove incorrect usage of nav * fix adminClient and tests * Nour/fee grid search testing (#1714) * grid search * grid search swap test * Nour/address comments (#1715) * low hanging fruit comments * remove pda checks and store lp pool on zero copy accounts * parameterize depeg threshold * make description in lp pool event * update idl for event change * add swap fee unit tests (#1713) * add swap fee unit tests * remove linear inventory fee component * Nour/settle accounting (#1723) * fixing the main settle test and settle function * all current tests pass * update msg occurrences * dont update lp quote owed unless collateralized * Nour/settle testing (#1725) * refactor settle pnl to modularize and add tests * more cargo tests * prettify * Nour/address more comments (#1726) * use oracle staleness threshold for staleness * add spot market vault invariant * refactor update_aum, add unit tests (#1727) * refactor update_aum, add unit tests * add constituent target base tests * update doc * Nour/parameterize dlp (#1731) * add validates and test for withdraw limit * settlement max * update idl * merge conflicts * fixes * update idl * bug fixes * mostly sdk fixes * bug fixes * bug fix and deploy script * program: new amm oracle (#1738) * zero unused amm fields * cargo fmt * bare bones ix * minimal anchor mm oracle impl * update test file * only do admin validate when not anchor test * updates * generalize native entry * fix weird function name chop off * make it compile for --feature cpi (#1748) Co-authored-by: jordy25519 * more efficeint clock and state bit flags check * vamm uses mm oracle (#1747) * add offset * working tests * refactor to use MM oracle as its own type * remove weird preface * sdk updates * bankrun tests all pass * fix test * changes and fixes * widen confidence if mm oracle too diff * sdk side for confidence adjust * changelog * fix lint * fix cargo tests * address comments * add conf check * remove anchor ix and cache oracle confidence * only state admin can reenable mm oracle kill switch * cargo fmt --------- Co-authored-by: jordy25519 * fix tests (#1764) * Nour/move ixs around (#1766) * move around ixs * remove message * add devnet oracle crank wallet * refactored mm oracle * sdk changes + cargo fmt * fix tests * validate price bands with fill fix * normalize fill within price bands * add sdk warning * updated type * undefined guard so anchor tests pass * accept vec for update amm and view amm * adjust test to work with new price bands * Revert "adjust test to work with new price bands" This reverts commit ee40ac8799fa2f6222ea7d0e9b3e07014346a699. * remove price bands logic * add zero ix for mm oracle for reset * add new drift client ix grouping * v1 safety improvements * isolate funding from MM oracle * add cargo tests for amm availability * change oracle validity log bool to enum * address comment * make validate fill direction agnostic * fix liquidate borrow for perp pnl test * fix tests and address comments * add RevenueShare and RevenueShareEscrow accounts an init ixs * fix multiple array zc account, and handling different message types in place_signed_msg_taker_order * decoding error * commit constituent map to barrel file * add lp fields to perp market account * recording orders in RevenueShareEscrow workin * rearrange perp market struct for lp fields * cancel and fill orders * idl * fix sdk build * fix math * bug fix for notional position tracking * update RevenueShareOrder bitflags, store builder_idx instead of pubkey * view function * merge RevenueShareOrders on add * fee view functions * max aum + whitelist check and removing get_mint_redeem_fee for now * add wsol support for add liquidity * fix sdk and typing bugs * update lp pool params ix * admin override cache and disable settle functions * remove builder accounts from cancel ixs, wip settle impl * dont fail settlpnl if no builder users provided * devnet swap working * finish settle, rename RevenueShare->Builder, RevenueShareEscrow->BuilderEscrow * add more bankrun tests, clean up * clean up, fix tests * why test fail * dlp taker discovered bug fixes and sdk changes * add subaccountid to BuilderOrder * reduce diff * refactor last settle ts to last settle slot * add referrals * add test can fill settle user with no builderescrow * add referral builder feature flag and referral migration method * fix cargo tests, try fix bankrun test timing issue * Nour/settle pnl fix (#1817) * settle perp to lp pool bug fixes * update bankrun test to not use admin fee pool deposit * fix tests using update spot market balances too * add log msgs for withdraw and fix casting bug * add SignedMsgOrderParamsMessageV2 * check in for z (#1823) * feat: option for custom oracle ws subscriber * fix: pass custom oracle ws sub option in dc constructor * sdk: add spot-market-index-57 to constants (#1815) * sdk: release v2.134.0-beta.2 * lazer oracle migration (#1813) * lazer oracle migration * spot markets too * sdk: release v2.134.0-beta.3 * sdk: release v2.134.0-beta.4 * program: settle pnl invariants (#1812) * program: settle pnl invariants * add test * fix lint * lints * add msg * CHANGELOG * cargo fmt -- * program: add_update_perp_pnl_pool (#1810) * program: add_update_perp_pnl_pool * test * CHANGELOG * sdk: release v2.134.0-beta.5 * program: update-mark-twap-integer-bias (#1783) * program: update-mark-twap-integer-bias * changelog update * program: update-fee-tier-determine-fix5 (#1800) * program: update-fee-tier-determine-fix5 * update changelog * program: update-mark-twap-crank-use-5min-basis (#1769) * program: update-mark-twap-crank-use-5min-basis * changelog * program: update-min-margin-const-limit (#1802) * program: update-min-margin-const-limit * add CHANGELOG.md * sdk: release v2.134.0-beta.6 * program: rm-burn-lp-shares-invariant (#1816) * program: rm-burn-lp-shares-invariant * update changelog * fix test and cargo fmt * fix anchor tests * yarn prettify:fix * reenable settle_pnl mode test * v2.134.0 * sdk: release v2.135.0-beta.0 * Merge pull request #1820 from drift-labs/chester/fix-zod * sdk: release v2.135.0-beta.1 * mm oracle sdk change (#1806) * mm oracle sdk change * better conditional typing * DLOB bug fix * updated idl * rm getAmmBidAskPrice * sdk: release v2.135.0-beta.2 * sdk: fix isHighLeverageMode * sdk: release v2.135.0-beta.3 * refactor(sdk): add update delegate ix method, ovrride authority for settle multiple pnl (#1822) * check in for z * more logging changes * mm oracle sdk additions (#1824) * strict typing for more MM oracle contact points * add comments to auction.ts * prettify * sdk: release v2.135.0-beta.4 * init constituent bug fix and type change * add in invariant to be within 1 bp of balance before after * add strict typing for getPrice and new auction trigger function (#1826) * add strict typing for getPrice and new auction trigger function * refactor getTriggerAuctionStartAndExecutionPrice * sdk: release v2.135.0-beta.5 * update tests and enforce atomic settles for withdraw * add failing withdraw test * withdraw fix * bring diff in validate back to 1 * make lp pool test fail * better failed test * only check after < before, do to spot precision limits * add balance check to be < 1 cent --------- Co-authored-by: Lukas deConantsesznak Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: LukasDeco Co-authored-by: lil perp Co-authored-by: wphan Co-authored-by: Chester Sim * zero pad swift messages to make backwards compatible * PR feedback * add price for lp validates (#1833) * update tests/placeAndMakeSignedMsgBankrun.ts to handle client side errors * add missing token account reloads and syncs * add disabled lp pool swaps by default * refactor account logic for borrows * remove double fee count, update tests to check filled position and quote amounts fda * more extensive aum logging * rename Builder -> RevenueShare * add test check accumulated builder/ref fees * fix settle multiple pnl accounts, test ref rewards in multiple markets * express builder fees in tenth of bps * update referral migration params * PR feedback * add builder code feature gate * fix tests * add referral fields * run all tests * kickoff build * disable extra instructions, fix builder code feature flag selection * update driftclient * Revert recent builder codes chain and merge (#1848) * Revert recent builder codes chain and merge * update driftclient * disable extra instructions, fix builder code feature flag selection * clean up account inclusion rules in settle pnl for builder codes * cargo fmt * PR comments, featureflag clean up * move authority check into get_revenue_share_escrow_account * clean up referrer eligibility check, support placeAndTake/Make referral fees * skip builder fee accrual on full escrow account, dont throw * add feature flag sdk fn * program: builder codes dont throw tx on missing acc * placeAndMake respect builder codes * ensure update userstats referrerstatus on migration * hold back OrderActionRecord idl changes * update CHANGELOG.md --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> Co-authored-by: moosecat Co-authored-by: Chris Heaney Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> * sdk: release v2.140.0-beta.2 * v2.140.0 * sdk: release v2.141.0-beta.0 * feat: add margin ratio ix to open orders + swift prop (#1864) * feat: add margin ratio ix to open orders + swift prop * fix: bug with max lev available calculation * fix: bug with swift msg encoding + margin ratio * feat: re-add types for swift non-optional * rm: unneeded undefined check on swift maxMarginRation * allow enter HLM on position margin ratio update * fix margin ratio calc * updates * rm logs --------- Co-authored-by: Nick Caradonna * sdk: release v2.141.0-beta.1 --------- Co-authored-by: jordy25519 Co-authored-by: Jack Waller Co-authored-by: lil perp Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: bigz_Pubkey <83473873+0xbigz@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: Nour Alharithi <14929853+moosecat2@users.noreply.github.com> Co-authored-by: moosecat Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: Luke Steyn Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> * borrow lend accounting (#1905) * add in event * emit new event type * event subscriber * reformat event * fix constituent map index * bump consittunet index * cargo tests work * remove duplicate admin client funcs * update idl * bump constituent map max size * make working devcontainer and dockerfile * bug fixes * bug fixes * fix node version * fixed idl * dockerfile and dev container working, and anchor build working * add dev container.json and dockerfile * update dlp types to include lp pool key * stable target base liquidity fix + cleanup * relax transfer from program lp invariant for devnet * further restrict constituent max borrow * give 1% flexilibity for race conditions on max transfer amount * introduce max borrow buffer * include new amm inventory limit (#1932) * refactor amm cache * change amm cache pda seed for devnet reset * update sdk types file to be up to parity * clean up and guard against negative amm_inventory_limit * Target delay increases fees (#1943) * only update the last slot if the oracles and perp positions pass the check per constituent * add in uncertainty fee * add simple test for target delays * change target base seed * prettify * update idl * Moose review (#1948) * better cap fees * add more constraints for user token accounts * use oracle map for updating aum * cargo tests pass and adding oracle map usage to derivative constituent in aum target as well * remove unnecessary admin func * idl * delete unused errors * make linter happy * fix all compiler warnings * Use lp pool id (#1992) * add lp pool id and replace lp pool name * add settle perp market enforcement with lp pool id * constituent map fix * address comments round 2 * add additional settle pnl invariant check * create separate hot wallet for lp pool * remove any unused fields * add oracle map logging argument * generalize lp pool test failure * unified swap mode compatibility * clippy and whitelist changes * make prettify and lint happy --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> Co-authored-by: wphan Co-authored-by: Chris Heaney Co-authored-by: Lukas deConantsesznak Co-authored-by: Chester Sim Co-authored-by: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lowkeynicc <85139158+lowkeynicc@users.noreply.github.com> Co-authored-by: jordy25519 Co-authored-by: Luke Steyn Co-authored-by: LukasDeco Co-authored-by: Nick Caradonna Co-authored-by: Jesse Cha <42378241+Jesscha@users.noreply.github.com> Co-authored-by: asuma Co-authored-by: Jack Waller --- .devcontainer/Dockerfile | 17 +- CHANGELOG.md | 10 - programs/drift/Cargo.toml | 2 +- programs/drift/src/controller/insurance.rs | 6 +- programs/drift/src/controller/liquidation.rs | 10 +- programs/drift/src/controller/orders.rs | 6 +- .../drift/src/controller/position/tests.rs | 55 +- programs/drift/src/controller/repeg/tests.rs | 4 +- .../src/controller/spot_balance/tests.rs | 4 +- programs/drift/src/controller/token.rs | 78 +- programs/drift/src/error.rs | 39 +- programs/drift/src/ids.rs | 19 + programs/drift/src/instructions/admin.rs | 257 +- programs/drift/src/instructions/if_staker.rs | 3 +- programs/drift/src/instructions/keeper.rs | 403 +- programs/drift/src/instructions/lp_admin.rs | 1392 ++++ programs/drift/src/instructions/lp_pool.rs | 2130 ++++++ programs/drift/src/instructions/mod.rs | 4 + programs/drift/src/instructions/user.rs | 22 +- programs/drift/src/lib.rs | 329 +- programs/drift/src/math/amm.rs | 6 +- programs/drift/src/math/auction.rs | 4 +- programs/drift/src/math/constants.rs | 2 + programs/drift/src/math/cp_curve/tests.rs | 10 +- programs/drift/src/math/fees.rs | 2 +- programs/drift/src/math/fuel.rs | 2 +- programs/drift/src/math/fulfillment/tests.rs | 30 +- programs/drift/src/math/insurance.rs | 3 +- programs/drift/src/math/liquidation.rs | 6 +- programs/drift/src/math/lp_pool.rs | 268 + programs/drift/src/math/margin.rs | 10 +- programs/drift/src/math/margin/tests.rs | 5 +- programs/drift/src/math/mod.rs | 1 + programs/drift/src/math/oracle.rs | 62 +- programs/drift/src/math/orders.rs | 15 +- programs/drift/src/math/orders/tests.rs | 8 +- programs/drift/src/math/position.rs | 5 +- programs/drift/src/math/spot_swap.rs | 3 +- programs/drift/src/state/amm_cache.rs | 355 + programs/drift/src/state/constituent_map.rs | 253 + .../drift/src/state/insurance_fund_stake.rs | 3 +- programs/drift/src/state/lp_pool.rs | 1891 ++++++ programs/drift/src/state/lp_pool/tests.rs | 3860 +++++++++++ .../drift/src/state/margin_calculation.rs | 7 +- programs/drift/src/state/mod.rs | 4 + programs/drift/src/state/oracle.rs | 50 + programs/drift/src/state/order_params.rs | 8 +- programs/drift/src/state/paused_operations.rs | 52 + programs/drift/src/state/perp_market.rs | 44 +- programs/drift/src/state/perp_market/tests.rs | 2 +- programs/drift/src/state/spot_market.rs | 5 +- programs/drift/src/state/state.rs | 26 +- programs/drift/src/state/user.rs | 13 +- programs/drift/src/state/user/tests.rs | 4 +- programs/drift/src/state/zero_copy.rs | 181 + programs/drift/src/validation/margin.rs | 6 +- programs/drift/src/validation/order.rs | 3 +- programs/drift/src/validation/perp_market.rs | 8 +- programs/drift/src/validation/user.rs | 3 +- sdk/src/accounts/types.ts | 20 + .../webSocketProgramAccountSubscriberV2.ts | 596 ++ sdk/src/addresses/pda.ts | 116 +- sdk/src/adminClient.ts | 1653 ++++- sdk/src/constituentMap/constituentMap.ts | 285 + .../pollingConstituentAccountSubscriber.ts | 97 + .../webSocketConstituentAccountSubscriber.ts | 112 + sdk/src/driftClient.ts | 1114 +++- sdk/src/driftClientConfig.ts | 23 +- sdk/src/idl/drift.json | 5682 +++++++++++++---- sdk/src/index.ts | 5 +- sdk/src/memcmp.ts | 24 +- sdk/src/types.ts | 160 + test-scripts/run-anchor-tests.sh | 4 +- test-scripts/single-anchor-test.sh | 4 +- tests/fixtures/token_2022.so | Bin 0 -> 1382016 bytes tests/lpPool.ts | 1734 +++++ tests/lpPoolCUs.ts | 658 ++ tests/lpPoolSwap.ts | 998 +++ tests/testHelpers.ts | 47 +- 79 files changed, 23652 insertions(+), 1690 deletions(-) create mode 100644 programs/drift/src/instructions/lp_admin.rs create mode 100644 programs/drift/src/instructions/lp_pool.rs create mode 100644 programs/drift/src/math/lp_pool.rs create mode 100644 programs/drift/src/state/amm_cache.rs create mode 100644 programs/drift/src/state/constituent_map.rs create mode 100644 programs/drift/src/state/lp_pool.rs create mode 100644 programs/drift/src/state/lp_pool/tests.rs create mode 100644 programs/drift/src/state/zero_copy.rs create mode 100644 sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts create mode 100644 sdk/src/constituentMap/constituentMap.ts create mode 100644 sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts create mode 100644 sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts create mode 100755 tests/fixtures/token_2022.so create mode 100644 tests/lpPool.ts create mode 100644 tests/lpPoolCUs.ts create mode 100644 tests/lpPoolSwap.ts diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 39d1ca340f..d9192d2ef6 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -13,11 +13,18 @@ ENV HOME="/root" ENV PATH="/usr/local/cargo/bin:${PATH}" ENV PATH="/root/.local/share/solana/install/active_release/bin:${PATH}" -RUN mkdir -p /workdir /tmp && \ - apt-get update -qq && apt-get upgrade -qq && apt-get install -y --no-install-recommends \ - build-essential git curl wget jq pkg-config python3-pip xz-utils ca-certificates \ - libssl-dev libudev-dev bash && \ - rm -rf /var/lib/apt/lists/* +RUN mkdir -p /workdir /tmp \ + && apt-get update -qq \ + && apt-get upgrade -qq \ + && apt-get install -y --no-install-recommends \ + build-essential git curl wget jq pkg-config python3-pip xz-utils ca-certificates \ + libssl-dev libudev-dev bash software-properties-common \ + && add-apt-repository 'deb http://deb.debian.org/debian bookworm main' \ + && apt-get update -qq \ + && apt-get install -y libc6 libc6-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN rustup component add rustfmt RUN rustup install 1.78.0 \ && rustup component add rustfmt clippy --toolchain 1.78.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0186e9e1..1d540031c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,16 +119,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking -## [2.135.0] - 2025-08-22 - -### Features - -### Fixes - -- program: trigger price use 5min mark price ([#1830](https://github.com/drift-labs/protocol-v2/pull/1830)) - -### Breaking - ## [2.134.0] - 2025-08-13 ### Features diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 08163a1fe6..e1f35eed93 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -20,7 +20,7 @@ drift-rs=[] [dependencies] anchor-lang = "0.29.0" solana-program = "1.16" -anchor-spl = "0.29.0" +anchor-spl = { version = "0.29.0", features = [] } pyth-client = "0.2.2" pyth-lazer-solana-contract = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "d790d1cb4da873a949cf33ff70349b7614b232eb", features = ["no-entrypoint"]} pythnet-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "3e8a24ecd0bcf22b787313e2020f4186bb22c729"} diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 17cb020405..fa53164733 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -14,9 +14,9 @@ use crate::error::ErrorCode; use crate::math::amm::calculate_net_user_pnl; use crate::math::casting::Cast; use crate::math::constants::{ - MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT, + FUEL_START_TS, GOV_SPOT_MARKET_INDEX, MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT, MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT_GOV, ONE_YEAR, PERCENTAGE_PRECISION, - SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_DENOMINATOR, + QUOTE_SPOT_MARKET_INDEX, SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_DENOMINATOR, SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_NUMERATOR, }; use crate::math::fuel::calculate_insurance_fuel_bonus; @@ -40,7 +40,7 @@ use crate::state::perp_market::PerpMarket; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::state::State; use crate::state::user::UserStats; -use crate::{emit, validate, FUEL_START_TS, GOV_SPOT_MARKET_INDEX, QUOTE_SPOT_MARKET_INDEX}; +use crate::{emit, validate}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 9503f24966..4e5543d313 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -21,8 +21,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::bankruptcy::is_user_bankrupt; use crate::math::casting::Cast; use crate::math::constants::{ - LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, QUOTE_PRECISION, - QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, SPOT_WEIGHT_PRECISION, + LIQUIDATION_FEE_PRECISION, LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, + QUOTE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, + SPOT_WEIGHT_PRECISION, }; use crate::math::liquidation::{ calculate_asset_transfer_for_liability_transfer, @@ -48,6 +49,7 @@ use crate::math::orders::{ use crate::math::position::calculate_base_asset_value_with_oracle_price; use crate::math::safe_math::SafeMath; +use crate::math::constants::LST_POOL_ID; use crate::math::spot_balance::get_token_value; use crate::state::events::{ LiquidateBorrowForPerpPnlRecord, LiquidatePerpPnlForDepositRecord, LiquidatePerpRecord, @@ -66,8 +68,8 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::State; use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User, UserStats}; use crate::state::user_map::{UserMap, UserStatsMap}; -use crate::{get_then_update_id, load_mut, LST_POOL_ID}; -use crate::{validate, LIQUIDATION_FEE_PRECISION}; +use crate::validate; +use crate::{get_then_update_id, load_mut}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 7514d9ea0d..bc8b3aec4c 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -10,6 +10,7 @@ use crate::state::revenue_share::{ }; use anchor_lang::prelude::*; +use crate::controller; use crate::controller::funding::settle_funding_payment; use crate::controller::position; use crate::controller::position::{ @@ -32,7 +33,9 @@ use crate::math::amm::calculate_amm_available_liquidity; use crate::math::amm_jit::calculate_amm_jit_liquidity; use crate::math::auction::{calculate_auction_params_for_trigger_order, calculate_auction_prices}; use crate::math::casting::Cast; -use crate::math::constants::{BASE_PRECISION_U64, PERP_DECIMALS, QUOTE_SPOT_MARKET_INDEX}; +use crate::math::constants::{ + BASE_PRECISION_U64, MARGIN_PRECISION, PERP_DECIMALS, QUOTE_SPOT_MARKET_INDEX, +}; use crate::math::fees::{determine_user_fee_tier, ExternalFillFees, FillFees}; use crate::math::fulfillment::{ determine_perp_fulfillment_methods, determine_spot_fulfillment_methods, @@ -81,7 +84,6 @@ use crate::validation; use crate::validation::order::{ validate_order, validate_order_for_force_reduce_only, validate_spot_order, }; -use crate::{controller, MARGIN_PRECISION}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 0741d5df16..390e8502a5 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -53,17 +53,7 @@ fn amm_pool_balance_liq_fees_example() { let perp_market_loader: AccountLoader = AccountLoader::try_from(&perp_market_account_info).unwrap(); - let perp_market_map = PerpMarketMap::load_one(&perp_market_account_info, true).unwrap(); - let now = 1725948560; - let clock_slot = 326319440; - let clock = Clock { - unix_timestamp: now, - slot: clock_slot, - ..Clock::default() - }; - - let mut state = State::default(); let mut prelaunch_oracle_price = PrelaunchOracle { price: PRICE_PRECISION_I64, @@ -77,9 +67,8 @@ fn amm_pool_balance_liq_fees_example() { prelaunch_oracle_price, &prelaunch_oracle_price_key, PrelaunchOracle, - oracle_account_info + _oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock_slot, None).unwrap(); let mut spot_market = SpotMarket { cumulative_deposit_interest: 11425141382, @@ -609,11 +598,11 @@ fn amm_ref_price_decay_tail_test() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -654,7 +643,7 @@ fn amm_ref_price_decay_tail_test() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -687,7 +676,7 @@ fn amm_ref_price_decay_tail_test() { ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -786,11 +775,11 @@ fn amm_ref_price_offset_decay_logic() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -831,7 +820,7 @@ fn amm_ref_price_offset_decay_logic() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -871,7 +860,7 @@ fn amm_ref_price_offset_decay_logic() { ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -961,11 +950,11 @@ fn amm_negative_ref_price_offset_decay_logic() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -1006,7 +995,7 @@ fn amm_negative_ref_price_offset_decay_logic() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -1047,7 +1036,7 @@ fn amm_negative_ref_price_offset_decay_logic() { ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -1147,11 +1136,11 @@ fn amm_perp_ref_offset() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -1173,7 +1162,7 @@ fn amm_perp_ref_offset() { max_ref_offset, ) .unwrap(); - assert_eq!(res, (perp_market.amm.max_spread / 2) as i32); + assert_eq!(res, 45000); assert_eq!(perp_market.amm.reference_price_offset, 18000); // not updated vs market account let now = 1741207620 + 1; @@ -1193,7 +1182,7 @@ fn amm_perp_ref_offset() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -1252,7 +1241,7 @@ fn amm_perp_ref_offset() { // Uses the original oracle if the slot is old, ignoring MM oracle perp_market.amm.mm_oracle_price = mm_oracle_price_data.get_price() * 995 / 1000; perp_market.amm.mm_oracle_slot = clock_slot - 100; - let mut mm_oracle_price = perp_market + let mm_oracle_price = perp_market .get_mm_oracle_price_data( oracle_price_data, clock_slot, @@ -1260,13 +1249,7 @@ fn amm_perp_ref_offset() { ) .unwrap(); - let _ = _update_amm( - &mut perp_market, - &mut mm_oracle_price, - &state, - now, - clock_slot, - ); + let _ = _update_amm(&mut perp_market, &mm_oracle_price, &state, now, clock_slot); let reserve_price_mm_offset_3 = perp_market.amm.reserve_price().unwrap(); let (b3, a3) = perp_market .amm diff --git a/programs/drift/src/controller/repeg/tests.rs b/programs/drift/src/controller/repeg/tests.rs index 171351437a..cc369c25e1 100644 --- a/programs/drift/src/controller/repeg/tests.rs +++ b/programs/drift/src/controller/repeg/tests.rs @@ -260,7 +260,7 @@ pub fn update_amm_test_bad_oracle() { #[test] pub fn update_amm_larg_conf_test() { let now = 1662800000 + 60; - let mut slot = 81680085; + let slot = 81680085; let mut market = PerpMarket::default_btc_test(); assert_eq!(market.amm.base_asset_amount_with_amm, -1000000000); @@ -409,7 +409,7 @@ pub fn update_amm_larg_conf_test() { #[test] pub fn update_amm_larg_conf_w_neg_tfmd_test() { let now = 1662800000 + 60; - let mut slot = 81680085; + let slot = 81680085; let mut market = PerpMarket::default_btc_test(); market.amm.concentration_coef = 1414213; diff --git a/programs/drift/src/controller/spot_balance/tests.rs b/programs/drift/src/controller/spot_balance/tests.rs index 83d6603fff..b1c19153c1 100644 --- a/programs/drift/src/controller/spot_balance/tests.rs +++ b/programs/drift/src/controller/spot_balance/tests.rs @@ -1385,7 +1385,7 @@ fn check_fee_collection_larger_nums() { #[test] fn test_multi_stage_borrow_rate_curve() { - let mut spot_market = SpotMarket { + let spot_market = SpotMarket { market_index: 0, oracle_source: OracleSource::QuoteAsset, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, @@ -1455,7 +1455,7 @@ fn test_multi_stage_borrow_rate_curve_sol() { let spot_market_loader: AccountLoader = AccountLoader::try_from(&sol_market_account_info).unwrap(); - let mut spot_market = spot_market_loader.load_mut().unwrap(); + let spot_market = spot_market_loader.load_mut().unwrap(); // Store all rates to verify monotonicity and smoothness later let mut last_rate = 0_u128; diff --git a/programs/drift/src/controller/token.rs b/programs/drift/src/controller/token.rs index d9fd82230a..a36a2e8404 100644 --- a/programs/drift/src/controller/token.rs +++ b/programs/drift/src/controller/token.rs @@ -9,7 +9,7 @@ use anchor_spl::token_2022::spl_token_2022::extension::{ }; use anchor_spl::token_2022::spl_token_2022::state::Mint as MintInner; use anchor_spl::token_interface::{ - self, CloseAccount, Mint, TokenAccount, TokenInterface, Transfer, TransferChecked, + self, Burn, CloseAccount, Mint, MintTo, TokenAccount, TokenInterface, Transfer, TransferChecked, }; use std::iter::Peekable; use std::slice::Iter; @@ -25,7 +25,31 @@ pub fn send_from_program_vault<'info>( remaining_accounts: Option<&mut Peekable>>>, ) -> Result<()> { let signature_seeds = get_signer_seeds(&nonce); - let signers = &[&signature_seeds[..]]; + + send_from_program_vault_with_signature_seeds( + token_program, + from, + to, + authority, + &signature_seeds, + amount, + mint, + remaining_accounts, + ) +} + +#[inline] +pub fn send_from_program_vault_with_signature_seeds<'info>( + token_program: &Interface<'info, TokenInterface>, + from: &InterfaceAccount<'info, TokenAccount>, + to: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + signature_seeds: &[&[u8]], + amount: u64, + mint: &Option>, + remaining_accounts: Option<&mut Peekable>>>, +) -> Result<()> { + let signers = &[signature_seeds]; if let Some(mint) = mint { if let Some(remaining_accounts) = remaining_accounts { @@ -137,6 +161,56 @@ pub fn close_vault<'info>( token_interface::close_account(cpi_context) } +pub fn mint_tokens<'info>( + token_program: &Interface<'info, TokenInterface>, + destination: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + signature_seeds: &[&[u8]], + amount: u64, + mint: &InterfaceAccount<'info, Mint>, +) -> Result<()> { + let signers = &[signature_seeds]; + + let mint_account_info = mint.to_account_info(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = MintTo { + mint: mint_account_info, + to: destination.to_account_info(), + authority: authority.to_account_info(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + token_interface::mint_to(cpi_context, amount) +} + +pub fn burn_tokens<'info>( + token_program: &Interface<'info, TokenInterface>, + destination: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + signature_seeds: &[&[u8]], + amount: u64, + mint: &InterfaceAccount<'info, Mint>, +) -> Result<()> { + let signers = &[signature_seeds]; + + let mint_account_info = mint.to_account_info(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = Burn { + mint: mint_account_info, + from: destination.to_account_info(), + authority: authority.to_account_info(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + token_interface::burn(cpi_context, amount) +} + pub fn validate_mint_fee(account_info: &AccountInfo) -> Result<()> { let mint_data = account_info.try_borrow_data()?; let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index d09e3bfd1c..ab38f477bc 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -1,5 +1,4 @@ use anchor_lang::prelude::*; - pub type DriftResult = std::result::Result; #[error_code] @@ -655,6 +654,44 @@ pub enum ErrorCode { CannotRevokeBuilderWithOpenOrders, #[msg("Unable to load builder account")] UnableToLoadRevenueShareAccount, + #[msg("Invalid Constituent")] + InvalidConstituent, + #[msg("Invalid Amm Constituent Mapping argument")] + InvalidAmmConstituentMappingArgument, + #[msg("Constituent not found")] + ConstituentNotFound, + #[msg("Constituent could not load")] + ConstituentCouldNotLoad, + #[msg("Constituent wrong mutability")] + ConstituentWrongMutability, + #[msg("Wrong number of constituents passed to instruction")] + WrongNumberOfConstituents, + #[msg("Insufficient constituent token balance")] + InsufficientConstituentTokenBalance, + #[msg("Amm Cache data too stale")] + AMMCacheStale, + #[msg("LP Pool AUM not updated recently")] + LpPoolAumDelayed, + #[msg("Constituent oracle is stale")] + ConstituentOracleStale, + #[msg("LP Invariant failed")] + LpInvariantFailed, + #[msg("Invalid constituent derivative weights")] + InvalidConstituentDerivativeWeights, + #[msg("Max DLP AUM Breached")] + MaxDlpAumBreached, + #[msg("Settle Lp Pool Disabled")] + SettleLpPoolDisabled, + #[msg("Mint/Redeem Lp Pool Disabled")] + MintRedeemLpPoolDisabled, + #[msg("Settlement amount exceeded")] + LpPoolSettleInvariantBreached, + #[msg("Invalid constituent operation")] + InvalidConstituentOperation, + #[msg("Unauthorized for operation")] + Unauthorized, + #[msg("Invalid Lp Pool Id for Operation")] + InvalidLpPoolId, } #[macro_export] diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index e3dce8aa73..a37dd452fb 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -108,6 +108,11 @@ pub mod amm_spread_adjust_wallet { declare_id!("1ucYHAGrBbi1PaecC4Ptq5ocZLWGLBmbGWysoDGNB1N"); } +pub mod lp_pool_swap_wallet { + use solana_program::declare_id; + declare_id!("25qbsE2oWri76c9a86ubn17NKKdo6Am4HXD2Jm8vT8K4"); +} + pub mod dflow_mainnet_aggregator_4 { use solana_program::declare_id; declare_id!("DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"); @@ -117,3 +122,17 @@ pub mod titan_mainnet_argos_v1 { use solana_program::declare_id; declare_id!("T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT"); } + +pub mod lp_pool_hot_wallet { + use solana_program::declare_id; + declare_id!("GP9qHLX8rx4BgRULGPV1poWQPdGuzbxGbvTB12DfmwFk"); +} + +pub const WHITELISTED_SWAP_PROGRAMS: &[solana_program::pubkey::Pubkey] = &[ + serum_program::id(), + jupiter_mainnet_3::id(), + jupiter_mainnet_4::id(), + jupiter_mainnet_6::id(), + dflow_mainnet_aggregator_4::id(), + titan_mainnet_argos_v1::id(), +]; diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 18cc6d3872..07b0913f48 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1,6 +1,3 @@ -use std::convert::{identity, TryInto}; -use std::mem::size_of; - use crate::{msg, FeatureBitFlags}; use anchor_lang::prelude::*; use anchor_spl::token_2022::Token2022; @@ -9,21 +6,27 @@ use phoenix::quantities::WrapperU64; use pyth_solana_receiver_sdk::cpi::accounts::InitPriceUpdate; use pyth_solana_receiver_sdk::program::PythSolanaReceiver; use serum_dex::state::ToAlignedBytes; +use std::convert::{identity, TryInto}; +use std::mem::size_of; +use crate::controller; use crate::controller::token::{close_vault, initialize_immutable_owner, initialize_token_account}; use crate::error::ErrorCode; +use crate::get_then_update_id; use crate::ids::{admin_hot_wallet, amm_spread_adjust_wallet, mm_oracle_crank_wallet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::load; use crate::math::casting::Cast; use crate::math::constants::{ AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO, DEFAULT_LIQUIDATION_MARGIN_BUFFER_RATIO, - FEE_POOL_TO_REVENUE_POOL_THRESHOLD, GOV_SPOT_MARKET_INDEX, IF_FACTOR_PRECISION, - INSURANCE_A_MAX, INSURANCE_B_MAX, INSURANCE_C_MAX, INSURANCE_SPECULATIVE_MAX, - LIQUIDATION_FEE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, MAX_SQRT_K, - MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, - QUOTE_SPOT_MARKET_INDEX, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_IMF_PRECISION, - SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, TWENTY_FOUR_HOUR, + EPOCH_DURATION, FEE_ADJUSTMENT_MAX, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, GOV_SPOT_MARKET_INDEX, + IF_FACTOR_PRECISION, INSURANCE_A_MAX, INSURANCE_B_MAX, INSURANCE_C_MAX, + INSURANCE_SPECULATIVE_MAX, LIQUIDATION_FEE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, + MAX_SQRT_K, MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, + QUOTE_PRECISION_I64, QUOTE_SPOT_MARKET_INDEX, SPOT_BALANCE_PRECISION, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, + TWENTY_FOUR_HOUR, }; use crate::math::cp_curve::get_update_k_result; use crate::math::helpers::get_proportion_u128; @@ -33,7 +36,9 @@ use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::math::{amm, bn}; +use crate::math_error; use crate::optional_accounts::get_token_mint; +use crate::state::amm_cache::{AmmCache, CacheInfo, AMM_POSITIONS_CACHE}; use crate::state::events::{ CurveRecord, DepositDirection, DepositExplanation, DepositRecord, SpotMarketVaultDepositRecord, }; @@ -67,7 +72,9 @@ use crate::state::spot_market::{ TokenProgramFlag, }; use crate::state::spot_market_map::get_writable_spot_market_set; -use crate::state::state::{ExchangeStatus, FeeStructure, OracleGuardRails, State}; +use crate::state::state::{ + ExchangeStatus, FeeStructure, LpPoolFeatureBitFlags, OracleGuardRails, State, +}; use crate::state::traits::Size; use crate::state::user::{User, UserStats}; use crate::validate; @@ -75,12 +82,9 @@ use crate::validation::fee_structure::validate_fee_structure; use crate::validation::margin::{validate_margin, validate_margin_weights}; use crate::validation::perp_market::validate_perp_market; use crate::validation::spot_market::validate_borrow_rate; -use crate::{controller, QUOTE_PRECISION_I64}; -use crate::{get_then_update_id, EPOCH_DURATION}; -use crate::{load, FEE_ADJUSTMENT_MAX}; use crate::{load_mut, PTYH_PRICE_FEED_SEED_PREFIX}; use crate::{math, safe_decrement, safe_increment}; -use crate::{math_error, SPOT_BALANCE_PRECISION}; + use anchor_spl::token_2022::spl_token_2022::extension::transfer_hook::TransferHook; use anchor_spl::token_2022::spl_token_2022::extension::{ BaseStateWithExtensions, StateWithExtensions, @@ -117,7 +121,8 @@ pub fn handle_initialize(ctx: Context) -> Result<()> { max_number_of_sub_accounts: 0, max_initialize_user_fee: 0, feature_bit_flags: 0, - padding: [0; 9], + lp_pool_feature_bit_flags: 0, + padding: [0; 8], }; Ok(()) @@ -158,6 +163,16 @@ pub fn handle_initialize_spot_market( )?; } + let is_token_2022 = *ctx.accounts.spot_market_mint.to_account_info().owner == Token2022::id(); + if is_token_2022 { + initialize_immutable_owner(&ctx.accounts.token_program, &ctx.accounts.spot_market_vault)?; + + initialize_immutable_owner( + &ctx.accounts.token_program, + &ctx.accounts.insurance_fund_vault, + )?; + } + initialize_token_account( &ctx.accounts.token_program, &ctx.accounts.spot_market_vault, @@ -699,6 +714,7 @@ pub fn handle_initialize_perp_market( curve_update_intensity: u8, amm_jit_intensity: u8, name: [u8; 32], + lp_pool_id: u8, ) -> Result<()> { msg!("perp market {}", market_index); let perp_market_pubkey = ctx.accounts.perp_market.to_account_info().key; @@ -981,9 +997,13 @@ pub fn handle_initialize_perp_market( high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding1: 0, + lp_fee_transfer_scalar: 1, + lp_status: 0, + lp_exchange_fee_excluscion_scalar: 0, + lp_paused_operations: 0, last_fill_price: 0, - padding: [0; 24], + lp_pool_id, + padding: [0; 23], amm: AMM { oracle: *ctx.accounts.oracle.key, oracle_source, @@ -1087,6 +1107,17 @@ pub fn handle_initialize_perp_market( safe_increment!(state.number_of_markets, 1); + let amm_cache = &mut ctx.accounts.amm_cache; + let current_len = amm_cache.cache.len(); + amm_cache + .cache + .resize_with(current_len + 1, CacheInfo::default); + let current_market_info = amm_cache.cache.get_mut(current_len).unwrap(); + current_market_info.slot = clock_slot; + current_market_info.oracle = perp_market.amm.oracle; + current_market_info.oracle_source = u8::from(perp_market.amm.oracle_source); + amm_cache.validate(state)?; + controller::amm::update_concentration_coef(perp_market, concentration_coef_scale)?; crate::dlog!(oracle_price); @@ -1102,6 +1133,17 @@ pub fn handle_initialize_perp_market( Ok(()) } +pub fn handle_initialize_amm_cache(ctx: Context) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + let state = &ctx.accounts.state; + amm_cache + .cache + .resize_with(state.number_of_markets as usize, CacheInfo::default); + amm_cache.bump = ctx.bumps.amm_cache; + + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -1958,6 +2000,12 @@ pub fn handle_deposit_into_perp_market_fee_pool<'c: 'info, 'info>( let quote_spot_market = &mut load_mut!(ctx.accounts.quote_spot_market)?; + controller::spot_balance::update_spot_market_cumulative_interest( + &mut *quote_spot_market, + None, + Clock::get()?.unix_timestamp, + )?; + controller::spot_balance::update_spot_balances( amount.cast::()?, &SpotBalanceType::Deposit, @@ -2783,6 +2831,25 @@ pub fn handle_update_perp_liquidation_fee( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_lp_pool_id( + ctx: Context, + lp_pool_id: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + + msg!( + "updating perp market {} lp pool id: {} -> {}", + perp_market.market_index, + perp_market.lp_pool_id, + lp_pool_id + ); + perp_market.lp_pool_id = lp_pool_id; + Ok(()) +} + #[access_control( spot_market_valid(&ctx.accounts.spot_market) )] @@ -3308,6 +3375,7 @@ pub fn handle_update_perp_market_contract_tier( ); perp_market.contract_tier = contract_tier; + Ok(()) } @@ -3645,6 +3713,7 @@ pub fn handle_update_perp_market_oracle( skip_invariant_check: bool, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let amm_cache = &mut ctx.accounts.amm_cache; msg!("perp market {}", perp_market.market_index); let clock = Clock::get()?; @@ -3723,6 +3792,8 @@ pub fn handle_update_perp_market_oracle( perp_market.amm.oracle = oracle; perp_market.amm.oracle_source = oracle_source; + amm_cache.update_perp_market_fields(perp_market)?; + Ok(()) } @@ -3873,6 +3944,40 @@ pub fn handle_update_perp_market_min_order_size( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_market_lp_pool_fee_transfer_scalar( + ctx: Context, + optional_lp_fee_transfer_scalar: Option, + optional_lp_net_pnl_transfer_scalar: Option, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + + if let Some(lp_fee_transfer_scalar) = optional_lp_fee_transfer_scalar { + msg!( + "perp_market.: {:?} -> {:?}", + perp_market.lp_fee_transfer_scalar, + lp_fee_transfer_scalar + ); + + perp_market.lp_fee_transfer_scalar = lp_fee_transfer_scalar; + } + + if let Some(lp_net_pnl_transfer_scalar) = optional_lp_net_pnl_transfer_scalar { + msg!( + "perp_market.: {:?} -> {:?}", + perp_market.lp_exchange_fee_excluscion_scalar, + lp_net_pnl_transfer_scalar + ); + + perp_market.lp_exchange_fee_excluscion_scalar = lp_net_pnl_transfer_scalar; + } + + Ok(()) +} + #[access_control( spot_market_valid(&ctx.accounts.spot_market) )] @@ -4147,6 +4252,16 @@ pub fn handle_update_perp_market_protected_maker_params( Ok(()) } +pub fn handle_update_perp_market_lp_pool_paused_operations( + ctx: Context, + lp_paused_operations: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + perp_market.lp_paused_operations = lp_paused_operations; + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -4673,8 +4788,8 @@ pub fn handle_update_protected_maker_mode_config( ) -> Result<()> { let mut config = load_mut!(ctx.accounts.protected_maker_mode_config)?; - if current_users.is_some() { - config.current_users = current_users.unwrap(); + if let Some(users) = current_users { + config.current_users = users; } config.max_users = max_users; config.reduce_only = reduce_only as u8; @@ -5056,6 +5171,75 @@ pub fn handle_update_feature_bit_flags_builder_referral( Ok(()) } +pub fn handle_update_feature_bit_flags_settle_lp_pool( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can re-enable after kill switch" + )?; + + msg!("Setting first bit to 1, enabling settle LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags | (LpPoolFeatureBitFlags::SettleLpPool as u8); + } else { + msg!("Setting first bit to 0, disabling settle LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags & !(LpPoolFeatureBitFlags::SettleLpPool as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_swap_lp_pool( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can re-enable after kill switch" + )?; + + msg!("Setting second bit to 1, enabling swapping with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags | (LpPoolFeatureBitFlags::SwapLpPool as u8); + } else { + msg!("Setting second bit to 0, disabling swapping with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags & !(LpPoolFeatureBitFlags::SwapLpPool as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_mint_redeem_lp_pool( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can re-enable after kill switch" + )?; + + msg!("Setting third bit to 1, enabling minting and redeeming with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags | (LpPoolFeatureBitFlags::MintRedeemLpPool as u8); + } else { + msg!("Setting third bit to 0, disabling minting and redeeming with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags & !(LpPoolFeatureBitFlags::MintRedeemLpPool as u8); + } + Ok(()) +} + #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] @@ -5300,12 +5484,41 @@ pub struct InitializePerpMarket<'info> { payer = admin )] pub perp_market: AccountLoader<'info, PerpMarket>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + realloc = AmmCache::space(amm_cache.cache.len() + 1_usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_cache: Box>, /// CHECK: checked in `initialize_perp_market` pub oracle: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct InitializeAmmCache<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account( + init, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + space = AmmCache::space(state.number_of_markets as usize), + bump, + payer = admin + )] + pub amm_cache: Box>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + #[derive(Accounts)] pub struct DeleteInitializedPerpMarket<'info> { #[account(mut)] @@ -5552,6 +5765,12 @@ pub struct AdminUpdatePerpMarketOracle<'info> { pub oracle: AccountInfo<'info>, /// CHECK: checked in `admin_update_perp_market_oracle` ix constraint pub old_oracle: AccountInfo<'info>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, } #[derive(Accounts)] diff --git a/programs/drift/src/instructions/if_staker.rs b/programs/drift/src/instructions/if_staker.rs index 1d15b5ff6f..3e2381eb5f 100644 --- a/programs/drift/src/instructions/if_staker.rs +++ b/programs/drift/src/instructions/if_staker.rs @@ -6,6 +6,8 @@ use crate::error::ErrorCode; use crate::ids::{admin_hot_wallet, if_rebalance_wallet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::load_mut; +use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; use crate::optional_accounts::get_token_mint; use crate::state::insurance_fund_stake::{InsuranceFundStake, ProtocolIfSharesTransferConfig}; use crate::state::paused_operations::InsuranceFundOperation; @@ -23,7 +25,6 @@ use crate::{ spot_market_map::get_writable_spot_market_set_from_many, }, }; -use crate::{load_mut, QUOTE_SPOT_MARKET_INDEX}; use anchor_lang::solana_program::sysvar::instructions; use super::optional_accounts::get_token_interface; diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 0fc1fc3850..c0c465395a 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -16,10 +16,12 @@ use crate::controller::liquidation::{ liquidate_spot_with_swap_begin, liquidate_spot_with_swap_end, }; use crate::controller::orders::cancel_orders; +use crate::controller::orders::validate_market_within_price_band; use crate::controller::position::PositionDirection; use crate::controller::spot_balance::update_spot_balances; use crate::controller::token::{receive, send_from_program_vault}; use crate::error::ErrorCode; +use crate::get_then_update_id; use crate::ids::admin_hot_wallet; use crate::ids::{ dflow_mainnet_aggregator_4, jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, @@ -28,15 +30,23 @@ use crate::ids::{ use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::load_mut; use crate::math::casting::Cast; -use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; +use crate::math::constants::{ + GOV_SPOT_MARKET_INDEX, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, +}; +use crate::math::lp_pool::perp_lp_pool_settlement; use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::optional_accounts::{get_token_mint, update_prelaunch_oracle}; +use crate::signer::get_signer_seeds; +use crate::state::amm_cache::CacheInfo; +use crate::state::events::LPSettleRecord; use crate::state::events::{DeleteUserRecord, OrderActionExplanation, SignedMsgOrderRecord}; use crate::state::fill_mode::FillMode; use crate::state::fulfillment_params::drift::MatchFulfillmentParams; @@ -45,8 +55,13 @@ use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams; use crate::state::fulfillment_params::serum::SerumFulfillmentParams; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use crate::state::insurance_fund_stake::InsuranceFundStake; +use crate::state::lp_pool::Constituent; +use crate::state::lp_pool::LPPool; +use crate::state::lp_pool::CONSTITUENT_PDA_SEED; +use crate::state::lp_pool::SETTLE_AMM_ORACLE_MAX_DELAY; use crate::state::oracle_map::OracleMap; use crate::state::order_params::{OrderParams, PlaceOrderOptions}; +use crate::state::paused_operations::PerpLpOperation; use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::{ContractType, MarketStatus, PerpMarket}; use crate::state::perp_market_map::{ @@ -72,14 +87,13 @@ use crate::state::user::{ MarginMode, MarketType, OrderStatus, OrderTriggerCondition, OrderType, User, UserStats, }; use crate::state::user_map::{load_user_map, load_user_maps, UserMap, UserStatsMap}; +use crate::state::zero_copy::AccountZeroCopyMut; +use crate::state::zero_copy::ZeroCopyLoader; +use crate::validate; use crate::validation::sig_verification::verify_and_decode_ed25519_msg; use crate::validation::user::{validate_user_deletion, validate_user_is_idle}; -use crate::{ - controller, load, math, print_error, safe_decrement, OracleSource, GOV_SPOT_MARKET_INDEX, -}; -use crate::{load_mut, QUOTE_PRECISION_U64}; +use crate::{controller, load, math, print_error, safe_decrement, OracleSource}; use crate::{math_error, ID}; -use crate::{validate, QUOTE_PRECISION_I128}; use anchor_spl::associated_token::AssociatedToken; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; @@ -3299,6 +3313,383 @@ pub fn handle_pause_spot_market_deposit_withdraw( Ok(()) } +pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>, +) -> Result<()> { + use perp_lp_pool_settlement::*; + + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + let now = Clock::get()?.unix_timestamp; + + if !state.allow_settle_lp_pool() { + msg!("settle lp pool disabled"); + return Err(ErrorCode::SettleLpPoolDisabled.into()); + } + + let mut amm_cache: AccountZeroCopyMut<'_, CacheInfo, _> = + ctx.accounts.amm_cache.load_zc_mut()?; + let quote_market = &mut ctx.accounts.quote_market.load_mut()?; + let mut quote_constituent = ctx.accounts.constituent.load_mut()?; + let lp_pool_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + let tvl_before = quote_market + .get_tvl()? + .safe_add(quote_constituent.vault_token_balance as u128)?; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map: _, + oracle_map: _, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &MarketSet::new(), + slot, + None, + )?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut *quote_market, + None, + now, + )?; + + for (_, perp_market_loader) in perp_market_map.0.iter() { + let mut perp_market = perp_market_loader.load_mut()?; + if lp_pool.lp_pool_id != perp_market.lp_pool_id { + msg!( + "Perp market {} does not have the same lp pool id as the lp pool being settled to: {} != {}", + perp_market.market_index, + perp_market.lp_pool_id, + lp_pool.lp_pool_id + ); + return Err(ErrorCode::InvalidLpPoolId.into()); + } + + if perp_market.lp_status == 0 + || PerpLpOperation::is_operation_paused( + perp_market.lp_paused_operations, + PerpLpOperation::SettleQuoteOwed, + ) + { + continue; + } + + let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + + // Early validation checks + if slot.saturating_sub(cached_info.oracle_slot) > SETTLE_AMM_ORACLE_MAX_DELAY { + msg!( + "Skipping settling perp market {} to dlp because oracle slot is not up to date", + perp_market.market_index + ); + continue; + } + + validate_market_within_price_band(&perp_market, state, cached_info.oracle_price)?; + + if perp_market.is_operation_paused(PerpOperation::SettlePnl) { + msg!( + "Cannot settle pnl under current market = {} status", + perp_market.market_index + ); + continue; + } + + if cached_info.slot != slot { + msg!("Skipping settling perp market {} to lp pool because amm cache was not updated in the same slot", + perp_market.market_index); + return Err(ErrorCode::AMMCacheStale.into()); + } + + quote_constituent.sync_token_balance(ctx.accounts.constituent_quote_token_account.amount); + + // Create settlement context + let settlement_ctx = SettlementContext { + quote_owed_from_lp: cached_info.quote_owed_from_lp_pool, + quote_constituent_token_balance: quote_constituent.vault_token_balance, + fee_pool_balance: get_token_amount( + perp_market.amm.fee_pool.scaled_balance, + quote_market, + &SpotBalanceType::Deposit, + )?, + pnl_pool_balance: get_token_amount( + perp_market.pnl_pool.scaled_balance, + quote_market, + &SpotBalanceType::Deposit, + )?, + quote_market, + max_settle_quote_amount: lp_pool.max_settle_quote_amount, + }; + + // Calculate settlement + let settlement_result = calculate_settlement_amount(&settlement_ctx)?; + validate_settlement_amount( + &settlement_ctx, + &settlement_result, + &perp_market, + quote_market, + )?; + + if settlement_result.direction == SettlementDirection::None { + continue; + } + + // Execute token transfer + match settlement_result.direction { + SettlementDirection::FromLpPool => { + execute_token_transfer( + &ctx.accounts.token_program, + &ctx.accounts.constituent_quote_token_account, + &ctx.accounts.quote_token_vault, + &ctx.accounts + .constituent_quote_token_account + .to_account_info(), + &Constituent::get_vault_signer_seeds( + "e_constituent.lp_pool, + "e_constituent.spot_market_index, + "e_constituent.vault_bump, + ), + settlement_result.amount_transferred, + Some(remaining_accounts_iter), + )?; + } + SettlementDirection::ToLpPool => { + execute_token_transfer( + &ctx.accounts.token_program, + &ctx.accounts.quote_token_vault, + &ctx.accounts.constituent_quote_token_account, + &ctx.accounts.drift_signer, + &get_signer_seeds(&state.signer_nonce), + settlement_result.amount_transferred, + Some(remaining_accounts_iter), + )?; + } + SettlementDirection::None => unreachable!(), + } + + // Update market pools + update_perp_market_pools_and_quote_market_balance( + &mut perp_market, + &settlement_result, + quote_market, + )?; + + // Emit settle event + let record_id = get_then_update_id!(lp_pool, settle_id); + emit!(LPSettleRecord { + record_id, + last_ts: cached_info.last_settle_ts, + last_slot: cached_info.last_settle_slot, + slot, + ts: now, + perp_market_index: perp_market.market_index, + settle_to_lp_amount: match settlement_result.direction { + SettlementDirection::FromLpPool => settlement_result + .amount_transferred + .cast::()? + .saturating_mul(-1), + SettlementDirection::ToLpPool => + settlement_result.amount_transferred.cast::()?, + SettlementDirection::None => unreachable!(), + }, + perp_amm_pnl_delta: cached_info + .last_net_pnl_pool_token_amount + .safe_sub(cached_info.last_settle_amm_pnl)? + .cast::()?, + perp_amm_ex_fee_delta: cached_info + .last_exchange_fees + .safe_sub(cached_info.last_settle_amm_ex_fees)? + .cast::()?, + lp_aum: lp_pool.last_aum, + lp_price: lp_pool.get_price(lp_pool.token_supply)?, + lp_pool: lp_pool_key, + }); + + // Calculate new quote owed amount + let new_quote_owed = match settlement_result.direction { + SettlementDirection::FromLpPool => cached_info + .quote_owed_from_lp_pool + .safe_sub(settlement_result.amount_transferred as i64)?, + SettlementDirection::ToLpPool => cached_info + .quote_owed_from_lp_pool + .safe_add(settlement_result.amount_transferred as i64)?, + SettlementDirection::None => cached_info.quote_owed_from_lp_pool, + }; + + // Update cache info + update_cache_info(cached_info, &settlement_result, new_quote_owed, slot, now)?; + + // Update LP pool stats + match settlement_result.direction { + SettlementDirection::FromLpPool => { + lp_pool.cumulative_quote_sent_to_perp_markets = lp_pool + .cumulative_quote_sent_to_perp_markets + .saturating_add(settlement_result.amount_transferred as u128); + } + SettlementDirection::ToLpPool => { + lp_pool.cumulative_quote_received_from_perp_markets = lp_pool + .cumulative_quote_received_from_perp_markets + .saturating_add(settlement_result.amount_transferred as u128); + } + SettlementDirection::None => {} + } + + // Sync constituent token balance + let constituent_token_account = &mut ctx.accounts.constituent_quote_token_account; + constituent_token_account.reload()?; + quote_constituent.sync_token_balance(constituent_token_account.amount); + } + + // Final validation + ctx.accounts.quote_token_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + quote_market, + ctx.accounts.quote_token_vault.amount, + )?; + + let tvl_after = quote_market + .get_tvl()? + .safe_add(quote_constituent.vault_token_balance as u128)?; + + validate!( + tvl_before.safe_sub(tvl_after)? <= 10, + ErrorCode::LpPoolSettleInvariantBreached, + "LP pool settlement would decrease TVL: {} -> {}", + tvl_before, + tvl_after + )?; + + Ok(()) +} + +pub fn handle_update_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateAmmCache<'info>>, +) -> Result<()> { + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mut amm_cache: AccountZeroCopyMut<'_, CacheInfo, _> = + ctx.accounts.amm_cache.load_zc_mut()?; + + let state = &ctx.accounts.state; + let quote_market = ctx.accounts.quote_market.load()?; + + let AccountMaps { + perp_market_map, + spot_market_map: _, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &MarketSet::new(), + Clock::get()?.slot, + None, + )?; + let slot = Clock::get()?.slot; + + for (_, perp_market_loader) in perp_market_map.0.iter() { + let perp_market = perp_market_loader.load()?; + if perp_market.lp_status == 0 { + continue; + } + let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + + validate!( + perp_market.oracle_id() == cached_info.oracle_id()?, + ErrorCode::DefaultError, + "oracle id mismatch between amm cache and perp market" + )?; + + let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; + let mm_oracle_price_data = perp_market.get_mm_oracle_price_data( + *oracle_data, + slot, + &ctx.accounts.state.oracle_guard_rails.validity, + )?; + + cached_info.update_perp_market_fields(&perp_market)?; + cached_info.try_update_oracle_info( + slot, + &mm_oracle_price_data, + &perp_market, + &state.oracle_guard_rails, + )?; + + if perp_market.lp_status != 0 + && !PerpLpOperation::is_operation_paused( + perp_market.lp_paused_operations, + PerpLpOperation::TrackAmmRevenue, + ) + { + amm_cache.update_amount_owed_from_lp_pool(&perp_market, "e_market)?; + } + } + + Ok(()) +} + +#[derive(Accounts)] +pub struct SettleAmmPnlToLp<'info> { + pub state: Box>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + constraint = keeper.key() == crate::ids::lp_pool_swap_wallet::id() || keeper.key() == admin_hot_wallet::id() || keeper.key() == state.admin.key(), + )] + pub keeper: Signer<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, + #[account( + mut, + owner = crate::ID, + seeds = [b"spot_market", QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump, + )] + pub quote_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + owner = crate::ID, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump = constituent.load()?.bump, + constraint = constituent.load()?.mint.eq("e_market.load()?.mint) + )] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.vault, + )] + pub constituent_quote_token_account: Box>, + #[account( + mut, + address = quote_market.load()?.vault, + token::authority = drift_signer, + )] + pub quote_token_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateAmmCache<'info> { + #[account(mut)] + pub keeper: Signer<'info>, + pub state: Box>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, + #[account( + owner = crate::ID, + seeds = [b"spot_market", QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump, + )] + pub quote_market: AccountLoader<'info, SpotMarket>, +} + #[derive(Accounts)] pub struct FillOrder<'info> { pub state: Box>, diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs new file mode 100644 index 0000000000..28e6026a44 --- /dev/null +++ b/programs/drift/src/instructions/lp_admin.rs @@ -0,0 +1,1392 @@ +use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; +use crate::error::ErrorCode; +use crate::ids::{lp_pool_hot_wallet, lp_pool_swap_wallet, WHITELISTED_SWAP_PROGRAMS}; +use crate::instructions::optional_accounts::{get_token_mint, load_maps, AccountMaps}; +use crate::math::constants::{PRICE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX}; +use crate::math::safe_math::SafeMath; +use crate::state::amm_cache::{AmmCache, CacheInfo, AMM_POSITIONS_CACHE}; +use crate::state::lp_pool::{ + AmmConstituentDatum, AmmConstituentMapping, Constituent, ConstituentCorrelations, + ConstituentTargetBase, LPPool, TargetsDatum, AMM_MAP_PDA_SEED, + CONSTITUENT_CORRELATIONS_PDA_SEED, CONSTITUENT_PDA_SEED, CONSTITUENT_TARGET_BASE_PDA_SEED, + CONSTITUENT_VAULT_PDA_SEED, +}; +use crate::state::perp_market::PerpMarket; +use crate::state::perp_market_map::MarketSet; +use crate::state::spot_market::SpotMarket; +use crate::state::state::State; +use crate::validate; +use crate::{controller, load_mut}; +use anchor_lang::prelude::*; +use anchor_lang::Discriminator; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::Token; +use anchor_spl::token_2022::Token2022; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + +use crate::ids::{lighthouse, marinade_mainnet}; + +use crate::state::traits::Size; +use solana_program::sysvar::instructions; + +use super::optional_accounts::get_token_interface; + +pub fn handle_initialize_lp_pool( + ctx: Context, + lp_pool_id: u8, + min_mint_fee: i64, + max_aum: u128, + max_settle_quote_amount_per_market: u64, + whitelist_mint: Pubkey, +) -> Result<()> { + let lp_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_init()?; + let mint = &ctx.accounts.mint; + + validate!( + mint.decimals == 6, + ErrorCode::DefaultError, + "lp mint must have 6 decimals" + )?; + + validate!( + mint.mint_authority == Some(lp_key).into(), + ErrorCode::DefaultError, + "lp mint must have drift_signer as mint authority" + )?; + + *lp_pool = LPPool { + pubkey: ctx.accounts.lp_pool.key(), + mint: mint.key(), + constituent_target_base: ctx.accounts.constituent_target_base.key(), + constituent_correlations: ctx.accounts.constituent_correlations.key(), + constituents: 0, + max_aum, + last_aum: 0, + last_aum_slot: 0, + max_settle_quote_amount: max_settle_quote_amount_per_market, + _padding: 0, + total_mint_redeem_fees_paid: 0, + bump: ctx.bumps.lp_pool, + min_mint_fee, + token_supply: 0, + mint_redeem_id: 1, + settle_id: 1, + quote_consituent_index: 0, + cumulative_quote_sent_to_perp_markets: 0, + cumulative_quote_received_from_perp_markets: 0, + gamma_execution: 2, + volatility: 4, + xi: 2, + target_oracle_delay_fee_bps_per_10_slots: 0, + target_position_delay_fee_bps_per_10_slots: 0, + lp_pool_id, + padding: [0u8; 174], + whitelist_mint, + }; + + let amm_constituent_mapping = &mut ctx.accounts.amm_constituent_mapping; + amm_constituent_mapping.lp_pool = ctx.accounts.lp_pool.key(); + amm_constituent_mapping.bump = ctx.bumps.amm_constituent_mapping; + amm_constituent_mapping + .weights + .resize_with(0 as usize, AmmConstituentDatum::default); + amm_constituent_mapping.validate()?; + + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + constituent_target_base.lp_pool = ctx.accounts.lp_pool.key(); + constituent_target_base.bump = ctx.bumps.constituent_target_base; + constituent_target_base + .targets + .resize_with(0 as usize, TargetsDatum::default); + constituent_target_base.validate()?; + + let consituent_correlations = &mut ctx.accounts.constituent_correlations; + consituent_correlations.lp_pool = ctx.accounts.lp_pool.key(); + consituent_correlations.bump = ctx.bumps.constituent_correlations; + consituent_correlations.correlations.resize(0 as usize, 0); + consituent_correlations.validate()?; + + Ok(()) +} + +pub fn handle_initialize_constituent<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, + spot_market_index: u16, + decimals: u8, + max_weight_deviation: i64, + swap_fee_min: i64, + swap_fee_max: i64, + max_borrow_token_amount: u64, + oracle_staleness_threshold: u64, + cost_to_trade_bps: i32, + constituent_derivative_index: Option, + constituent_derivative_depeg_threshold: u64, + derivative_weight: u64, + volatility: u64, + gamma_execution: u8, + gamma_inventory: u8, + xi: u8, + new_constituent_correlations: Vec, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_init()?; + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + let current_len = constituent_target_base.targets.len(); + + constituent_target_base + .targets + .resize_with((current_len + 1) as usize, TargetsDatum::default); + + let new_target = constituent_target_base + .targets + .get_mut(current_len) + .unwrap(); + new_target.cost_to_trade_bps = cost_to_trade_bps; + constituent_target_base.validate()?; + + msg!( + "initializing constituent {} with spot market index {}", + lp_pool.constituents, + spot_market_index + ); + + validate!( + derivative_weight <= PRICE_PRECISION_U64, + ErrorCode::InvalidConstituent, + "stablecoin_weight must be between 0 and 1", + )?; + + if let Some(constituent_derivative_index) = constituent_derivative_index { + validate!( + constituent_derivative_index < lp_pool.constituents as i16, + ErrorCode::InvalidConstituent, + "constituent_derivative_index must be less than lp_pool.constituents" + )?; + } + + constituent.spot_market_index = spot_market_index; + constituent.constituent_index = lp_pool.constituents; + constituent.decimals = decimals; + constituent.max_weight_deviation = max_weight_deviation; + constituent.swap_fee_min = swap_fee_min; + constituent.swap_fee_max = swap_fee_max; + constituent.oracle_staleness_threshold = oracle_staleness_threshold; + constituent.pubkey = ctx.accounts.constituent.key(); + constituent.mint = ctx.accounts.spot_market_mint.key(); + constituent.vault = ctx.accounts.constituent_vault.key(); + constituent.bump = ctx.bumps.constituent; + constituent.vault_bump = ctx.bumps.constituent_vault; + constituent.max_borrow_token_amount = max_borrow_token_amount; + constituent.lp_pool = lp_pool.pubkey; + constituent.constituent_index = (constituent_target_base.targets.len() - 1) as u16; + constituent.next_swap_id = 1; + constituent.constituent_derivative_index = constituent_derivative_index.unwrap_or(-1); + constituent.constituent_derivative_depeg_threshold = constituent_derivative_depeg_threshold; + constituent.derivative_weight = derivative_weight; + constituent.volatility = volatility; + constituent.gamma_execution = gamma_execution; + constituent.gamma_inventory = gamma_inventory; + constituent.spot_balance.market_index = spot_market_index; + constituent.xi = xi; + lp_pool.constituents += 1; + + if constituent.spot_market_index == QUOTE_SPOT_MARKET_INDEX { + lp_pool.quote_consituent_index = constituent.constituent_index; + } + + let constituent_correlations = &mut ctx.accounts.constituent_correlations; + validate!( + new_constituent_correlations.len() as u16 == lp_pool.constituents - 1, + ErrorCode::InvalidConstituent, + "expected {} correlations, got {}", + lp_pool.constituents, + new_constituent_correlations.len() + )?; + constituent_correlations.add_new_constituent(&new_constituent_correlations)?; + + Ok(()) +} + +pub fn handle_update_constituent_status<'info>( + ctx: Context, + new_status: u8, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + msg!( + "constituent status: {:?} -> {:?}", + constituent.status, + new_status + ); + constituent.status = new_status; + Ok(()) +} + +pub fn handle_update_constituent_paused_operations<'info>( + ctx: Context, + paused_operations: u8, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + msg!( + "constituent paused operations: {:?} -> {:?}", + constituent.paused_operations, + paused_operations + ); + constituent.paused_operations = paused_operations; + Ok(()) +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ConstituentParams { + pub max_weight_deviation: Option, + pub swap_fee_min: Option, + pub swap_fee_max: Option, + pub max_borrow_token_amount: Option, + pub oracle_staleness_threshold: Option, + pub cost_to_trade_bps: Option, + pub constituent_derivative_index: Option, + pub derivative_weight: Option, + pub volatility: Option, + pub gamma_execution: Option, + pub gamma_inventory: Option, + pub xi: Option, +} + +pub fn handle_update_constituent_params<'info>( + ctx: Context, + constituent_params: ConstituentParams, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + if constituent.spot_balance.market_index != constituent.spot_market_index { + constituent.spot_balance.market_index = constituent.spot_market_index; + } + + if let Some(max_weight_deviation) = constituent_params.max_weight_deviation { + msg!( + "max_weight_deviation: {:?} -> {:?}", + constituent.max_weight_deviation, + max_weight_deviation + ); + constituent.max_weight_deviation = max_weight_deviation; + } + + if let Some(swap_fee_min) = constituent_params.swap_fee_min { + msg!( + "swap_fee_min: {:?} -> {:?}", + constituent.swap_fee_min, + swap_fee_min + ); + constituent.swap_fee_min = swap_fee_min; + } + + if let Some(swap_fee_max) = constituent_params.swap_fee_max { + msg!( + "swap_fee_max: {:?} -> {:?}", + constituent.swap_fee_max, + swap_fee_max + ); + constituent.swap_fee_max = swap_fee_max; + } + + if let Some(oracle_staleness_threshold) = constituent_params.oracle_staleness_threshold { + msg!( + "oracle_staleness_threshold: {:?} -> {:?}", + constituent.oracle_staleness_threshold, + oracle_staleness_threshold + ); + constituent.oracle_staleness_threshold = oracle_staleness_threshold; + } + + if let Some(cost_to_trade_bps) = constituent_params.cost_to_trade_bps { + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + + let target = constituent_target_base + .targets + .get_mut(constituent.constituent_index as usize) + .unwrap(); + + msg!( + "cost_to_trade: {:?} -> {:?}", + target.cost_to_trade_bps, + cost_to_trade_bps + ); + target.cost_to_trade_bps = cost_to_trade_bps; + } + + if let Some(derivative_weight) = constituent_params.derivative_weight { + msg!( + "derivative_weight: {:?} -> {:?}", + constituent.derivative_weight, + derivative_weight + ); + constituent.derivative_weight = derivative_weight; + } + + if let Some(constituent_derivative_index) = constituent_params.constituent_derivative_index { + msg!( + "constituent_derivative_index: {:?} -> {:?}", + constituent.constituent_derivative_index, + constituent_derivative_index + ); + constituent.constituent_derivative_index = constituent_derivative_index; + } + + if let Some(gamma_execution) = constituent_params.gamma_execution { + msg!( + "gamma_execution: {:?} -> {:?}", + constituent.gamma_execution, + gamma_execution + ); + constituent.gamma_execution = gamma_execution; + } + + if let Some(gamma_inventory) = constituent_params.gamma_inventory { + msg!( + "gamma_inventory: {:?} -> {:?}", + constituent.gamma_inventory, + gamma_inventory + ); + constituent.gamma_inventory = gamma_inventory; + } + + if let Some(xi) = constituent_params.xi { + msg!("xi: {:?} -> {:?}", constituent.xi, xi); + constituent.xi = xi; + } + + if let Some(max_borrow_token_amount) = constituent_params.max_borrow_token_amount { + msg!( + "max_borrow_token_amount: {:?} -> {:?}", + constituent.max_borrow_token_amount, + max_borrow_token_amount + ); + constituent.max_borrow_token_amount = max_borrow_token_amount; + } + + if let Some(volatility) = constituent_params.volatility { + msg!( + "volatility: {:?} -> {:?}", + constituent.volatility, + volatility + ); + constituent.volatility = volatility; + } + + Ok(()) +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct LpPoolParams { + pub max_settle_quote_amount: Option, + pub volatility: Option, + pub gamma_execution: Option, + pub xi: Option, + pub max_aum: Option, + pub whitelist_mint: Option, +} + +pub fn handle_update_lp_pool_params<'info>( + ctx: Context, + lp_pool_params: LpPoolParams, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + if let Some(max_settle_quote_amount) = lp_pool_params.max_settle_quote_amount { + msg!( + "max_settle_quote_amount: {:?} -> {:?}", + lp_pool.max_settle_quote_amount, + max_settle_quote_amount + ); + lp_pool.max_settle_quote_amount = max_settle_quote_amount; + } + + if let Some(volatility) = lp_pool_params.volatility { + msg!("volatility: {:?} -> {:?}", lp_pool.volatility, volatility); + lp_pool.volatility = volatility; + } + + if let Some(gamma_execution) = lp_pool_params.gamma_execution { + msg!( + "gamma_execution: {:?} -> {:?}", + lp_pool.gamma_execution, + gamma_execution + ); + lp_pool.gamma_execution = gamma_execution; + } + + if let Some(xi) = lp_pool_params.xi { + msg!("xi: {:?} -> {:?}", lp_pool.xi, xi); + lp_pool.xi = xi; + } + + if let Some(whitelist_mint) = lp_pool_params.whitelist_mint { + msg!( + "whitelist_mint: {:?} -> {:?}", + lp_pool.whitelist_mint, + whitelist_mint + ); + lp_pool.whitelist_mint = whitelist_mint; + } + + if let Some(max_aum) = lp_pool_params.max_aum { + validate!( + max_aum >= lp_pool.max_aum, + ErrorCode::DefaultError, + "new max_aum must be greater than or equal to current max_aum" + )?; + msg!("max_aum: {:?} -> {:?}", lp_pool.max_aum, max_aum); + lp_pool.max_aum = max_aum; + } + + Ok(()) +} + +pub fn handle_update_amm_constituent_mapping_data<'info>( + ctx: Context, + amm_constituent_mapping_data: Vec, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + + for datum in amm_constituent_mapping_data { + let existing_datum = amm_mapping.weights.iter().position(|existing_datum| { + existing_datum.perp_market_index == datum.perp_market_index + && existing_datum.constituent_index == datum.constituent_index + }); + + if existing_datum.is_none() { + msg!( + "AmmConstituentDatum not found for perp_market_index {} and constituent_index {}", + datum.perp_market_index, + datum.constituent_index + ); + return Err(ErrorCode::InvalidAmmConstituentMappingArgument.into()); + } + + amm_mapping.weights[existing_datum.unwrap()] = AmmConstituentDatum { + perp_market_index: datum.perp_market_index, + constituent_index: datum.constituent_index, + weight: datum.weight, + last_slot: Clock::get()?.slot, + ..AmmConstituentDatum::default() + }; + + msg!( + "Updated AmmConstituentDatum for perp_market_index {} and constituent_index {} to {}", + datum.perp_market_index, + datum.constituent_index, + datum.weight + ); + } + + amm_mapping.sort(); + + Ok(()) +} + +pub fn handle_remove_amm_constituent_mapping_data<'info>( + ctx: Context, + perp_market_index: u16, + constituent_index: u16, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + + let position = amm_mapping.weights.iter().position(|existing_datum| { + existing_datum.perp_market_index == perp_market_index + && existing_datum.constituent_index == constituent_index + }); + + if position.is_none() { + msg!( + "Not found for perp_market_index {} and constituent_index {}", + perp_market_index, + constituent_index + ); + return Err(ErrorCode::InvalidAmmConstituentMappingArgument.into()); + } + + amm_mapping.weights.remove(position.unwrap()); + amm_mapping.weights.shrink_to_fit(); + amm_mapping.sort(); + + Ok(()) +} + +pub fn handle_add_amm_constituent_data<'info>( + ctx: Context, + init_amm_constituent_mapping_data: Vec, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + let constituent_target_base = &ctx.accounts.constituent_target_base; + let state = &ctx.accounts.state; + let mut current_len = amm_mapping.weights.len(); + + for init_datum in init_amm_constituent_mapping_data { + let perp_market_index = init_datum.perp_market_index; + + validate!( + perp_market_index < state.number_of_markets, + ErrorCode::InvalidAmmConstituentMappingArgument, + "perp_market_index too large compared to number of markets" + )?; + + validate!( + (init_datum.constituent_index as usize) < constituent_target_base.targets.len(), + ErrorCode::InvalidAmmConstituentMappingArgument, + "constituent_index too large compared to number of constituents in target weights" + )?; + + let constituent_index = init_datum.constituent_index; + let mut datum = AmmConstituentDatum::default(); + datum.perp_market_index = perp_market_index; + datum.constituent_index = constituent_index; + datum.weight = init_datum.weight; + datum.last_slot = Clock::get()?.slot; + + // Check if the datum already exists + let exists = amm_mapping.weights.iter().any(|d| { + d.perp_market_index == perp_market_index && d.constituent_index == constituent_index + }); + + validate!( + !exists, + ErrorCode::InvalidAmmConstituentMappingArgument, + "AmmConstituentDatum already exists for perp_market_index {} and constituent_index {}", + perp_market_index, + constituent_index + )?; + + // Add the new datum to the mapping + current_len += 1; + amm_mapping.weights.resize(current_len, datum); + } + amm_mapping.sort(); + + Ok(()) +} + +pub fn handle_update_constituent_correlation_data<'info>( + ctx: Context, + index1: u16, + index2: u16, + corr: i64, +) -> Result<()> { + let constituent_correlations = &mut ctx.accounts.constituent_correlations; + constituent_correlations.set_correlation(index1, index2, corr)?; + + msg!( + "Updated correlation between constituent {} and {} to {}", + index1, + index2, + corr + ); + + constituent_correlations.validate()?; + + Ok(()) +} + +pub fn handle_begin_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + amount_in: u64, +) -> Result<()> { + // Check admin + let admin = &ctx.accounts.admin; + #[cfg(feature = "anchor-test")] + { + let state = &ctx.accounts.state; + validate!( + admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin, + ErrorCode::Unauthorized, + "Wrong signer for lp taker swap" + )?; + } + #[cfg(not(feature = "anchor-test"))] + validate!( + admin.key() == lp_pool_swap_wallet::id(), + ErrorCode::DefaultError, + "Wrong signer for lp taker swap" + )?; + + let ixs = ctx.accounts.instructions.as_ref(); + let current_index = instructions::load_current_index_checked(ixs)? as usize; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let current_ix = instructions::load_instruction_at_checked(current_index, ixs)?; + validate!( + current_ix.program_id == *ctx.program_id, + ErrorCode::InvalidSwap, + "SwapBegin must be a top-level instruction (cant be cpi)" + )?; + + validate!( + in_market_index != out_market_index, + ErrorCode::InvalidSwap, + "in and out market the same" + )?; + + validate!( + amount_in != 0, + ErrorCode::InvalidSwap, + "amount_in cannot be zero" + )?; + + // Make sure we have enough balance to do the swap + let constituent_in_token_account = &ctx.accounts.constituent_in_token_account; + validate!( + amount_in <= constituent_in_token_account.amount, + ErrorCode::InvalidSwap, + "trying to swap more than the balance of the constituent in token account" + )?; + + validate!( + out_constituent.flash_loan_initial_token_amount == 0, + ErrorCode::InvalidSwap, + "begin_lp_swap ended in invalid state" + )?; + + in_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_in_token_account.amount; + out_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_out_token_account.amount; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + constituent_in_token_account, + &ctx.accounts.signer_in_token_account, + &constituent_in_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &in_constituent.lp_pool, + &in_constituent.spot_market_index, + &in_constituent.vault_bump, + ), + amount_in, + &mint, + Some(remaining_accounts_iter), + )?; + + drop(in_constituent); + + // The only other drift program allowed is SwapEnd + let mut index = current_index + 1; + let mut found_end = false; + loop { + let ix = match instructions::load_instruction_at_checked(index, ixs) { + Ok(ix) => ix, + Err(ProgramError::InvalidArgument) => break, + Err(e) => return Err(e.into()), + }; + + // Check that the drift program key is not used + if ix.program_id == crate::id() { + // must be the last ix -- this could possibly be relaxed + validate!( + !found_end, + ErrorCode::InvalidSwap, + "the transaction must not contain a Drift instruction after FlashLoanEnd" + )?; + found_end = true; + + // must be the SwapEnd instruction + let discriminator = crate::instruction::EndLpSwap::discriminator(); + validate!( + ix.data[0..8] == discriminator, + ErrorCode::InvalidSwap, + "last drift ix must be end of swap" + )?; + + validate!( + ctx.accounts.signer_out_token_account.key() == ix.accounts[2].pubkey, + ErrorCode::InvalidSwap, + "the out_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.signer_in_token_account.key() == ix.accounts[3].pubkey, + ErrorCode::InvalidSwap, + "the in_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.constituent_out_token_account.key() == ix.accounts[4].pubkey, + ErrorCode::InvalidSwap, + "the constituent out_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.constituent_in_token_account.key() == ix.accounts[5].pubkey, + ErrorCode::InvalidSwap, + "the constituent in token account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.out_constituent.key() == ix.accounts[6].pubkey, + ErrorCode::InvalidSwap, + "the out constituent passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.in_constituent.key() == ix.accounts[7].pubkey, + ErrorCode::InvalidSwap, + "the in constituent passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.lp_pool.key() == ix.accounts[8].pubkey, + ErrorCode::InvalidSwap, + "the lp pool passed to SwapBegin and End must match" + )?; + } else { + if found_end { + if ix.program_id == lighthouse::ID { + continue; + } + + for meta in ix.accounts.iter() { + validate!( + meta.is_writable == false, + ErrorCode::InvalidSwap, + "instructions after swap end must not have writable accounts" + )?; + } + } else { + let mut whitelisted_programs = WHITELISTED_SWAP_PROGRAMS.to_vec(); + whitelisted_programs.push(AssociatedToken::id()); + whitelisted_programs.push(Token::id()); + whitelisted_programs.push(Token2022::id()); + whitelisted_programs.push(marinade_mainnet::ID); + + validate!( + whitelisted_programs.contains(&ix.program_id), + ErrorCode::InvalidSwap, + "only allowed to pass in ixs to token, openbook, and Jupiter v3/v4/v6 programs" + )?; + + for meta in ix.accounts.iter() { + validate!( + meta.pubkey != crate::id(), + ErrorCode::InvalidSwap, + "instructions between begin and end must not be drift instructions" + )?; + } + } + } + + index += 1; + } + + validate!( + found_end, + ErrorCode::InvalidSwap, + "found no SwapEnd instruction in transaction" + )?; + + Ok(()) +} + +pub fn handle_end_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, +) -> Result<()> { + let signer_in_token_account = &ctx.accounts.signer_in_token_account; + let signer_out_token_account = &ctx.accounts.signer_out_token_account; + + let admin_account_info = ctx.accounts.admin.to_account_info(); + + let constituent_in_token_account = &mut ctx.accounts.constituent_in_token_account; + let constituent_out_token_account = &mut ctx.accounts.constituent_out_token_account; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let out_token_program = get_token_interface(remaining_accounts)?; + + let in_mint = get_token_mint(remaining_accounts)?; + let out_mint = get_token_mint(remaining_accounts)?; + + // Residual of what wasnt swapped + if signer_in_token_account.amount > in_constituent.flash_loan_initial_token_amount { + let residual = signer_in_token_account + .amount + .safe_sub(in_constituent.flash_loan_initial_token_amount)?; + + controller::token::receive( + &ctx.accounts.token_program, + signer_in_token_account, + constituent_in_token_account, + &admin_account_info, + residual, + &in_mint, + Some(remaining_accounts), + )?; + } + + // Whatever was swapped + if signer_out_token_account.amount > out_constituent.flash_loan_initial_token_amount { + let residual = signer_out_token_account + .amount + .safe_sub(out_constituent.flash_loan_initial_token_amount)?; + + if let Some(token_interface) = out_token_program { + receive( + &token_interface, + signer_out_token_account, + constituent_out_token_account, + &admin_account_info, + residual, + &out_mint, + Some(remaining_accounts), + )?; + } else { + receive( + &ctx.accounts.token_program, + signer_out_token_account, + constituent_out_token_account, + &admin_account_info, + residual, + &out_mint, + Some(remaining_accounts), + )?; + } + } + + // Update the balance on the token accounts for after swap + constituent_out_token_account.reload()?; + constituent_in_token_account.reload()?; + out_constituent.sync_token_balance(constituent_out_token_account.amount); + in_constituent.sync_token_balance(constituent_in_token_account.amount); + + out_constituent.flash_loan_initial_token_amount = 0; + in_constituent.flash_loan_initial_token_amount = 0; + + Ok(()) +} + +pub fn handle_update_perp_market_lp_pool_status( + ctx: Context, + lp_status: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let amm_cache = &mut ctx.accounts.amm_cache; + + msg!("perp market {}", perp_market.market_index); + perp_market.lp_status = lp_status; + amm_cache.update_perp_market_fields(&perp_market)?; + + Ok(()) +} + +pub fn handle_update_initial_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, +) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + + let AccountMaps { + perp_market_map, + spot_market_map: _, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + Clock::get()?.slot, + None, + )?; + + for (_, perp_market_loader) in perp_market_map.0 { + let perp_market = perp_market_loader.load()?; + let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; + let mm_oracle_data = perp_market.get_mm_oracle_price_data( + *oracle_data, + slot, + &ctx.accounts.state.oracle_guard_rails.validity, + )?; + + amm_cache.update_perp_market_fields(&perp_market)?; + amm_cache.update_oracle_info( + slot, + perp_market.market_index, + &mm_oracle_data, + &perp_market, + &state.oracle_guard_rails, + )?; + } + + Ok(()) +} + +pub fn handle_reset_amm_cache(ctx: Context) -> Result<()> { + let state = &ctx.accounts.state; + let amm_cache = &mut ctx.accounts.amm_cache; + + amm_cache.cache.clear(); + amm_cache + .cache + .resize_with(state.number_of_markets as usize, CacheInfo::default); + amm_cache.validate(state)?; + + msg!("AMM cache reset. markets: {}", state.number_of_markets); + Ok(()) +} + +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] +pub struct OverrideAmmCacheParams { + pub quote_owed_from_lp_pool: Option, + pub last_settle_slot: Option, + pub last_fee_pool_token_amount: Option, + pub last_net_pnl_pool_token_amount: Option, + pub amm_position_scalar: Option, + pub amm_inventory_limit: Option, +} + +pub fn handle_override_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + market_index: u16, + override_params: OverrideAmmCacheParams, +) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + + let cache_entry = amm_cache.cache.get_mut(market_index as usize); + if cache_entry.is_none() { + msg!("No cache entry found for market index {}", market_index); + return Ok(()); + } + + let cache_entry = cache_entry.unwrap(); + if let Some(quote_owed_from_lp_pool) = override_params.quote_owed_from_lp_pool { + cache_entry.quote_owed_from_lp_pool = quote_owed_from_lp_pool; + } + if let Some(last_settle_slot) = override_params.last_settle_slot { + cache_entry.last_settle_slot = last_settle_slot; + } + if let Some(last_fee_pool_token_amount) = override_params.last_fee_pool_token_amount { + cache_entry.last_fee_pool_token_amount = last_fee_pool_token_amount; + } + if let Some(last_net_pnl_pool_token_amount) = override_params.last_net_pnl_pool_token_amount { + cache_entry.last_net_pnl_pool_token_amount = last_net_pnl_pool_token_amount; + } + + if let Some(amm_position_scalar) = override_params.amm_position_scalar { + cache_entry.amm_position_scalar = amm_position_scalar; + } + + if let Some(amm_position_scalar) = override_params.amm_position_scalar { + cache_entry.amm_position_scalar = amm_position_scalar; + } + + if let Some(amm_inventory_limit) = override_params.amm_inventory_limit { + if amm_inventory_limit < 0 { + msg!("amm_inventory_limit must be non-negative"); + return Err(ErrorCode::DefaultError.into()); + } + cache_entry.amm_inventory_limit = amm_inventory_limit; + } + + Ok(()) +} + +#[derive(Accounts)] +#[instruction( + id: u8, +)] +pub struct InitializeLpPool<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + #[account( + init, + seeds = [b"lp_pool", id.to_le_bytes().as_ref()], + space = LPPool::SIZE, + bump, + payer = admin + )] + pub lp_pool: AccountLoader<'info, LPPool>, + pub mint: Account<'info, anchor_spl::token::Mint>, + + #[account( + init, + seeds = [b"LP_POOL_TOKEN_VAULT".as_ref(), lp_pool.key().as_ref()], + bump, + payer = admin, + token::mint = mint, + token::authority = lp_pool + )] + pub lp_pool_token_vault: Box>, + + #[account( + init, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = AmmConstituentMapping::space(0 as usize), + payer = admin, + )] + pub amm_constituent_mapping: Box>, + + #[account( + init, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = ConstituentTargetBase::space(0 as usize), + payer = admin, + )] + pub constituent_target_base: Box>, + + #[account( + init, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = ConstituentCorrelations::space(0 as usize), + payer = admin, + )] + pub constituent_correlations: Box>, + + pub state: Box>, + pub token_program: Program<'info, Token>, + + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction( + spot_market_index: u16, +)] +pub struct InitializeConstituent<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_target_base.bump, + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1_usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_target_base: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_correlations.bump, + realloc = ConstituentCorrelations::space(constituent_target_base.targets.len() + 1_usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_correlations: Box>, + + #[account( + init, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + space = Constituent::SIZE, + payer = admin, + )] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + seeds = [b"spot_market", spot_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + address = spot_market.load()?.mint + )] + pub spot_market_mint: Box>, + #[account( + init, + seeds = [CONSTITUENT_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + payer = admin, + token::mint = spot_market_mint, + token::authority = constituent_vault + )] + pub constituent_vault: Box>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentParams<'info> { + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_target_base.bump, + constraint = constituent.load()?.lp_pool == lp_pool.key() + )] + pub constituent_target_base: Box>, + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentStatus<'info> { + #[account( + mut, + constraint = admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentPausedOperations<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateLpPoolParams<'info> { + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct AddAmmConstituentMappingDatum { + pub constituent_index: u16, + pub perp_market_index: u16, + pub weight: i64, +} + +#[derive(Accounts)] +#[instruction( + amm_constituent_mapping_data: Vec, +)] +pub struct AddAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = AmmConstituentMapping::space(amm_constituent_mapping.weights.len() + amm_constituent_mapping_data.len()), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_constituent_mapping: Box>, + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1_usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_target_base: Box>, + pub state: Box>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction( + amm_constituent_mapping_data: Vec, +)] +pub struct UpdateAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub amm_constituent_mapping: Box>, + pub system_program: Program<'info, System>, + pub state: Box>, +} + +#[derive(Accounts)] +pub struct RemoveAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = AmmConstituentMapping::space(amm_constituent_mapping.weights.len() - 1), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_constituent_mapping: Box>, + pub system_program: Program<'info, System>, + pub state: Box>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentCorrelation<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_correlations.bump, + )] + pub constituent_correlations: Box>, + pub state: Box>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct LPTakerSwap<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == lp_pool_swap_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + /// Signer token accounts + #[account( + mut, + constraint = &constituent_out_token_account.mint.eq(&signer_out_token_account.mint), + token::authority = admin + )] + pub signer_out_token_account: Box>, + #[account( + mut, + constraint = &constituent_in_token_account.mint.eq(&signer_in_token_account.mint), + token::authority = admin + )] + pub signer_in_token_account: Box>, + + /// Constituent token accounts + #[account( + mut, + address = out_constituent.load()?.vault, + )] + pub constituent_out_token_account: Box>, + #[account( + mut, + address = in_constituent.load()?.vault, + )] + pub constituent_in_token_account: Box>, + + /// Constituents + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump = out_constituent.load()?.bump, + )] + pub out_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump = in_constituent.load()?.bump, + )] + pub in_constituent: AccountLoader<'info, Constituent>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// Instructions Sysvar for instruction introspection + /// CHECK: fixed instructions sysvar account + #[account(address = instructions::ID)] + pub instructions: UncheckedAccount<'info>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +pub struct UpdatePerpMarketLpPoolStatus<'info> { + pub admin: Signer<'info>, + #[account( + has_one = admin + )] + pub state: Box>, + #[account(mut)] + pub perp_market: AccountLoader<'info, PerpMarket>, + #[account(mut, seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump,)] + pub amm_cache: Box>, +} + +#[derive(Accounts)] +pub struct UpdateInitialAmmCacheInfo<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub state: Box>, + pub admin: Signer<'info>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, +} + +#[derive(Accounts)] +pub struct ResetAmmCache<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + realloc = AmmCache::space(state.number_of_markets as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_cache: Box>, + pub system_program: Program<'info, System>, +} diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs new file mode 100644 index 0000000000..cfb4856751 --- /dev/null +++ b/programs/drift/src/instructions/lp_pool.rs @@ -0,0 +1,2130 @@ +use anchor_lang::{prelude::*, Accounts, Key, Result}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + +use crate::ids::lp_pool_swap_wallet; +use crate::math::constants::PRICE_PRECISION_I64; +use crate::state::events::{DepositDirection, LPBorrowLendDepositRecord}; +use crate::state::paused_operations::ConstituentLpOperation; +use crate::validation::whitelist::validate_whitelist_token; +use crate::{ + controller::{ + self, + spot_balance::update_spot_balances, + token::{burn_tokens, mint_tokens}, + }, + error::ErrorCode, + get_then_update_id, + ids::admin_hot_wallet, + math::{ + self, + casting::Cast, + constants::PERCENTAGE_PRECISION_I64, + oracle::{is_oracle_valid_for_action, DriftAction}, + safe_math::SafeMath, + }, + math_error, msg, safe_decrement, safe_increment, + state::{ + amm_cache::{AmmCacheFixed, CacheInfo, AMM_POSITIONS_CACHE}, + constituent_map::{ConstituentMap, ConstituentSet}, + events::{emit_stack, LPMintRedeemRecord, LPSwapRecord}, + lp_pool::{ + update_constituent_target_base_for_derivatives, AmmConstituentDatum, + AmmConstituentMappingFixed, Constituent, ConstituentCorrelationsFixed, + ConstituentTargetBaseFixed, LPPool, TargetsDatum, LP_POOL_SWAP_AUM_UPDATE_DELAY, + }, + oracle_map::OracleMap, + perp_market_map::MarketSet, + spot_market::{SpotBalanceType, SpotMarket}, + spot_market_map::get_writable_spot_market_set_from_many, + state::State, + traits::Size, + user::MarketType, + zero_copy::{AccountZeroCopy, AccountZeroCopyMut, ZeroCopyLoader}, + }, + validate, +}; +use std::iter::Peekable; +use std::slice::Iter; + +use solana_program::sysvar::clock::Clock; + +use super::optional_accounts::{get_whitelist_token, load_maps, AccountMaps}; +use crate::controller::spot_balance::update_spot_market_cumulative_interest; +use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; +use crate::instructions::constraints::*; +use crate::state::lp_pool::{ + AmmInventoryAndPricesAndSlots, ConstituentIndexAndDecimalAndPrice, CONSTITUENT_PDA_SEED, + LP_POOL_TOKEN_VAULT_PDA_SEED, +}; + +pub fn handle_update_constituent_target_base<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, +) -> Result<()> { + let slot = Clock::get()?.slot; + + let lp_pool_key: &Pubkey = &ctx.accounts.lp_pool.key(); + let lp_pool = ctx.accounts.lp_pool.load()?; + let constituent_target_base_key: &Pubkey = &ctx.accounts.constituent_target_base.key(); + + let amm_cache: AccountZeroCopy<'_, CacheInfo, AmmCacheFixed> = + ctx.accounts.amm_cache.load_zc()?; + + let mut constituent_target_base: AccountZeroCopyMut< + '_, + TargetsDatum, + ConstituentTargetBaseFixed, + > = ctx.accounts.constituent_target_base.load_zc_mut()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let amm_constituent_mapping: AccountZeroCopy< + '_, + AmmConstituentDatum, + AmmConstituentMappingFixed, + > = ctx.accounts.amm_constituent_mapping.load_zc()?; + validate!( + amm_constituent_mapping.fixed.lp_pool.eq(lp_pool_key), + ErrorCode::InvalidPDA, + "Amm constituent mapping lp pool pubkey does not match lp pool pubkey", + )?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let constituent_map = + ConstituentMap::load(&ConstituentSet::new(), &lp_pool_key, remaining_accounts)?; + + let mut amm_inventories: Vec = + Vec::with_capacity(amm_cache.len() as usize); + for (_, cache_info) in amm_cache.iter().enumerate() { + if cache_info.lp_status_for_perp_market == 0 { + continue; + } + + amm_inventories.push(AmmInventoryAndPricesAndSlots { + inventory: { + let scaled_position = cache_info + .position + .safe_mul(cache_info.amm_position_scalar as i64)? + .safe_div(100)?; + + scaled_position.clamp( + -cache_info.amm_inventory_limit, + cache_info.amm_inventory_limit, + ) + }, + price: cache_info.oracle_price, + last_oracle_slot: cache_info.oracle_slot, + last_position_slot: cache_info.slot, + }); + } + msg!("amm inventories: {:?}", amm_inventories); + + if amm_inventories.is_empty() { + msg!("No valid inventories found for constituent target weights update"); + return Ok(()); + } + + let mut constituent_indexes_and_decimals_and_prices: Vec = + Vec::with_capacity(constituent_map.0.len()); + for (index, loader) in &constituent_map.0 { + let constituent_ref = loader.load()?; + constituent_indexes_and_decimals_and_prices.push(ConstituentIndexAndDecimalAndPrice { + constituent_index: *index, + decimals: constituent_ref.decimals, + price: constituent_ref.last_oracle_price, + }); + } + + constituent_target_base.update_target_base( + &amm_constituent_mapping, + amm_inventories.as_slice(), + constituent_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + )?; + + Ok(()) +} + +pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateLPPoolAum<'info>>, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + let state = &ctx.accounts.state; + + let slot = Clock::get()?.slot; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let constituent_map = + ConstituentMap::load(&ConstituentSet::new(), &lp_pool.pubkey, remaining_accounts)?; + + validate!( + constituent_map.0.len() == lp_pool.constituents as usize, + ErrorCode::WrongNumberOfConstituents, + "Constituent map length does not match lp pool constituent count" + )?; + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let mut constituent_target_base: AccountZeroCopyMut< + '_, + TargetsDatum, + ConstituentTargetBaseFixed, + > = ctx.accounts.constituent_target_base.load_zc_mut()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool.pubkey) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let amm_cache: AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed> = + ctx.accounts.amm_cache.load_zc_mut()?; + + let (aum, crypto_delta, derivative_groups) = lp_pool.update_aum( + slot, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &constituent_target_base, + &amm_cache, + )?; + + // Set USDC stable weight + msg!("aum: {}", aum); + let total_stable_target_base = aum.cast::()?.safe_sub(crypto_delta)?; + constituent_target_base + .get_mut(lp_pool.quote_consituent_index as u32) + .target_base = total_stable_target_base.cast::()?; + + msg!( + "stable target base: {}", + constituent_target_base + .get(lp_pool.quote_consituent_index as u32) + .target_base + ); + msg!("aum: {}, crypto_delta: {}", aum, crypto_delta); + msg!("derivative groups: {:?}", derivative_groups); + + update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + )?; + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + min_out_amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + validate!( + state.allow_swap_lp_pool(), + ErrorCode::DefaultError, + "Swapping with LP Pool is disabled" + )?; + + validate!( + in_market_index != out_market_index, + ErrorCode::InvalidSpotMarketAccount, + "In and out spot market indices cannot be the same" + )?; + + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let lp_pool_key = ctx.accounts.lp_pool.key(); + let lp_pool = &ctx.accounts.lp_pool.load()?; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + in_constituent.does_constituent_allow_operation(ConstituentLpOperation::Swap)?; + out_constituent.does_constituent_allow_operation(ConstituentLpOperation::Swap)?; + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let constituent_correlations_key = &ctx.accounts.constituent_correlations.key(); + let constituent_correlations: AccountZeroCopy<'_, i64, ConstituentCorrelationsFixed> = + ctx.accounts.constituent_correlations.load_zc()?; + validate!( + constituent_correlations.fixed.lp_pool.eq(&lp_pool_key) + && constituent_correlations_key.eq(&lp_pool.constituent_correlations), + ErrorCode::InvalidPDA, + "Constituent correlations lp pool pubkey does not match lp pool pubkey", + )?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + let out_spot_market = spot_market_map.get_ref(&out_market_index)?; + + if in_constituent.is_reduce_only()? + && !in_constituent.is_operation_reducing(&in_spot_market, true)? + { + msg!("In constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + if out_constituent.is_reduce_only()? + && !out_constituent.is_operation_reducing(&out_spot_market, false)? + { + msg!("Out constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + let in_oracle_id = in_spot_market.oracle_id(); + let out_oracle_id = out_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let in_oracle = in_oracle.clone(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + let in_target_weight = constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, + )?; + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, + )?; + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount( + in_target_position_slot_delay, + out_target_position_slot_delay, + in_target_oracle_slot_delay, + out_target_oracle_slot_delay, + &in_oracle, + &out_oracle, + &in_constituent, + &out_constituent, + &in_spot_market, + &out_spot_market, + in_target_weight, + out_target_weight, + in_amount as u128, + constituent_correlations.get_correlation( + in_constituent.constituent_index, + out_constituent.constituent_index, + )?, + )?; + msg!( + "in_amount: {}, out_amount: {}, in_fee: {}, out_fee: {}", + in_amount, + out_amount, + in_fee, + out_fee + ); + let out_amount_net_fees = if out_fee > 0 { + out_amount.safe_sub(out_fee.unsigned_abs())? + } else { + out_amount.safe_add(out_fee.unsigned_abs())? + }; + + validate!( + out_amount_net_fees.cast::()? >= min_out_amount, + ErrorCode::SlippageOutsideLimit, + format!( + "Slippage outside limit: out_amount_net_fees({}) < min_out_amount({})", + out_amount_net_fees, min_out_amount + ) + .as_str() + )?; + + validate!( + out_amount_net_fees.cast::()? <= out_constituent.vault_token_balance, + ErrorCode::InsufficientConstituentTokenBalance, + format!( + "Insufficient out constituent balance: out_amount_net_fees({}) > out_constituent.token_balance({})", + out_amount_net_fees, out_constituent.vault_token_balance + ) + .as_str() + )?; + + in_constituent.record_swap_fees(in_fee)?; + out_constituent.record_swap_fees(out_fee)?; + + let in_swap_id = get_then_update_id!(in_constituent, next_swap_id); + let out_swap_id = get_then_update_id!(out_constituent, next_swap_id); + + emit_stack::<_, { LPSwapRecord::SIZE }>(LPSwapRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + out_amount: out_amount_net_fees, + in_amount, + out_fee, + in_fee, + out_spot_market_index: out_market_index, + in_spot_market_index: in_market_index, + out_constituent_index: out_constituent.constituent_index, + in_constituent_index: in_constituent.constituent_index, + out_oracle_price: out_oracle.price, + in_oracle_price: in_oracle.price, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: in_constituent.get_weight( + in_oracle.price, + &in_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: in_target_weight, + out_market_current_weight: out_constituent.get_weight( + out_oracle.price, + &out_spot_market, + 0, + lp_pool.last_aum, + )?, + out_market_target_weight: out_target_weight, + in_swap_id, + out_swap_id, + lp_pool: lp_pool_key, + })?; + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_in_token_account, + &ctx.accounts.constituent_in_token_account, + &ctx.accounts.authority, + in_amount.cast::()?, + &Some((*ctx.accounts.in_market_mint).clone()), + Some(remaining_accounts), + )?; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.constituent_out_token_account, + &ctx.accounts.user_out_token_account, + &ctx.accounts.constituent_out_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &out_constituent.lp_pool, + &out_constituent.spot_market_index, + &out_constituent.vault_bump, + ), + out_amount_net_fees.cast::()?, + &Some((*ctx.accounts.out_market_mint).clone()), + Some(remaining_accounts), + )?; + + ctx.accounts.constituent_in_token_account.reload()?; + ctx.accounts.constituent_out_token_account.reload()?; + + in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount); + out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount); + + Ok(()) +} + +pub fn handle_view_lp_pool_swap_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolSwapFees<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + in_target_weight: i64, + out_target_weight: i64, +) -> Result<()> { + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + + let lp_pool = &ctx.accounts.lp_pool.load()?; + let in_constituent = ctx.accounts.in_constituent.load()?; + let out_constituent = ctx.accounts.out_constituent.load()?; + let constituent_correlations: AccountZeroCopy<'_, i64, ConstituentCorrelationsFixed> = + ctx.accounts.constituent_correlations.load_zc()?; + + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + let out_spot_market = spot_market_map.get_ref(&out_market_index)?; + + let in_oracle_id = in_spot_market.oracle_id(); + let out_oracle_id = out_spot_market.oracle_id(); + + let (in_oracle, _) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let in_oracle = in_oracle.clone(); + + let (out_oracle, _) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount( + in_target_position_slot_delay, + out_target_position_slot_delay, + in_target_oracle_slot_delay, + out_target_oracle_slot_delay, + &in_oracle, + &out_oracle, + &in_constituent, + &out_constituent, + &in_spot_market, + &out_spot_market, + in_target_weight, + out_target_weight, + in_amount as u128, + constituent_correlations.get_correlation( + in_constituent.constituent_index, + out_constituent.constituent_index, + )?, + )?; + msg!( + "in_amount: {}, out_amount: {}, in_fee: {}, out_fee: {}", + in_amount, + out_amount, + in_fee, + out_fee + ); + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolAddLiquidity<'info>>, + in_market_index: u16, + in_amount: u128, + min_mint_amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + + validate!( + state.allow_mint_redeem_lp_pool(), + ErrorCode::MintRedeemLpPoolDisabled, + "Mint/redeem LP pool is disabled" + )?; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + in_constituent.does_constituent_allow_operation(ConstituentLpOperation::Deposit)?; + + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let lp_pool_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + let lp_price_before = lp_pool.get_price(lp_pool.token_supply)?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![in_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let whitelist_mint = &lp_pool.whitelist_mint; + if !whitelist_mint.eq(&Pubkey::default()) { + validate_whitelist_token( + get_whitelist_token(remaining_accounts)?, + whitelist_mint, + &ctx.accounts.authority.key(), + )?; + } + + let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?; + + if in_constituent.is_reduce_only()? + && !in_constituent.is_operation_reducing(&in_spot_market, true)? + { + msg!("In constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + let in_oracle_id = in_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let in_oracle = in_oracle.clone(); + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + // TODO: check self.aum validity + + update_spot_market_cumulative_interest(&mut in_spot_market, Some(&in_oracle), now)?; + + msg!("aum: {}", lp_pool.last_aum); + let in_target_weight = if lp_pool.last_aum == 0 { + PERCENTAGE_PRECISION_I64 // 100% weight if no aum + } else { + constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, // TODO: add in_amount * in_oracle to est post add_liquidity aum + )? + }; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + + let (lp_amount, in_amount, lp_fee_amount, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + &in_spot_market, + &in_constituent, + in_amount, + &in_oracle, + in_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_amount: {}, in_amount: {}, lp_fee_amount: {}, in_fee_amount: {}", + lp_amount, + in_amount, + lp_fee_amount, + in_fee_amount + ); + + let lp_mint_amount_net_fees = if lp_fee_amount > 0 { + lp_amount.safe_sub(lp_fee_amount.unsigned_abs() as u64)? + } else { + lp_amount.safe_add(lp_fee_amount.unsigned_abs() as u64)? + }; + + validate!( + lp_mint_amount_net_fees >= min_mint_amount, + ErrorCode::SlippageOutsideLimit, + format!( + "Slippage outside limit: lp_mint_amount_net_fees({}) < min_mint_amount({})", + lp_mint_amount_net_fees, min_mint_amount + ) + .as_str() + )?; + + in_constituent.record_swap_fees(in_fee_amount)?; + lp_pool.record_mint_redeem_fees(lp_fee_amount)?; + + let lp_pool_id = lp_pool.lp_pool_id; + let lp_bump = lp_pool.bump; + + let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_pool_id, &lp_bump); + + drop(lp_pool); + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_in_token_account, + &ctx.accounts.constituent_in_token_account, + &ctx.accounts.authority, + in_amount.cast::()?, + &Some((*ctx.accounts.in_market_mint).clone()), + Some(remaining_accounts), + )?; + + mint_tokens( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.lp_pool.to_account_info(), + &lp_vault_signer_seeds, + lp_amount, + &ctx.accounts.lp_mint, + )?; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.user_lp_token_account, + &ctx.accounts.lp_pool.to_account_info(), + &lp_vault_signer_seeds, + lp_mint_amount_net_fees, + &Some((*ctx.accounts.lp_mint).clone()), + Some(remaining_accounts), + )?; + + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + lp_pool.last_aum = lp_pool.last_aum.safe_add( + in_amount + .cast::()? + .safe_mul(in_oracle.price.cast::()?)? + .safe_div(10_u128.pow(in_spot_market.decimals))?, + )?; + + if lp_pool.last_aum > lp_pool.max_aum { + return Err(ErrorCode::MaxDlpAumBreached.into()); + } + + ctx.accounts.constituent_in_token_account.reload()?; + ctx.accounts.lp_mint.reload()?; + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + + in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount); + + ctx.accounts.lp_mint.reload()?; + let lp_price_after = lp_pool.get_price(lp_pool.token_supply)?; + let price_diff = (lp_price_after.cast::()?).safe_sub(lp_price_before.cast::()?)?; + + if lp_price_before > 0 && price_diff.signum() != 0 && in_fee_amount.signum() != 0 { + validate!( + price_diff.signum() == in_fee_amount.signum() || price_diff == 0, + ErrorCode::LpInvariantFailed, + "Adding liquidity resulted in price direction != fee sign, price_diff: {}, in_fee_amount: {}", + price_diff, + in_fee_amount + )?; + } + + let mint_redeem_id = get_then_update_id!(lp_pool, mint_redeem_id); + emit_stack::<_, { LPMintRedeemRecord::SIZE }>(LPMintRedeemRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + description: 1, + amount: in_amount, + fee: in_fee_amount, + spot_market_index: in_market_index, + constituent_index: in_constituent.constituent_index, + oracle_price: in_oracle.price, + mint: in_constituent.mint, + lp_amount, + lp_fee: lp_fee_amount, + lp_price: lp_price_after, + mint_redeem_id, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: in_constituent.get_weight( + in_oracle.price, + &in_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: in_target_weight, + lp_pool: lp_pool_key, + })?; + + Ok(()) +} + +pub fn handle_view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolAddLiquidityFees<'info>>, + in_market_index: u16, + in_amount: u128, +) -> Result<()> { + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + let lp_pool = ctx.accounts.lp_pool.load()?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let in_constituent = ctx.accounts.in_constituent.load()?; + + let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + + let in_oracle_id = in_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let in_oracle = in_oracle.clone(); + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + msg!("aum: {}", lp_pool.last_aum); + let in_target_weight = if lp_pool.last_aum == 0 { + PERCENTAGE_PRECISION_I64 // 100% weight if no aum + } else { + constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, // TODO: add in_amount * in_oracle to est post add_liquidity aum + )? + }; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + + let (lp_amount, in_amount, lp_fee_amount, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + &in_spot_market, + &in_constituent, + in_amount, + &in_oracle, + in_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_amount: {}, in_amount: {}, lp_fee_amount: {}, in_fee_amount: {}", + lp_amount, + in_amount, + lp_fee_amount, + in_fee_amount + ); + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolRemoveLiquidity<'info>>, + out_market_index: u16, + lp_to_burn: u64, + min_amount_out: u128, +) -> Result<()> { + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let state = &ctx.accounts.state; + + validate!( + state.allow_mint_redeem_lp_pool(), + ErrorCode::MintRedeemLpPoolDisabled, + "Mint/redeem LP pool is disabled" + )?; + + let lp_pool_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + + let lp_price_before = lp_pool.get_price(lp_pool.token_supply)?; + + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + out_constituent.does_constituent_allow_operation(ConstituentLpOperation::Withdraw)?; + + // Verify previous settle + let amm_cache: AccountZeroCopy<'_, CacheInfo, _> = ctx.accounts.amm_cache.load_zc()?; + for (i, _) in amm_cache.iter().enumerate() { + let cache_info = amm_cache.get(i as u32); + if cache_info.last_fee_pool_token_amount != 0 && cache_info.last_settle_slot != slot { + msg!( + "Market {} has not been settled in current slot. Last slot: {}", + i, + cache_info.last_settle_slot + ); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![out_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let mut out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?; + + if out_constituent.is_reduce_only()? + && !out_constituent.is_operation_reducing(&out_spot_market, false)? + { + msg!("Out constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + let out_oracle_id = out_spot_market.oracle_id(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let out_oracle = *out_oracle; + + // TODO: check self.aum validity + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + update_spot_market_cumulative_interest(&mut out_spot_market, Some(&out_oracle), now)?; + + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, // TODO: remove out_amount * out_oracle to est post remove_liquidity aum + )?; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + + let (lp_burn_amount, out_amount, lp_fee_amount, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + &out_spot_market, + &out_constituent, + lp_to_burn, + &out_oracle, + out_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_burn_amount: {}, out_amount: {}, lp_fee_amount: {}, out_fee_amount: {}", + lp_burn_amount, + out_amount, + lp_fee_amount, + out_fee_amount + ); + + let lp_burn_amount_net_fees = if lp_fee_amount > 0 { + lp_burn_amount.safe_sub(lp_fee_amount.unsigned_abs() as u64)? + } else { + lp_burn_amount.safe_add(lp_fee_amount.unsigned_abs() as u64)? + }; + + let out_amount_net_fees = if out_fee_amount > 0 { + out_amount.safe_sub(out_fee_amount.unsigned_abs())? + } else { + out_amount.safe_add(out_fee_amount.unsigned_abs())? + }; + + validate!( + out_amount_net_fees >= min_amount_out, + ErrorCode::SlippageOutsideLimit, + "Slippage outside limit: out_amount_net_fees({}) < min_amount_out({})", + out_amount_net_fees, + min_amount_out + )?; + + if out_amount_net_fees > out_constituent.vault_token_balance.cast()? { + let transfer_amount = out_amount_net_fees + .cast::()? + .safe_sub(out_constituent.vault_token_balance)?; + msg!( + "transfering from program vault to constituent vault: {}", + transfer_amount + ); + transfer_from_program_vault( + transfer_amount, + &mut out_spot_market, + &mut out_constituent, + out_oracle.price, + &ctx.accounts.state, + &mut ctx.accounts.spot_market_token_account, + &mut ctx.accounts.constituent_out_token_account, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + &None, + Some(remaining_accounts), + )?; + } + + validate!( + out_amount_net_fees <= out_constituent.vault_token_balance.cast()?, + ErrorCode::InsufficientConstituentTokenBalance, + "Insufficient out constituent balance: out_amount_net_fees({}) > out_constituent.token_balance({})", + out_amount_net_fees, + out_constituent.vault_token_balance + )?; + + out_constituent.record_swap_fees(out_fee_amount)?; + lp_pool.record_mint_redeem_fees(lp_fee_amount)?; + + let lp_pool_id = lp_pool.lp_pool_id; + let lp_bump = lp_pool.bump; + + let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_pool_id, &lp_bump); + + drop(lp_pool); + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_lp_token_account, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.authority, + lp_burn_amount, + &None, + Some(remaining_accounts), + )?; + + burn_tokens( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.lp_pool.to_account_info(), + &lp_vault_signer_seeds, + lp_burn_amount_net_fees, + &ctx.accounts.lp_mint, + )?; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.constituent_out_token_account, + &ctx.accounts.user_out_token_account, + &ctx.accounts.constituent_out_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &out_constituent.lp_pool, + &out_constituent.spot_market_index, + &out_constituent.vault_bump, + ), + out_amount_net_fees.cast::()?, + &None, + Some(remaining_accounts), + )?; + + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + lp_pool.last_aum = lp_pool.last_aum.safe_sub( + out_amount_net_fees + .cast::()? + .safe_mul(out_oracle.price.cast::()?)? + .safe_div(10_u128.pow(out_spot_market.decimals))?, + )?; + + ctx.accounts.constituent_out_token_account.reload()?; + ctx.accounts.lp_mint.reload()?; + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + + out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount); + + ctx.accounts.lp_mint.reload()?; + let lp_price_after = lp_pool.get_price(lp_pool.token_supply)?; + + let price_diff = (lp_price_after.cast::()?).safe_sub(lp_price_before.cast::()?)?; + if price_diff.signum() != 0 && out_fee_amount.signum() != 0 { + validate!( + price_diff.signum() == out_fee_amount.signum(), + ErrorCode::LpInvariantFailed, + "Removing liquidity resulted in price direction != fee sign, price_diff: {}, out_fee_amount: {}", + price_diff, + out_fee_amount + )?; + } + + let mint_redeem_id = get_then_update_id!(lp_pool, mint_redeem_id); + emit_stack::<_, { LPMintRedeemRecord::SIZE }>(LPMintRedeemRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + description: 0, + amount: out_amount, + fee: out_fee_amount, + spot_market_index: out_market_index, + constituent_index: out_constituent.constituent_index, + oracle_price: out_oracle.price, + mint: out_constituent.mint, + lp_amount: lp_burn_amount, + lp_fee: lp_fee_amount, + lp_price: lp_price_after, + mint_redeem_id, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: out_constituent.get_weight( + out_oracle.price, + &out_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: out_target_weight, + lp_pool: lp_pool_key, + })?; + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolRemoveLiquidityFees<'info>>, + out_market_index: u16, + lp_to_burn: u64, +) -> Result<()> { + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + let lp_pool = ctx.accounts.lp_pool.load()?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![out_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?; + + let out_oracle_id = out_spot_market.oracle_id(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let out_oracle = out_oracle.clone(); + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, + )?; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + + let (lp_burn_amount, out_amount, lp_fee_amount, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + &out_spot_market, + &out_constituent, + lp_to_burn, + &out_oracle, + out_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_burn_amount: {}, out_amount: {}, lp_fee_amount: {}, out_fee_amount: {}", + lp_burn_amount, + out_amount, + lp_fee_amount, + out_fee_amount + ); + + Ok(()) +} + +pub fn handle_update_constituent_oracle_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentOracleInfo<'info>>, +) -> Result<()> { + let clock = Clock::get()?; + let mut constituent = ctx.accounts.constituent.load_mut()?; + let spot_market = ctx.accounts.spot_market.load()?; + + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; + if constituent.last_oracle_slot < oracle_data_slot { + constituent.last_oracle_price = oracle_data.price; + constituent.last_oracle_slot = oracle_data_slot; + } + + Ok(()) +} + +pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositProgramVault<'info>>, + amount: u64, +) -> Result<()> { + let clock = Clock::get()?; + + let mut spot_market = ctx.accounts.spot_market.load_mut()?; + let spot_market_vault = &ctx.accounts.spot_market_vault; + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let mut constituent = ctx.accounts.constituent.load_mut()?; + let lp_pool_key = constituent.lp_pool; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + let deposit_plus_token_amount_before = amount.safe_add(spot_market_vault.amount)?; + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_data), + clock.unix_timestamp, + )?; + let token_balance_after_cumulative_interest_update = constituent + .spot_balance + .get_signed_token_amount(&spot_market)?; + + let interest_accrued_token_amount = token_balance_after_cumulative_interest_update + .cast::()? + .safe_sub(constituent.last_spot_balance_token_amount)?; + + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + let balance_before = constituent.get_full_token_amount(&spot_market)?; + + controller::token::send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.constituent_token_account, + &spot_market_vault, + &ctx.accounts.constituent_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &constituent.lp_pool, + &constituent.spot_market_index, + &constituent.vault_bump, + ), + amount, + &Some(*ctx.accounts.mint.clone()), + Some(remaining_accounts), + )?; + + // Adjust BLPosition for the new deposits + let spot_position = &mut constituent.spot_balance; + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + spot_position, + false, + )?; + + safe_increment!(spot_position.cumulative_deposits, amount.cast()?); + + ctx.accounts.spot_market_vault.reload()?; + ctx.accounts.constituent_token_account.reload()?; + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + spot_market.validate_max_token_deposits_and_borrows(false)?; + + validate!( + ctx.accounts.spot_market_vault.amount == deposit_plus_token_amount_before, + ErrorCode::LpInvariantFailed, + "Spot market vault amount mismatch after deposit" + )?; + + let balance_after = constituent.get_full_token_amount(&spot_market)?; + let balance_diff_notional = if spot_market.decimals > 6 { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(oracle_data.price)? + .safe_div(PRICE_PRECISION_I64)? + .safe_div(10_i64.pow(spot_market.decimals - 6))? + } else { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(10_i64.pow(6 - spot_market.decimals))? + .safe_mul(oracle_data.price)? + .safe_div(PRICE_PRECISION_I64)? + }; + + msg!("Balance difference (notional): {}", balance_diff_notional); + + validate!( + balance_diff_notional <= PRICE_PRECISION_I64 / 100, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault" + )?; + + let new_token_balance = constituent + .spot_balance + .get_signed_token_amount(&spot_market)? + .cast::()?; + + emit!(LPBorrowLendDepositRecord { + ts: clock.unix_timestamp, + slot: clock.slot, + spot_market_index: spot_market.market_index, + constituent_index: constituent.constituent_index, + direction: DepositDirection::Deposit, + token_balance: new_token_balance, + last_token_balance: constituent.last_spot_balance_token_amount, + interest_accrued_token_amount, + amount_deposit_withdraw: amount, + lp_pool: lp_pool_key, + }); + constituent.last_spot_balance_token_amount = new_token_balance; + constituent.cumulative_spot_interest_accrued_token_amount = constituent + .cumulative_spot_interest_accrued_token_amount + .safe_add(interest_accrued_token_amount)?; + + Ok(()) +} + +pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, WithdrawProgramVault<'info>>, + amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + let clock = Clock::get()?; + + let mut spot_market = ctx.accounts.spot_market.load_mut()?; + + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let mut constituent = ctx.accounts.constituent.load_mut()?; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(oracle_data), + clock.unix_timestamp, + )?; + let token_balance_after_cumulative_interest_update = constituent + .spot_balance + .get_signed_token_amount(&spot_market)?; + + let interest_accrued_token_amount = token_balance_after_cumulative_interest_update + .cast::()? + .safe_sub(constituent.last_spot_balance_token_amount)?; + + let mint = &Some(*ctx.accounts.mint.clone()); + transfer_from_program_vault( + amount, + &mut spot_market, + &mut constituent, + oracle_data.price, + &state, + &mut ctx.accounts.spot_market_vault, + &mut ctx.accounts.constituent_token_account, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + mint, + Some(remaining_accounts), + )?; + + let new_token_balance = constituent + .spot_balance + .get_signed_token_amount(&spot_market)? + .cast::()?; + + emit!(LPBorrowLendDepositRecord { + ts: clock.unix_timestamp, + slot: clock.slot, + spot_market_index: spot_market.market_index, + constituent_index: constituent.constituent_index, + direction: DepositDirection::Withdraw, + token_balance: new_token_balance, + last_token_balance: constituent.last_spot_balance_token_amount, + interest_accrued_token_amount, + amount_deposit_withdraw: amount, + lp_pool: constituent.lp_pool, + }); + constituent.last_spot_balance_token_amount = new_token_balance; + constituent.cumulative_spot_interest_accrued_token_amount = constituent + .cumulative_spot_interest_accrued_token_amount + .safe_add(interest_accrued_token_amount)?; + + Ok(()) +} + +fn transfer_from_program_vault<'info>( + amount: u64, + spot_market: &mut SpotMarket, + constituent: &mut Constituent, + oracle_price: i64, + state: &State, + spot_market_vault: &mut InterfaceAccount<'info, TokenAccount>, + constituent_token_account: &mut InterfaceAccount<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + drift_signer: &AccountInfo<'info>, + mint: &Option>, + remaining_accounts: Option<&mut Peekable>>>, +) -> Result<()> { + constituent.sync_token_balance(constituent_token_account.amount); + + let balance_before = constituent.get_full_token_amount(&spot_market)?; + + // Adding some 5% flexibility to max threshold to prevent race conditions + let buffer = constituent + .max_borrow_token_amount + .safe_mul(5)? + .safe_div(100)?; + let max_transfer = constituent + .max_borrow_token_amount + .safe_add(buffer)? + .cast::()? + .safe_add( + constituent + .spot_balance + .get_signed_token_amount(spot_market)?, + )? + .max(0) + .cast::()?; + + validate!( + max_transfer >= amount, + ErrorCode::LpInvariantFailed, + "Max transfer ({}) is less than amount ({})", + max_transfer, + amount + )?; + + // Execute transfer and sync new balance in the constituent account + controller::token::send_from_program_vault( + token_program, + spot_market_vault, + constituent_token_account, + drift_signer, + state.signer_nonce, + amount, + mint, + remaining_accounts, + )?; + constituent_token_account.reload()?; + constituent.sync_token_balance(constituent_token_account.amount); + + // Adjust BLPosition for the new deposits + let spot_position = &mut constituent.spot_balance; + update_spot_balances( + amount as u128, + &SpotBalanceType::Borrow, + spot_market, + spot_position, + true, + )?; + + safe_decrement!(spot_position.cumulative_deposits, amount.cast()?); + + // Re-check spot market invariants + spot_market_vault.reload()?; + spot_market.validate_max_token_deposits_and_borrows(true)?; + math::spot_withdraw::validate_spot_market_vault_amount(&spot_market, spot_market_vault.amount)?; + + // Verify withdraw fully accounted for in BLPosition + let balance_after = constituent.get_full_token_amount(&spot_market)?; + + let balance_diff_notional = if spot_market.decimals > 6 { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(oracle_price)? + .safe_div(PRICE_PRECISION_I64)? + .safe_div(10_i64.pow(spot_market.decimals - 6))? + } else { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(10_i64.pow(6 - spot_market.decimals))? + .safe_mul(oracle_price)? + .safe_div(PRICE_PRECISION_I64)? + }; + + #[cfg(feature = "mainnet-beta")] + validate!( + balance_diff_notional <= PRICE_PRECISION_I64 / 100, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault, {}", + balance_diff_notional + )?; + #[cfg(not(feature = "mainnet-beta"))] + validate!( + balance_diff_notional <= PRICE_PRECISION_I64 / 10, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault, {}", + balance_diff_notional + )?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct DepositProgramVault<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin || admin.key() == lp_pool_swap_wallet::id() + )] + pub admin: Signer<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.vault, + constraint = &constituent.load()?.mint.eq(&constituent_token_account.mint), + )] + pub constituent_token_account: Box>, + #[account( + mut, + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + address = spot_market.load()?.vault, + )] + pub spot_market_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + #[account( + address = spot_market.load()?.mint, + )] + pub mint: Box>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct WithdrawProgramVault<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin || admin.key() == lp_pool_swap_wallet::id() + )] + pub admin: Signer<'info>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.vault, + constraint = &constituent.load()?.mint.eq(&constituent_token_account.mint), + )] + pub constituent_token_account: Box>, + #[account( + mut, + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + address = spot_market.load()?.vault, + token::authority = drift_signer, + )] + pub spot_market_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + #[account( + address = spot_market.load()?.mint, + )] + pub mint: Box>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentOracleInfo<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentTargetBase<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + /// CHECK: checked in AmmConstituentMappingZeroCopy checks + pub amm_constituent_mapping: AccountInfo<'info>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + #[account(mut)] + pub constituent_target_base: AccountInfo<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + pub amm_cache: AccountInfo<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, +} + +#[derive(Accounts)] +pub struct UpdateLPPoolAum<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + #[account(mut)] + pub constituent_target_base: AccountInfo<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, +} + +/// `in`/`out` is in the program's POV for this swap. So `user_in_token_account` is the user owned token account +/// for the `in` token for this swap. +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct LPPoolSwap<'info> { + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and in ix + pub constituent_target_base: AccountInfo<'info>, + + /// CHECK: checked in ConstituentCorrelationsZeroCopy checks and in ix + pub constituent_correlations: AccountInfo<'info>, + + #[account( + mut, + address = in_constituent.load()?.vault, + )] + pub constituent_in_token_account: Box>, + #[account( + mut, + address = out_constituent.load()?.vault, + )] + pub constituent_out_token_account: Box>, + + #[account( + mut, + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) && user_in_token_account.owner == authority.key() + )] + pub user_in_token_account: Box>, + #[account( + mut, + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) && user_out_token_account.owner == authority.key() + )] + pub user_out_token_account: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump=in_constituent.load()?.bump, + constraint = in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump=out_constituent.load()?.bump, + constraint = out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = in_market_mint.key() == in_constituent.load()?.mint, + )] + pub in_market_mint: Box>, + #[account( + constraint = out_market_mint.key() == out_constituent.load()?.mint, + )] + pub out_market_mint: Box>, + + pub authority: Signer<'info>, + + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct ViewLPPoolSwapFees<'info> { + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and in ix + pub constituent_target_base: AccountInfo<'info>, + + /// CHECK: checked in ConstituentCorrelationsZeroCopy checks and in ix + pub constituent_correlations: AccountInfo<'info>, + + #[account( + mut, + address = in_constituent.load()?.vault, + )] + pub constituent_in_token_account: Box>, + #[account( + mut, + address = out_constituent.load()?.vault, + )] + pub constituent_out_token_account: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump=in_constituent.load()?.bump, + constraint = in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump=out_constituent.load()?.bump, + constraint = out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + pub authority: Signer<'info>, + + // TODO: in/out token program + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct LPPoolAddLiquidity<'info> { + pub state: Box>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub in_market_mint: Box>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + constraint = + in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + + #[account( + mut, + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) && user_in_token_account.owner == authority.key() + )] + pub user_in_token_account: Box>, + + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] + pub constituent_in_token_account: Box>, + + #[account( + mut, + constraint = user_lp_token_account.mint.eq(&lp_mint.key()) + )] + pub user_lp_token_account: Box>, + + #[account( + mut, + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + pub constituent_target_base: AccountInfo<'info>, + + #[account( + mut, + seeds = [LP_POOL_TOKEN_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub lp_pool_token_vault: Box>, + + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct ViewLPPoolAddLiquidityFees<'info> { + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub in_market_mint: Box>, + #[account( + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] + pub in_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and address checked in code + pub constituent_target_base: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction( + out_market_index: u16, +)] +pub struct LPPoolRemoveLiquidity<'info> { + pub state: Box>, + #[account( + constraint = drift_signer.key() == state.signer + )] + /// CHECK: drift_signer + pub drift_signer: AccountInfo<'info>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub out_market_mint: Box>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + constraint = + out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + mut, + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) && user_out_token_account.owner == authority.key() + )] + pub user_out_token_account: Box>, + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + )] + pub constituent_out_token_account: Box>, + #[account( + mut, + constraint = user_lp_token_account.mint.eq(&lp_mint.key()) + )] + pub user_lp_token_account: Box>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_token_account: Box>, + + #[account( + mut, + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and address checked in code + pub constituent_target_base: AccountInfo<'info>, + + #[account( + mut, + seeds = [LP_POOL_TOKEN_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub lp_pool_token_vault: Box>, + + pub token_program: Interface<'info, TokenInterface>, + + #[account( + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump, + )] + /// CHECK: checked in AmmCacheZeroCopy checks + pub amm_cache: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct ViewLPPoolRemoveLiquidityFees<'info> { + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub out_market_mint: Box>, + #[account( + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and address checked in code + pub constituent_target_base: AccountInfo<'info>, +} diff --git a/programs/drift/src/instructions/mod.rs b/programs/drift/src/instructions/mod.rs index 0caa84f731..d2267d7d5a 100644 --- a/programs/drift/src/instructions/mod.rs +++ b/programs/drift/src/instructions/mod.rs @@ -2,6 +2,8 @@ pub use admin::*; pub use constraints::*; pub use if_staker::*; pub use keeper::*; +pub use lp_admin::*; +pub use lp_pool::*; pub use pyth_lazer_oracle::*; pub use pyth_pull_oracle::*; pub use user::*; @@ -10,6 +12,8 @@ mod admin; mod constraints; mod if_staker; mod keeper; +mod lp_admin; +mod lp_pool; pub mod optional_accounts; mod pyth_lazer_oracle; mod pyth_pull_oracle; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 2d7a56e6af..a7fb0ca854 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -21,18 +21,19 @@ use crate::controller::spot_position::{ update_spot_balances_and_cumulative_deposits_with_limits, }; use crate::error::ErrorCode; +use crate::get_then_update_id; use crate::ids::admin_hot_wallet; -use crate::ids::{ - dflow_mainnet_aggregator_4, jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, - lighthouse, marinade_mainnet, serum_program, titan_mainnet_argos_v1, -}; +use crate::ids::WHITELISTED_SWAP_PROGRAMS; +use crate::ids::{lighthouse, marinade_mainnet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{ get_referrer_and_referrer_stats, get_whitelist_token, load_maps, AccountMaps, }; use crate::instructions::SpotFulfillmentType; +use crate::load; use crate::math::casting::Cast; +use crate::math::constants::{QUOTE_SPOT_MARKET_INDEX, THIRTEEN_DAY}; use crate::math::liquidation::is_user_being_liquidated; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; @@ -113,11 +114,8 @@ use crate::validation::position::validate_perp_position_with_perp_market; use crate::validation::user::validate_user_deletion; use crate::validation::whitelist::validate_whitelist_token; use crate::{controller, math}; -use crate::{get_then_update_id, QUOTE_SPOT_MARKET_INDEX}; -use crate::{load, THIRTEEN_DAY}; use crate::{load_mut, ExchangeStatus}; use anchor_lang::solana_program::sysvar::instructions; -use anchor_spl::associated_token::AssociatedToken; use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::sysvar::instructions::ID as IX_ID; @@ -3681,15 +3679,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( )?; } } else { - let mut whitelisted_programs = vec![ - serum_program::id(), - AssociatedToken::id(), - jupiter_mainnet_3::ID, - jupiter_mainnet_4::ID, - jupiter_mainnet_6::ID, - dflow_mainnet_aggregator_4::ID, - titan_mainnet_argos_v1::ID, - ]; + let mut whitelisted_programs = WHITELISTED_SWAP_PROGRAMS.to_vec(); if !delegate_is_signer { whitelisted_programs.push(Token::id()); whitelisted_programs.push(Token2022::id()); diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index a393cf0670..fbf629a34d 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -6,7 +6,6 @@ use anchor_lang::prelude::*; use instructions::*; #[cfg(test)] -use math::amm; use math::{bn, constants::*}; use state::oracle::OracleSource; @@ -982,6 +981,7 @@ pub mod drift { curve_update_intensity: u8, amm_jit_intensity: u8, name: [u8; 32], + lp_pool_id: u8, ) -> Result<()> { handle_initialize_perp_market( ctx, @@ -1010,9 +1010,22 @@ pub mod drift { curve_update_intensity, amm_jit_intensity, name, + lp_pool_id, ) } + pub fn initialize_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeAmmCache<'info>>, + ) -> Result<()> { + handle_initialize_amm_cache(ctx) + } + + pub fn update_initial_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + ) -> Result<()> { + handle_update_initial_amm_cache_info(ctx) + } + pub fn initialize_prediction_market<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, AdminUpdatePerpMarket<'info>>, ) -> Result<()> { @@ -1064,6 +1077,32 @@ pub mod drift { handle_update_perp_market_expiry(ctx, expiry_ts) } + pub fn update_perp_market_lp_pool_paused_operations( + ctx: Context, + lp_paused_operations: u8, + ) -> Result<()> { + handle_update_perp_market_lp_pool_paused_operations(ctx, lp_paused_operations) + } + + pub fn update_perp_market_lp_pool_status( + ctx: Context, + lp_status: u8, + ) -> Result<()> { + handle_update_perp_market_lp_pool_status(ctx, lp_status) + } + + pub fn update_perp_market_lp_pool_fee_transfer_scalar( + ctx: Context, + optional_lp_fee_transfer_scalar: Option, + optional_lp_net_pnl_transfer_scalar: Option, + ) -> Result<()> { + handle_update_perp_market_lp_pool_fee_transfer_scalar( + ctx, + optional_lp_fee_transfer_scalar, + optional_lp_net_pnl_transfer_scalar, + ) + } + pub fn settle_expired_market_pools_to_revenue_pool( ctx: Context, ) -> Result<()> { @@ -1163,6 +1202,13 @@ pub mod drift { handle_update_perp_liquidation_fee(ctx, liquidator_fee, if_liquidation_fee) } + pub fn update_perp_market_lp_pool_id( + ctx: Context, + lp_pool_id: u8, + ) -> Result<()> { + handle_update_perp_lp_pool_id(ctx, lp_pool_id) + } + pub fn update_insurance_fund_unstaking_period( ctx: Context, insurance_fund_unstaking_period: i64, @@ -1869,6 +1915,287 @@ pub mod drift { ) -> Result<()> { handle_change_approved_builder(ctx, builder, max_fee_bps, add) } + + pub fn initialize_lp_pool( + ctx: Context, + lp_pool_id: u8, + min_mint_fee: i64, + max_aum: u128, + max_settle_quote_amount_per_market: u64, + whitelist_mint: Pubkey, + ) -> Result<()> { + handle_initialize_lp_pool( + ctx, + lp_pool_id, + min_mint_fee, + max_aum, + max_settle_quote_amount_per_market, + whitelist_mint, + ) + } + + pub fn update_feature_bit_flags_settle_lp_pool( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_settle_lp_pool(ctx, enable) + } + + pub fn update_feature_bit_flags_swap_lp_pool( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_swap_lp_pool(ctx, enable) + } + + pub fn update_feature_bit_flags_mint_redeem_lp_pool( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_mint_redeem_lp_pool(ctx, enable) + } + + pub fn initialize_constituent<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, + spot_market_index: u16, + decimals: u8, + max_weight_deviation: i64, + swap_fee_min: i64, + swap_fee_max: i64, + max_borrow_token_amount: u64, + oracle_staleness_threshold: u64, + cost_to_trade: i32, + constituent_derivative_index: Option, + constituent_derivative_depeg_threshold: u64, + derivative_weight: u64, + volatility: u64, + gamma_execution: u8, + gamma_inventory: u8, + xi: u8, + new_constituent_correlations: Vec, + ) -> Result<()> { + handle_initialize_constituent( + ctx, + spot_market_index, + decimals, + max_weight_deviation, + swap_fee_min, + swap_fee_max, + max_borrow_token_amount, + oracle_staleness_threshold, + cost_to_trade, + constituent_derivative_index, + constituent_derivative_depeg_threshold, + derivative_weight, + volatility, + gamma_execution, + gamma_inventory, + xi, + new_constituent_correlations, + ) + } + + pub fn update_constituent_status<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateConstituentStatus<'info>>, + new_status: u8, + ) -> Result<()> { + handle_update_constituent_status(ctx, new_status) + } + + pub fn update_constituent_paused_operations<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateConstituentPausedOperations<'info>>, + paused_operations: u8, + ) -> Result<()> { + handle_update_constituent_paused_operations(ctx, paused_operations) + } + + pub fn update_constituent_params( + ctx: Context, + constituent_params: ConstituentParams, + ) -> Result<()> { + handle_update_constituent_params(ctx, constituent_params) + } + + pub fn update_lp_pool_params( + ctx: Context, + lp_pool_params: LpPoolParams, + ) -> Result<()> { + handle_update_lp_pool_params(ctx, lp_pool_params) + } + + pub fn add_amm_constituent_mapping_data( + ctx: Context, + amm_constituent_mapping_data: Vec, + ) -> Result<()> { + handle_add_amm_constituent_data(ctx, amm_constituent_mapping_data) + } + + pub fn update_amm_constituent_mapping_data( + ctx: Context, + amm_constituent_mapping_data: Vec, + ) -> Result<()> { + handle_update_amm_constituent_mapping_data(ctx, amm_constituent_mapping_data) + } + + pub fn remove_amm_constituent_mapping_data<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, RemoveAmmConstituentMappingData<'info>>, + perp_market_index: u16, + constituent_index: u16, + ) -> Result<()> { + handle_remove_amm_constituent_mapping_data(ctx, perp_market_index, constituent_index) + } + + pub fn update_constituent_correlation_data( + ctx: Context, + index1: u16, + index2: u16, + correlation: i64, + ) -> Result<()> { + handle_update_constituent_correlation_data(ctx, index1, index2, correlation) + } + + pub fn update_lp_constituent_target_base<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, + ) -> Result<()> { + handle_update_constituent_target_base(ctx) + } + + pub fn update_lp_pool_aum<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateLPPoolAum<'info>>, + ) -> Result<()> { + handle_update_lp_pool_aum(ctx) + } + + pub fn update_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateAmmCache<'info>>, + ) -> Result<()> { + handle_update_amm_cache(ctx) + } + + pub fn override_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + market_index: u16, + override_params: OverrideAmmCacheParams, + ) -> Result<()> { + handle_override_amm_cache_info(ctx, market_index, override_params) + } + + pub fn reset_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResetAmmCache<'info>>, + ) -> Result<()> { + handle_reset_amm_cache(ctx) + } + + pub fn lp_pool_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + min_out_amount: u64, + ) -> Result<()> { + handle_lp_pool_swap( + ctx, + in_market_index, + out_market_index, + in_amount, + min_out_amount, + ) + } + + pub fn view_lp_pool_swap_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolSwapFees<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + in_target_weight: i64, + out_target_weight: i64, + ) -> Result<()> { + handle_view_lp_pool_swap_fees( + ctx, + in_market_index, + out_market_index, + in_amount, + in_target_weight, + out_target_weight, + ) + } + + pub fn lp_pool_add_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolAddLiquidity<'info>>, + in_market_index: u16, + in_amount: u128, + min_mint_amount: u64, + ) -> Result<()> { + handle_lp_pool_add_liquidity(ctx, in_market_index, in_amount, min_mint_amount) + } + + pub fn lp_pool_remove_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolRemoveLiquidity<'info>>, + in_market_index: u16, + in_amount: u64, + min_out_amount: u128, + ) -> Result<()> { + handle_lp_pool_remove_liquidity(ctx, in_market_index, in_amount, min_out_amount) + } + + pub fn view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolAddLiquidityFees<'info>>, + in_market_index: u16, + in_amount: u128, + ) -> Result<()> { + handle_view_lp_pool_add_liquidity_fees(ctx, in_market_index, in_amount) + } + + pub fn view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolRemoveLiquidityFees<'info>>, + in_market_index: u16, + in_amount: u64, + ) -> Result<()> { + handle_view_lp_pool_remove_liquidity_fees(ctx, in_market_index, in_amount) + } + + pub fn begin_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + amount_in: u64, + ) -> Result<()> { + handle_begin_lp_swap(ctx, in_market_index, out_market_index, amount_in) + } + + pub fn end_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + _in_market_index: u16, + _out_market_index: u16, + ) -> Result<()> { + handle_end_lp_swap(ctx) + } + + pub fn update_constituent_oracle_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentOracleInfo<'info>>, + ) -> Result<()> { + handle_update_constituent_oracle_info(ctx) + } + + pub fn deposit_to_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositProgramVault<'info>>, + amount: u64, + ) -> Result<()> { + handle_deposit_to_program_vault(ctx, amount) + } + + pub fn withdraw_from_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, WithdrawProgramVault<'info>>, + amount: u64, + ) -> Result<()> { + handle_withdraw_from_program_vault(ctx, amount) + } + + pub fn settle_perp_to_lp_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>, + ) -> Result<()> { + handle_settle_perp_to_lp_pool(ctx) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/drift/src/math/amm.rs b/programs/drift/src/math/amm.rs index 0407f627cf..914dffe621 100644 --- a/programs/drift/src/math/amm.rs +++ b/programs/drift/src/math/amm.rs @@ -10,8 +10,8 @@ use crate::math::casting::Cast; use crate::math::constants::{ BID_ASK_SPREAD_PRECISION_I128, CONCENTRATION_PRECISION, DEFAULT_MAX_TWAP_UPDATE_PRICE_BAND_DENOMINATOR, FIVE_MINUTE, ONE_HOUR, ONE_MINUTE, - PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, - PRICE_TO_PEG_PRECISION_RATIO, + PERCENTAGE_PRECISION_U64, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, + PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, PRICE_TO_PEG_PRECISION_RATIO, }; use crate::math::orders::standardize_base_asset_amount; use crate::math::quote_asset::reserve_to_asset_amount; @@ -19,7 +19,7 @@ use crate::math::stats::{calculate_new_twap, calculate_rolling_sum, calculate_we use crate::state::oracle::{MMOraclePriceData, OraclePriceData}; use crate::state::perp_market::AMM; use crate::state::state::PriceDivergenceGuardRails; -use crate::{validate, PERCENTAGE_PRECISION_U64}; +use crate::validate; use super::helpers::get_proportion_u128; use crate::math::safe_math::SafeMath; diff --git a/programs/drift/src/math/auction.rs b/programs/drift/src/math/auction.rs index 53263983c0..476dd539d0 100644 --- a/programs/drift/src/math/auction.rs +++ b/programs/drift/src/math/auction.rs @@ -1,7 +1,7 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; -use crate::math::constants::AUCTION_DERIVE_PRICE_FRACTION; +use crate::math::constants::{AUCTION_DERIVE_PRICE_FRACTION, MAX_PREDICTION_MARKET_PRICE}; use crate::math::orders::standardize_price; use crate::math::safe_math::SafeMath; use crate::msg; @@ -10,7 +10,7 @@ use crate::state::perp_market::ContractTier; use crate::state::user::{Order, OrderBitFlag, OrderType}; use crate::state::perp_market::PerpMarket; -use crate::{OrderParams, MAX_PREDICTION_MARKET_PRICE}; +use crate::OrderParams; use std::cmp::min; #[cfg(test)] diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index dc6ffeb94f..cf9148aff6 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -53,6 +53,8 @@ pub const PERCENTAGE_PRECISION: u128 = 1_000_000; // expo -6 (represents 100%) pub const PERCENTAGE_PRECISION_I128: i128 = PERCENTAGE_PRECISION as i128; pub const PERCENTAGE_PRECISION_U64: u64 = PERCENTAGE_PRECISION as u64; pub const PERCENTAGE_PRECISION_I64: i64 = PERCENTAGE_PRECISION as i64; +pub const PERCENTAGE_PRECISION_I32: i32 = PERCENTAGE_PRECISION as i32; + pub const TEN_BPS: i128 = PERCENTAGE_PRECISION_I128 / 1000; pub const TEN_BPS_I64: i64 = TEN_BPS as i64; pub const TWO_PT_TWO_PCT: i128 = 22_000; diff --git a/programs/drift/src/math/cp_curve/tests.rs b/programs/drift/src/math/cp_curve/tests.rs index ca7de7e987..b6c8b667b7 100644 --- a/programs/drift/src/math/cp_curve/tests.rs +++ b/programs/drift/src/math/cp_curve/tests.rs @@ -1,14 +1,10 @@ use crate::controller::amm::update_spreads; use crate::controller::position::PositionDirection; -use crate::math::amm::calculate_bid_ask_bounds; -use crate::math::constants::BASE_PRECISION; -use crate::math::constants::CONCENTRATION_PRECISION; use crate::math::constants::{ - BASE_PRECISION_U64, MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, QUOTE_PRECISION_I64, + MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, QUOTE_PRECISION_I64, }; use crate::math::cp_curve::*; use crate::state::perp_market::AMM; -use crate::state::user::PerpPosition; #[test] fn k_update_results_bound_flag() { @@ -331,10 +327,6 @@ fn amm_spread_adj_logic() { // let (t_price, _t_qar, _t_bar) = calculate_terminal_price_and_reserves(&market.amm).unwrap(); // market.amm.terminal_quote_asset_reserve = _t_qar; - let mut position = PerpPosition { - ..PerpPosition::default() - }; - // todo fix this market.amm.base_asset_amount_per_lp = 1; diff --git a/programs/drift/src/math/fees.rs b/programs/drift/src/math/fees.rs index 4b358b071a..8431be24bf 100644 --- a/programs/drift/src/math/fees.rs +++ b/programs/drift/src/math/fees.rs @@ -16,8 +16,8 @@ use crate::math::safe_math::SafeMath; use crate::state::state::{FeeStructure, FeeTier, OrderFillerRewardStructure}; use crate::state::user::{MarketType, UserStats}; +use crate::math::constants::{FEE_ADJUSTMENT_MAX, QUOTE_PRECISION_U64}; use crate::msg; -use crate::{FEE_ADJUSTMENT_MAX, QUOTE_PRECISION_U64}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/math/fuel.rs b/programs/drift/src/math/fuel.rs index 2cd139d9f3..5069761c98 100644 --- a/programs/drift/src/math/fuel.rs +++ b/programs/drift/src/math/fuel.rs @@ -1,9 +1,9 @@ use crate::error::DriftResult; use crate::math::casting::Cast; +use crate::math::constants::{FUEL_WINDOW_U128, QUOTE_PRECISION, QUOTE_PRECISION_U64}; use crate::math::safe_math::SafeMath; use crate::state::perp_market::PerpMarket; use crate::state::spot_market::SpotMarket; -use crate::{FUEL_WINDOW_U128, QUOTE_PRECISION, QUOTE_PRECISION_U64}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/math/fulfillment/tests.rs b/programs/drift/src/math/fulfillment/tests.rs index d12afa60b1..005bbefa63 100644 --- a/programs/drift/src/math/fulfillment/tests.rs +++ b/programs/drift/src/math/fulfillment/tests.rs @@ -1,11 +1,9 @@ mod determine_perp_fulfillment_methods { use crate::controller::position::PositionDirection; use crate::math::constants::{ - AMM_RESERVE_PRECISION, PEG_PRECISION, PRICE_PRECISION, PRICE_PRECISION_I64, - PRICE_PRECISION_U64, + AMM_RESERVE_PRECISION, PEG_PRECISION, PRICE_PRECISION, PRICE_PRECISION_U64, }; use crate::math::fulfillment::determine_perp_fulfillment_methods; - use crate::state::fill_mode::FillMode; use crate::state::fulfillment::PerpFulfillmentMethod; use crate::state::oracle::HistoricalOracleData; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; @@ -52,8 +50,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -109,8 +105,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -178,8 +172,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -242,8 +234,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -310,8 +300,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -385,8 +373,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -454,8 +440,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -522,8 +506,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -589,8 +571,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -658,8 +638,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -718,8 +696,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -779,8 +755,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -837,8 +811,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( diff --git a/programs/drift/src/math/insurance.rs b/programs/drift/src/math/insurance.rs index 1232a19451..4658f97db6 100644 --- a/programs/drift/src/math/insurance.rs +++ b/programs/drift/src/math/insurance.rs @@ -1,7 +1,8 @@ -use crate::{msg, PRICE_PRECISION}; +use crate::msg; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; +use crate::math::constants::PRICE_PRECISION; use crate::math::helpers::{get_proportion_u128, log10_iter}; use crate::math::safe_math::SafeMath; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 24a54afc59..dbe608ceaa 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -12,6 +12,7 @@ use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_l use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; +use crate::math::constants::{BASE_PRECISION, LIQUIDATION_FEE_INCREASE_PER_SLOT}; use crate::math::spot_swap::calculate_swap_price; use crate::msg; use crate::state::margin_calculation::MarginContext; @@ -21,10 +22,7 @@ use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::{OrderType, User}; -use crate::{ - validate, MarketType, OrderParams, PositionDirection, BASE_PRECISION, - LIQUIDATION_FEE_INCREASE_PER_SLOT, -}; +use crate::{validate, MarketType, OrderParams, PositionDirection}; pub const LIQUIDATION_FEE_ADJUST_GRACE_PERIOD_SLOTS: u64 = 1_500; // ~10 minutes diff --git a/programs/drift/src/math/lp_pool.rs b/programs/drift/src/math/lp_pool.rs new file mode 100644 index 0000000000..cb35bf743d --- /dev/null +++ b/programs/drift/src/math/lp_pool.rs @@ -0,0 +1,268 @@ +pub mod perp_lp_pool_settlement { + use core::slice::Iter; + use std::iter::Peekable; + + use crate::error::ErrorCode; + use crate::math::casting::Cast; + use crate::math::constants::QUOTE_PRECISION_U64; + use crate::math::spot_balance::get_token_amount; + use crate::state::spot_market::{SpotBalance, SpotBalanceType}; + use crate::{ + math::safe_math::SafeMath, + state::{amm_cache::CacheInfo, perp_market::PerpMarket, spot_market::SpotMarket}, + *, + }; + use anchor_spl::token_interface::{TokenAccount, TokenInterface}; + + #[derive(Debug, Clone, Copy)] + pub struct SettlementResult { + pub amount_transferred: u64, + pub direction: SettlementDirection, + pub fee_pool_used: u128, + pub pnl_pool_used: u128, + } + + #[derive(Debug, Clone, Copy, PartialEq)] + pub enum SettlementDirection { + ToLpPool, + FromLpPool, + None, + } + + pub struct SettlementContext<'a> { + pub quote_owed_from_lp: i64, + pub quote_constituent_token_balance: u64, + pub fee_pool_balance: u128, + pub pnl_pool_balance: u128, + pub quote_market: &'a SpotMarket, + pub max_settle_quote_amount: u64, + } + + pub fn calculate_settlement_amount(ctx: &SettlementContext) -> Result { + if ctx.quote_owed_from_lp > 0 { + calculate_lp_to_perp_settlement(ctx) + } else if ctx.quote_owed_from_lp < 0 { + calculate_perp_to_lp_settlement(ctx) + } else { + Ok(SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }) + } + } + + pub fn validate_settlement_amount( + ctx: &SettlementContext, + result: &SettlementResult, + perp_market: &PerpMarket, + quote_spot_market: &SpotMarket, + ) -> Result<()> { + if result.amount_transferred > ctx.max_settle_quote_amount { + msg!( + "Amount to settle exceeds maximum allowed, {} > {}", + result.amount_transferred, + ctx.max_settle_quote_amount + ); + return Err(ErrorCode::LpPoolSettleInvariantBreached.into()); + } + + if result.direction == SettlementDirection::ToLpPool { + if result.fee_pool_used > 0 { + let fee_pool_token_amount = get_token_amount( + perp_market.amm.fee_pool.balance(), + quote_spot_market, + &SpotBalanceType::Deposit, + )?; + validate!( + fee_pool_token_amount >= result.fee_pool_used, + ErrorCode::LpPoolSettleInvariantBreached.into(), + "Fee pool balance insufficient for settlement: {} < {}", + fee_pool_token_amount, + result.fee_pool_used + )?; + } + + if result.pnl_pool_used > 0 { + let pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.balance(), + quote_spot_market, + &SpotBalanceType::Deposit, + )?; + validate!( + pnl_pool_token_amount >= result.pnl_pool_used, + ErrorCode::LpPoolSettleInvariantBreached.into(), + "Pnl pool balance insufficient for settlement: {} < {}", + pnl_pool_token_amount, + result.pnl_pool_used + )?; + } + } + if result.direction == SettlementDirection::FromLpPool { + validate!( + ctx.quote_constituent_token_balance + .saturating_sub(result.amount_transferred) + >= QUOTE_PRECISION_U64, + ErrorCode::LpPoolSettleInvariantBreached.into(), + "Quote constituent token balance insufficient for settlement: {} < {}", + ctx.quote_constituent_token_balance, + result.amount_transferred + )?; + } + Ok(()) + } + + fn calculate_lp_to_perp_settlement(ctx: &SettlementContext) -> Result { + if ctx.quote_constituent_token_balance == 0 { + return Ok(SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }); + } + + let amount_to_send = ctx + .quote_owed_from_lp + .cast::()? + .min( + ctx.quote_constituent_token_balance + .saturating_sub(QUOTE_PRECISION_U64), + ) + .min(ctx.max_settle_quote_amount); + + Ok(SettlementResult { + amount_transferred: amount_to_send, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }) + } + + fn calculate_perp_to_lp_settlement(ctx: &SettlementContext) -> Result { + let amount_to_send = + (ctx.quote_owed_from_lp.abs().cast::()?).min(ctx.max_settle_quote_amount); + + if ctx.fee_pool_balance >= amount_to_send as u128 { + // Fee pool can cover entire amount + Ok(SettlementResult { + amount_transferred: amount_to_send, + direction: SettlementDirection::ToLpPool, + fee_pool_used: amount_to_send as u128, + pnl_pool_used: 0, + }) + } else { + // Need to use both fee pool and pnl pool + let remaining_amount = (amount_to_send as u128).safe_sub(ctx.fee_pool_balance)?; + let pnl_pool_used = remaining_amount.min(ctx.pnl_pool_balance); + let actual_transfer = ctx.fee_pool_balance.safe_add(pnl_pool_used)?; + + Ok(SettlementResult { + amount_transferred: actual_transfer as u64, + direction: SettlementDirection::ToLpPool, + fee_pool_used: ctx.fee_pool_balance, + pnl_pool_used, + }) + } + } + + pub fn execute_token_transfer<'info>( + token_program: &Interface<'info, TokenInterface>, + from_vault: &InterfaceAccount<'info, TokenAccount>, + to_vault: &InterfaceAccount<'info, TokenAccount>, + signer: &AccountInfo<'info>, + signer_seed: &[&[u8]], + amount: u64, + remaining_accounts: Option<&mut Peekable>>>, + ) -> Result<()> { + controller::token::send_from_program_vault_with_signature_seeds( + token_program, + from_vault, + to_vault, + signer, + signer_seed, + amount, + &None, + remaining_accounts, + ) + } + + // Market state updates + pub fn update_perp_market_pools_and_quote_market_balance( + perp_market: &mut PerpMarket, + result: &SettlementResult, + quote_spot_market: &mut SpotMarket, + ) -> Result<()> { + match result.direction { + SettlementDirection::FromLpPool => { + controller::spot_balance::update_spot_balances( + result.amount_transferred as u128, + &SpotBalanceType::Deposit, + quote_spot_market, + &mut perp_market.amm.fee_pool, + false, + )?; + } + SettlementDirection::ToLpPool => { + if result.fee_pool_used > 0 { + controller::spot_balance::update_spot_balances( + result.fee_pool_used, + &SpotBalanceType::Borrow, + quote_spot_market, + &mut perp_market.amm.fee_pool, + true, + )?; + } + if result.pnl_pool_used > 0 { + controller::spot_balance::update_spot_balances( + result.pnl_pool_used, + &SpotBalanceType::Borrow, + quote_spot_market, + &mut perp_market.pnl_pool, + true, + )?; + } + } + SettlementDirection::None => {} + } + Ok(()) + } + + pub fn update_cache_info( + cache_info: &mut CacheInfo, + result: &SettlementResult, + new_quote_owed: i64, + slot: u64, + now: i64, + ) -> Result<()> { + cache_info.quote_owed_from_lp_pool = new_quote_owed; + cache_info.last_settle_amount = result.amount_transferred; + cache_info.last_settle_slot = slot; + cache_info.last_settle_ts = now; + cache_info.last_settle_amm_ex_fees = cache_info.last_exchange_fees; + cache_info.last_settle_amm_pnl = cache_info.last_net_pnl_pool_token_amount; + + match result.direction { + SettlementDirection::FromLpPool => { + cache_info.last_fee_pool_token_amount = cache_info + .last_fee_pool_token_amount + .safe_add(result.amount_transferred as u128)?; + } + SettlementDirection::ToLpPool => { + if result.fee_pool_used > 0 { + cache_info.last_fee_pool_token_amount = cache_info + .last_fee_pool_token_amount + .safe_sub(result.fee_pool_used)?; + } + if result.pnl_pool_used > 0 { + cache_info.last_net_pnl_pool_token_amount = cache_info + .last_net_pnl_pool_token_amount + .safe_sub(result.pnl_pool_used as i128)?; + } + } + SettlementDirection::None => {} + } + Ok(()) + } +} diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 341f2d8b81..fc31624842 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -7,9 +7,9 @@ use crate::math::constants::{ use crate::math::oracle::LogMode; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; -use crate::MARGIN_PRECISION; -use crate::{validate, PRICE_PRECISION_I128}; -use crate::{validation, PRICE_PRECISION_I64}; +use crate::math::constants::{MARGIN_PRECISION, PRICE_PRECISION_I128, PRICE_PRECISION_I64}; +use crate::validate; +use crate::validation; use crate::math::casting::Cast; use crate::math::funding::calculate_funding_payment; @@ -83,11 +83,11 @@ pub fn calc_high_leverage_mode_initial_margin_ratio_from_size( ) -> DriftResult { let result = if size_adj_margin_ratio < pre_size_adj_margin_ratio { let size_pct_discount_factor = PERCENTAGE_PRECISION.saturating_sub( - (pre_size_adj_margin_ratio + pre_size_adj_margin_ratio .cast::()? .safe_sub(size_adj_margin_ratio.cast::()?)? .safe_mul(PERCENTAGE_PRECISION)? - .safe_div((pre_size_adj_margin_ratio.safe_div(5)?).cast::()?)?), + .safe_div((pre_size_adj_margin_ratio.safe_div(5)?).cast::()?)?, ); let hlm_margin_delta = pre_size_adj_margin_ratio diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 2e9e25da44..d4b1eefd2e 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -1,9 +1,5 @@ #[cfg(test)] mod test { - use num_integer::Roots; - - use crate::amm::calculate_swap_output; - use crate::controller::amm::SwapDirection; use crate::math::constants::{ AMM_RESERVE_PRECISION, PRICE_PRECISION, PRICE_PRECISION_U64, QUOTE_PRECISION, QUOTE_PRECISION_I64, SPOT_IMF_PRECISION, @@ -18,6 +14,7 @@ mod test { PRICE_PRECISION_I64, QUOTE_PRECISION_U64, SPOT_BALANCE_PRECISION, SPOT_CUMULATIVE_INTEREST_PRECISION, }; + use num_integer::Roots; #[test] fn asset_tier_checks() { diff --git a/programs/drift/src/math/mod.rs b/programs/drift/src/math/mod.rs index 89edbdafc5..17a972d826 100644 --- a/programs/drift/src/math/mod.rs +++ b/programs/drift/src/math/mod.rs @@ -16,6 +16,7 @@ pub mod funding; pub mod helpers; pub mod insurance; pub mod liquidation; +pub mod lp_pool; pub mod margin; pub mod matching; pub mod oracle; diff --git a/programs/drift/src/math/oracle.rs b/programs/drift/src/math/oracle.rs index f2bb0f20f0..92bf2a47e8 100644 --- a/programs/drift/src/math/oracle.rs +++ b/programs/drift/src/math/oracle.rs @@ -11,6 +11,7 @@ use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::PerpMarket; use crate::state::state::{OracleGuardRails, ValidityGuardRails}; use crate::state::user::MarketType; +use std::convert::TryFrom; use std::fmt; #[cfg(test)] @@ -71,6 +72,59 @@ impl fmt::Display for OracleValidity { } } +impl TryFrom for OracleValidity { + type Error = ErrorCode; + + fn try_from(v: u8) -> DriftResult { + match v { + 0 => Ok(OracleValidity::NonPositive), + 1 => Ok(OracleValidity::TooVolatile), + 2 => Ok(OracleValidity::TooUncertain), + 3 => Ok(OracleValidity::StaleForMargin), + 4 => Ok(OracleValidity::InsufficientDataPoints), + 5 => Ok(OracleValidity::StaleForAMM { + immediate: true, + low_risk: true, + }), + 6 => Ok(OracleValidity::StaleForAMM { + immediate: true, + low_risk: false, + }), + 7 => Ok(OracleValidity::Valid), + _ => panic!("Invalid OracleValidity"), + } + } +} + +impl From for u8 { + fn from(src: OracleValidity) -> u8 { + match src { + OracleValidity::NonPositive => 0, + OracleValidity::TooVolatile => 1, + OracleValidity::TooUncertain => 2, + OracleValidity::StaleForMargin => 3, + OracleValidity::InsufficientDataPoints => 4, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: true, + } => 5, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: false, + } => 6, + OracleValidity::Valid + | OracleValidity::StaleForAMM { + immediate: false, + low_risk: false, + } => 7, + OracleValidity::StaleForAMM { + immediate: false, + low_risk: true, + } => unreachable!(), + } + } +} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] pub enum DriftAction { UpdateFunding, @@ -85,6 +139,9 @@ pub enum DriftAction { UpdateAMMCurve, OracleOrderPrice, UseMMOraclePrice, + UpdateAmmCache, + UpdateLpPoolAum, + LpPoolSwap, } pub fn is_oracle_valid_for_action( @@ -142,7 +199,10 @@ pub fn is_oracle_valid_for_action( | OracleValidity::InsufficientDataPoints | OracleValidity::StaleForMargin ), - DriftAction::FillOrderMatch => !matches!( + DriftAction::FillOrderMatch + | DriftAction::UpdateAmmCache + | DriftAction::UpdateLpPoolAum + | DriftAction::LpPoolSwap => !matches!( oracle_validity, OracleValidity::NonPositive | OracleValidity::TooVolatile diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index b4495afd5c..7f4844da5a 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -8,17 +8,16 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::amm::calculate_amm_available_liquidity; use crate::math::casting::Cast; -use crate::state::protected_maker_mode_config::ProtectedMakerParams; -use crate::state::user::OrderBitFlag; -use crate::PERCENTAGE_PRECISION_I128; -use crate::{ - load, math, FeeTier, BASE_PRECISION_I128, FEE_ADJUSTMENT_MAX, MARGIN_PRECISION_I128, +use crate::math::constants::{ + BASE_PRECISION_I128, FEE_ADJUSTMENT_MAX, MARGIN_PRECISION_I128, MARGIN_PRECISION_U128, MAX_PREDICTION_MARKET_PRICE, MAX_PREDICTION_MARKET_PRICE_I64, OPEN_ORDER_MARGIN_REQUIREMENT, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I128, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, - SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_I128, + PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I128, PRICE_PRECISION_U64, + QUOTE_PRECISION_I128, SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_I128, }; +use crate::state::protected_maker_mode_config::ProtectedMakerParams; +use crate::state::user::OrderBitFlag; +use crate::{load, math, FeeTier}; -use crate::math::constants::MARGIN_PRECISION_U128; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, }; diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 9d01e4dbb1..339aa20361 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -1956,7 +1956,7 @@ mod calculate_max_perp_order_size { use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{MarginMode, Order, PerpPosition, SpotPosition, User, UserStatus}; + use crate::state::user::{MarginMode, Order, PerpPosition, SpotPosition, User}; use crate::test_utils::get_pyth_price; use crate::test_utils::*; use crate::{ @@ -3243,7 +3243,7 @@ mod calculate_max_perp_order_size { scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, ..SpotPosition::default() }; - let mut user = User { + let user = User { orders: [Order::default(); 32], perp_positions: get_positions(PerpPosition { market_index: 0, @@ -3273,7 +3273,7 @@ mod calculate_max_perp_order_size { scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, ..SpotPosition::default() }; - let mut user = User { + let user = User { orders: [Order::default(); 32], perp_positions: get_positions(PerpPosition { market_index: 0, @@ -3533,7 +3533,7 @@ mod calculate_max_perp_order_size { &lazer_program, ); - let mut account_infos = vec![ + let account_infos = vec![ usdc_oracle_info, sol_oracle_info, eth_oracle_info, diff --git a/programs/drift/src/math/position.rs b/programs/drift/src/math/position.rs index 8201123f09..e40c75205d 100644 --- a/programs/drift/src/math/position.rs +++ b/programs/drift/src/math/position.rs @@ -5,15 +5,14 @@ use crate::math::amm; use crate::math::amm::calculate_quote_asset_amount_swapped; use crate::math::casting::Cast; use crate::math::constants::{ - AMM_RESERVE_PRECISION_I128, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, - PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, + AMM_RESERVE_PRECISION_I128, BASE_PRECISION, MAX_PREDICTION_MARKET_PRICE_U128, + PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, }; use crate::math::pnl::calculate_pnl; use crate::math::safe_math::SafeMath; use crate::state::perp_market::{ContractType, AMM}; use crate::state::user::PerpPosition; -use crate::{BASE_PRECISION, MAX_PREDICTION_MARKET_PRICE_U128}; pub fn calculate_base_asset_value_and_pnl( base_asset_amount: i128, diff --git a/programs/drift/src/math/spot_swap.rs b/programs/drift/src/math/spot_swap.rs index 10bcc87e16..8087786c05 100644 --- a/programs/drift/src/math/spot_swap.rs +++ b/programs/drift/src/math/spot_swap.rs @@ -1,12 +1,13 @@ use crate::error::DriftResult; use crate::math::casting::Cast; +use crate::math::constants::{PRICE_PRECISION, SPOT_WEIGHT_PRECISION_U128}; use crate::math::margin::MarginRequirementType; use crate::math::orders::{calculate_fill_price, validate_fill_price_within_price_bands}; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::{get_strict_token_value, get_token_value}; use crate::state::oracle::StrictOraclePrice; use crate::state::spot_market::SpotMarket; -use crate::{PositionDirection, PRICE_PRECISION, SPOT_WEIGHT_PRECISION_U128}; +use crate::PositionDirection; #[cfg(test)] mod tests; diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs new file mode 100644 index 0000000000..090213235c --- /dev/null +++ b/programs/drift/src/state/amm_cache.rs @@ -0,0 +1,355 @@ +use std::convert::TryFrom; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::amm::calculate_net_user_pnl; +use crate::math::casting::Cast; +use crate::math::oracle::{is_oracle_valid_for_action, oracle_validity, DriftAction, LogMode}; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; +use crate::state::oracle::MMOraclePriceData; +use crate::state::oracle_map::OracleIdentifier; +use crate::state::perp_market::PerpMarket; +use crate::state::spot_market::{SpotBalance, SpotMarket}; +use crate::state::state::State; +use crate::state::traits::Size; +use crate::state::zero_copy::HasLen; +use crate::state::zero_copy::{AccountZeroCopy, AccountZeroCopyMut}; +use crate::validate; +use crate::OracleSource; +use crate::{impl_zero_copy_loader, OracleGuardRails}; + +use anchor_lang::prelude::*; + +use super::user::MarketType; + +pub const AMM_POSITIONS_CACHE: &str = "amm_cache_seed"; + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct AmmCache { + pub bump: u8, + _padding: [u8; 3], + pub cache: Vec, +} + +#[zero_copy] +#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +#[repr(C)] +pub struct CacheInfo { + pub oracle: Pubkey, + pub last_fee_pool_token_amount: u128, + pub last_net_pnl_pool_token_amount: i128, + pub last_exchange_fees: u128, + pub last_settle_amm_ex_fees: u128, + pub last_settle_amm_pnl: i128, + /// BASE PRECISION + pub position: i64, + pub slot: u64, + pub last_settle_amount: u64, + pub last_settle_slot: u64, + pub last_settle_ts: i64, + pub quote_owed_from_lp_pool: i64, + pub amm_inventory_limit: i64, + pub oracle_price: i64, + pub oracle_slot: u64, + pub oracle_source: u8, + pub oracle_validity: u8, + pub lp_status_for_perp_market: u8, + pub amm_position_scalar: u8, + pub _padding: [u8; 36], +} + +impl Size for CacheInfo { + const SIZE: usize = 230; +} + +impl Default for CacheInfo { + fn default() -> Self { + CacheInfo { + position: 0i64, + slot: 0u64, + oracle_price: 0i64, + oracle_slot: 0u64, + oracle_validity: 0u8, + oracle: Pubkey::default(), + last_fee_pool_token_amount: 0u128, + last_net_pnl_pool_token_amount: 0i128, + last_exchange_fees: 0u128, + last_settle_amount: 0u64, + last_settle_slot: 0u64, + last_settle_ts: 0i64, + last_settle_amm_pnl: 0i128, + last_settle_amm_ex_fees: 0u128, + amm_inventory_limit: 0i64, + oracle_source: 0u8, + quote_owed_from_lp_pool: 0i64, + lp_status_for_perp_market: 0u8, + amm_position_scalar: 0u8, + _padding: [0u8; 36], + } + } +} + +impl CacheInfo { + pub fn get_oracle_source(&self) -> DriftResult { + Ok(OracleSource::try_from(self.oracle_source)?) + } + + pub fn oracle_id(&self) -> DriftResult { + let oracle_source = self.get_oracle_source()?; + Ok((self.oracle, oracle_source)) + } + + pub fn get_last_available_amm_token_amount(&self) -> DriftResult { + let last_available_balance = self + .last_fee_pool_token_amount + .cast::()? + .safe_add(self.last_net_pnl_pool_token_amount)?; + Ok(last_available_balance) + } + + pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { + self.oracle = perp_market.amm.oracle; + self.oracle_source = u8::from(perp_market.amm.oracle_source); + self.position = perp_market + .amm + .get_protocol_owned_position()? + .safe_mul(-1)?; + self.lp_status_for_perp_market = perp_market.lp_status; + Ok(()) + } + + pub fn try_update_oracle_info( + &mut self, + clock_slot: u64, + oracle_price_data: &MMOraclePriceData, + perp_market: &PerpMarket, + oracle_guard_rails: &OracleGuardRails, + ) -> DriftResult<()> { + let safe_oracle_data = oracle_price_data.get_safe_oracle_price_data(); + let validity = oracle_validity( + MarketType::Perp, + perp_market.market_index, + perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap, + &safe_oracle_data, + &oracle_guard_rails.validity, + perp_market.get_max_confidence_interval_multiplier()?, + &perp_market.amm.oracle_source, + LogMode::SafeMMOracle, + perp_market.amm.oracle_slot_delay_override, + perp_market.amm.oracle_low_risk_slot_delay_override, + )?; + if is_oracle_valid_for_action(validity, Some(DriftAction::UpdateAmmCache))? { + self.oracle_price = safe_oracle_data.price; + self.oracle_slot = clock_slot.safe_sub(safe_oracle_data.delay.max(0) as u64)?; + self.oracle_validity = u8::from(validity); + } else { + msg!( + "Not updating oracle price for perp market {}. Oracle data is invalid", + perp_market.market_index + ); + } + self.slot = clock_slot; + + Ok(()) + } +} + +#[zero_copy] +#[derive(Default, Debug)] +#[repr(C)] +pub struct AmmCacheFixed { + pub bump: u8, + _pad: [u8; 3], + pub len: u32, +} + +impl HasLen for AmmCacheFixed { + fn len(&self) -> u32 { + self.len + } +} + +impl AmmCache { + pub fn space(num_markets: usize) -> usize { + 8 + 8 + 4 + num_markets * CacheInfo::SIZE + } + + pub fn validate(&self, state: &State) -> DriftResult<()> { + validate!( + self.cache.len() == state.number_of_markets as usize, + ErrorCode::DefaultError, + "Number of amm positions is different than number of markets" + )?; + Ok(()) + } + + pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { + let cache_info = self.cache.get_mut(perp_market.market_index as usize); + if let Some(cache_info) = cache_info { + cache_info.update_perp_market_fields(perp_market)?; + } else { + msg!( + "Updating amm cache from admin with perp market index not found in cache: {}", + perp_market.market_index + ); + return Err(ErrorCode::DefaultError.into()); + } + + Ok(()) + } + + pub fn update_oracle_info( + &mut self, + clock_slot: u64, + market_index: u16, + oracle_price_data: &MMOraclePriceData, + perp_market: &PerpMarket, + oracle_guard_rails: &OracleGuardRails, + ) -> DriftResult<()> { + let cache_info = self.cache.get_mut(market_index as usize); + if let Some(cache_info) = cache_info { + cache_info.try_update_oracle_info( + clock_slot, + oracle_price_data, + perp_market, + oracle_guard_rails, + )?; + } else { + msg!( + "Updating amm cache from admin with perp market index not found in cache: {}", + market_index + ); + return Err(ErrorCode::DefaultError.into()); + } + + Ok(()) + } +} + +impl_zero_copy_loader!(AmmCache, crate::id, AmmCacheFixed, CacheInfo); + +impl<'a> AccountZeroCopy<'a, CacheInfo, AmmCacheFixed> { + pub fn check_settle_staleness(&self, slot: u64, threshold_slot_diff: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot == 0 { + continue; + } + if cache_info.last_settle_slot < slot.saturating_sub(threshold_slot_diff) { + msg!("AMM settle data is stale for perp market {}", i); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } + + pub fn check_perp_market_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot == 0 { + continue; + } + if cache_info.slot < slot.saturating_sub(threshold) { + msg!("Perp market cache info is stale for perp market {}", i); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } + + pub fn check_oracle_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot == 0 { + continue; + } + if cache_info.oracle_slot < slot.saturating_sub(threshold) { + msg!( + "Perp market cache info is stale for perp market {}. oracle slot: {}, slot: {}", + i, + cache_info.oracle_slot, + slot + ); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } +} + +impl<'a> AccountZeroCopyMut<'a, CacheInfo, AmmCacheFixed> { + pub fn update_amount_owed_from_lp_pool( + &mut self, + perp_market: &PerpMarket, + quote_market: &SpotMarket, + ) -> DriftResult<()> { + let cached_info = self.get_mut(perp_market.market_index as u32); + + let fee_pool_token_amount = get_token_amount( + perp_market.amm.fee_pool.scaled_balance, + "e_market, + perp_market.amm.fee_pool.balance_type(), + )?; + + let net_pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.scaled_balance, + "e_market, + perp_market.pnl_pool.balance_type(), + )? + .cast::()? + .safe_sub(calculate_net_user_pnl( + &perp_market.amm, + cached_info.oracle_price, + )?)?; + + let amm_amount_available = + net_pnl_pool_token_amount.safe_add(fee_pool_token_amount.cast::()?)?; + + if cached_info.last_net_pnl_pool_token_amount == 0 + && cached_info.last_fee_pool_token_amount == 0 + && cached_info.last_exchange_fees == 0 + { + cached_info.last_fee_pool_token_amount = fee_pool_token_amount; + cached_info.last_net_pnl_pool_token_amount = net_pnl_pool_token_amount; + cached_info.last_exchange_fees = perp_market.amm.total_exchange_fee; + cached_info.last_settle_amm_ex_fees = perp_market.amm.total_exchange_fee; + cached_info.last_settle_amm_pnl = net_pnl_pool_token_amount; + return Ok(()); + } + + let exchange_fee_delta = perp_market + .amm + .total_exchange_fee + .saturating_sub(cached_info.last_exchange_fees); + + let amount_to_send_to_lp_pool = amm_amount_available + .safe_sub(cached_info.get_last_available_amm_token_amount()?)? + .safe_mul(perp_market.lp_fee_transfer_scalar as i128)? + .safe_div_ceil(100)? + .safe_sub( + exchange_fee_delta + .cast::()? + .safe_mul(perp_market.lp_exchange_fee_excluscion_scalar as i128)? + .safe_div_ceil(100)?, + )?; + + cached_info.quote_owed_from_lp_pool = cached_info + .quote_owed_from_lp_pool + .safe_sub(amount_to_send_to_lp_pool.cast::()?)?; + + cached_info.last_fee_pool_token_amount = fee_pool_token_amount; + cached_info.last_net_pnl_pool_token_amount = net_pnl_pool_token_amount; + cached_info.last_exchange_fees = perp_market.amm.total_exchange_fee; + + Ok(()) + } + + pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { + let cache_info = self.get_mut(perp_market.market_index as u32); + cache_info.update_perp_market_fields(perp_market)?; + + Ok(()) + } +} diff --git a/programs/drift/src/state/constituent_map.rs b/programs/drift/src/state/constituent_map.rs new file mode 100644 index 0000000000..57518c0d4b --- /dev/null +++ b/programs/drift/src/state/constituent_map.rs @@ -0,0 +1,253 @@ +use anchor_lang::accounts::account_loader::AccountLoader; +use std::cell::{Ref, RefMut}; +use std::collections::{BTreeMap, BTreeSet}; +use std::iter::Peekable; +use std::slice::Iter; + +use anchor_lang::prelude::{AccountInfo, Pubkey}; + +use anchor_lang::Discriminator; +use arrayref::array_ref; + +use crate::error::{DriftResult, ErrorCode}; + +use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::traits::Size; +use crate::{msg, validate}; +use std::panic::Location; + +use super::lp_pool::Constituent; + +pub struct ConstituentMap<'a>(pub BTreeMap>); + +impl<'a> ConstituentMap<'a> { + #[track_caller] + #[inline(always)] + pub fn get_ref(&self, constituent_index: &u16) -> DriftResult> { + let loader = match self.0.get(constituent_index) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find constituent {} at {}:{}", + constituent_index, + caller.file(), + caller.line() + ); + return Err(ErrorCode::ConstituentNotFound); + } + }; + + match loader.load() { + Ok(constituent) => Ok(constituent), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load constituent {} at {}:{}", + constituent_index, + caller.file(), + caller.line() + ); + Err(ErrorCode::ConstituentCouldNotLoad) + } + } + } + + #[track_caller] + #[inline(always)] + pub fn get_ref_mut(&self, market_index: &u16) -> DriftResult> { + let loader = match self.0.get(market_index) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find constituent {} at {}:{}", + market_index, + caller.file(), + caller.line() + ); + return Err(ErrorCode::ConstituentNotFound); + } + }; + + match loader.load_mut() { + Ok(perp_market) => Ok(perp_market), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load constituent {} at {}:{}", + market_index, + caller.file(), + caller.line() + ); + Err(ErrorCode::ConstituentCouldNotLoad) + } + } + } + + pub fn load<'b, 'c>( + writable_constituents: &'b ConstituentSet, + lp_pool_key: &Pubkey, + account_info_iter: &'c mut Peekable>>, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + while let Some(account_info) = account_info_iter.peek() { + if account_info.owner != &crate::ID { + break; + } + + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + msg!( + "didnt match constituent size, {}, {}", + data.len(), + expected_data_len + ); + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + msg!( + "didnt match account discriminator {:?}, {:?}", + account_discriminator, + constituent_discriminator + ); + break; + } + + // Pubkey + let constituent_lp_key = Pubkey::from(*array_ref![data, 72, 32]); + validate!( + &constituent_lp_key == lp_pool_key, + ErrorCode::InvalidConstituent, + "Constituent lp pool pubkey does not match lp pool pubkey" + )?; + + // constituent index 308 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 308, 2]); + if constituent_map.0.contains_key(&constituent_index) { + msg!( + "Can not include same constituent index twice {}", + constituent_index + ); + return Err(ErrorCode::InvalidConstituent); + } + + let account_info = account_info_iter.next().safe_unwrap()?; + + let is_writable = account_info.is_writable; + if writable_constituents.contains(&constituent_index) && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + let account_loader: AccountLoader = AccountLoader::try_from(account_info) + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + constituent_map.0.insert(constituent_index, account_loader); + } + + Ok(constituent_map) + } +} + +#[cfg(test)] +impl<'a> ConstituentMap<'a> { + pub fn load_one<'c: 'a>( + account_info: &'c AccountInfo<'a>, + must_be_writable: bool, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + // market index 1160 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 308, 2]); + + let is_writable = account_info.is_writable; + let account_loader: AccountLoader = + AccountLoader::try_from(account_info).or(Err(ErrorCode::InvalidMarketAccount))?; + + if must_be_writable && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + constituent_map.0.insert(constituent_index, account_loader); + + Ok(constituent_map) + } + + pub fn load_multiple<'c: 'a>( + account_info: Vec<&'c AccountInfo<'a>>, + must_be_writable: bool, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let account_info_iter = account_info.into_iter(); + for account_info in account_info_iter { + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let constituent_index = u16::from_le_bytes(*array_ref![data, 308, 2]); + + if constituent_map.0.contains_key(&constituent_index) { + msg!( + "Can not include same constituent index twice {}", + constituent_index + ); + return Err(ErrorCode::InvalidConstituent); + } + + let is_writable = account_info.is_writable; + let account_loader: AccountLoader = AccountLoader::try_from(account_info) + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + if must_be_writable && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + constituent_map.0.insert(constituent_index, account_loader); + } + + Ok(constituent_map) + } + + pub fn empty() -> Self { + ConstituentMap(BTreeMap::new()) + } +} + +pub(crate) type ConstituentSet = BTreeSet; diff --git a/programs/drift/src/state/insurance_fund_stake.rs b/programs/drift/src/state/insurance_fund_stake.rs index bd92019969..25d81f4b0d 100644 --- a/programs/drift/src/state/insurance_fund_stake.rs +++ b/programs/drift/src/state/insurance_fund_stake.rs @@ -1,12 +1,13 @@ use crate::error::DriftResult; use crate::error::ErrorCode; +use crate::math::constants::EPOCH_DURATION; use crate::math::safe_math::SafeMath; +use crate::math_error; use crate::safe_decrement; use crate::safe_increment; use crate::state::spot_market::SpotMarket; use crate::state::traits::Size; use crate::validate; -use crate::{math_error, EPOCH_DURATION}; use anchor_lang::prelude::*; #[cfg(test)] diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs new file mode 100644 index 0000000000..106945fa5a --- /dev/null +++ b/programs/drift/src/state/lp_pool.rs @@ -0,0 +1,1891 @@ +use std::collections::BTreeMap; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::constants::{ + BASE_PRECISION_I128, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, + PERCENTAGE_PRECISION_U64, PRICE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, +}; +use crate::math::oracle::{is_oracle_valid_for_action, DriftAction}; +use crate::math::safe_math::SafeMath; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::math::spot_balance::{get_signed_token_amount, get_token_amount}; +use crate::state::amm_cache::{AmmCacheFixed, CacheInfo}; +use crate::state::constituent_map::ConstituentMap; +use crate::state::oracle_map::OracleMap; +use crate::state::paused_operations::ConstituentLpOperation; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::user::MarketType; +use anchor_lang::prelude::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use enumflags2::BitFlags; + +use super::oracle::OraclePriceData; +use super::spot_market::SpotMarket; +use super::zero_copy::{AccountZeroCopy, AccountZeroCopyMut, HasLen}; +use crate::state::spot_market::{SpotBalance, SpotBalanceType}; +use crate::state::traits::Size; +use crate::{impl_zero_copy_loader, validate}; + +pub const LP_POOL_PDA_SEED: &str = "lp_pool"; +pub const AMM_MAP_PDA_SEED: &str = "AMM_MAP"; +pub const CONSTITUENT_PDA_SEED: &str = "CONSTITUENT"; +pub const CONSTITUENT_TARGET_BASE_PDA_SEED: &str = "constituent_target_base_seed"; +pub const CONSTITUENT_CORRELATIONS_PDA_SEED: &str = "constituent_correlations"; +pub const CONSTITUENT_VAULT_PDA_SEED: &str = "CONSTITUENT_VAULT"; +pub const LP_POOL_TOKEN_VAULT_PDA_SEED: &str = "LP_POOL_TOKEN_VAULT"; + +pub const BASE_SWAP_FEE: i128 = 300; // 0.3% in PERCENTAGE_PRECISION +pub const MAX_SWAP_FEE: i128 = 37_500; // 37.5% in PERCENTAGE_PRECISION + +pub const MIN_AUM_EXECUTION_FEE: u128 = 10_000_000_000_000; + +// Delay constants +#[cfg(feature = "anchor-test")] +pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 100; +#[cfg(not(feature = "anchor-test"))] +pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 10; +pub const LP_POOL_SWAP_AUM_UPDATE_DELAY: u64 = 0; +#[cfg(feature = "anchor-test")] +pub const MAX_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; +#[cfg(not(feature = "anchor-test"))] +pub const MAX_STALENESS_FOR_TARGET_CALC: u64 = 0u64; + +#[cfg(feature = "anchor-test")] +pub const MAX_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; +#[cfg(not(feature = "anchor-test"))] +pub const MAX_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10u64; + +#[cfg(test)] +mod tests; + +#[account(zero_copy(unsafe))] +#[derive(Debug)] +#[repr(C)] +pub struct LPPool { + /// address of the vault. + pub pubkey: Pubkey, + // vault token mint + pub mint: Pubkey, // 32, 96 + // whitelist mint + pub whitelist_mint: Pubkey, + // constituent target base pubkey + pub constituent_target_base: Pubkey, + // constituent correlations pubkey + pub constituent_correlations: Pubkey, + + /// The current number of VaultConstituents in the vault, each constituent is pda(LPPool.address, constituent_index) + /// which constituent is the quote, receives revenue pool distributions. (maybe this should just be implied idx 0) + /// pub quote_constituent_index: u16, + + /// QUOTE_PRECISION: Max AUM, Prohibit minting new DLP beyond this + pub max_aum: u128, + + /// QUOTE_PRECISION: AUM of the vault in USD, updated lazily + pub last_aum: u128, + + /// QUOTE PRECISION: Cumulative quotes from settles + pub cumulative_quote_sent_to_perp_markets: u128, + pub cumulative_quote_received_from_perp_markets: u128, + + /// QUOTE_PRECISION: Total fees paid for minting and redeeming LP tokens + pub total_mint_redeem_fees_paid: i128, + + /// timestamp of last AUM slot + pub last_aum_slot: u64, + + pub max_settle_quote_amount: u64, + + /// timestamp of last vAMM revenue rebalance + pub _padding: u64, + + /// Every mint/redeem has a monotonically increasing id. This is the next id to use + pub mint_redeem_id: u64, + pub settle_id: u64, + + /// PERCENTAGE_PRECISION + pub min_mint_fee: i64, + + pub token_supply: u64, + + // PERCENTAGE_PRECISION: percentage precision const = 100% + pub volatility: u64, + + pub constituents: u16, + pub quote_consituent_index: u16, + + pub bump: u8, + + // No precision - just constant + pub gamma_execution: u8, + // No precision - just constant + pub xi: u8, + + // Bps of fees for target delays + pub target_oracle_delay_fee_bps_per_10_slots: u8, + pub target_position_delay_fee_bps_per_10_slots: u8, + + pub lp_pool_id: u8, + + pub padding: [u8; 174], +} + +impl Default for LPPool { + fn default() -> Self { + Self { + pubkey: Pubkey::default(), + mint: Pubkey::default(), + whitelist_mint: Pubkey::default(), + constituent_target_base: Pubkey::default(), + constituent_correlations: Pubkey::default(), + max_aum: 0, + last_aum: 0, + cumulative_quote_sent_to_perp_markets: 0, + cumulative_quote_received_from_perp_markets: 0, + total_mint_redeem_fees_paid: 0, + last_aum_slot: 0, + max_settle_quote_amount: 0, + _padding: 0, + mint_redeem_id: 0, + settle_id: 0, + min_mint_fee: 0, + token_supply: 0, + volatility: 0, + constituents: 0, + quote_consituent_index: 0, + bump: 0, + gamma_execution: 0, + xi: 0, + target_oracle_delay_fee_bps_per_10_slots: 0, + target_position_delay_fee_bps_per_10_slots: 0, + lp_pool_id: 0, + padding: [0u8; 174], + } + } +} + +impl Size for LPPool { + const SIZE: usize = 504; +} + +impl LPPool { + pub fn sync_token_supply(&mut self, supply: u64) { + self.token_supply = supply; + } + + pub fn get_price(&self, mint_supply: u64) -> Result { + match mint_supply { + 0 => Ok(0), + supply => { + // TODO: assuming mint decimals = quote decimals = 6 + Ok(self + .last_aum + .safe_mul(PRICE_PRECISION)? + .safe_div(supply as u128)?) + } + } + } + + /// Get the swap price between two (non-LP token) constituents. + /// Accounts for precision differences between in and out constituents + /// returns swap price in PRICE_PRECISION + pub fn get_swap_price( + &self, + in_decimals: u32, + out_decimals: u32, + in_oracle: &OraclePriceData, + out_oracle: &OraclePriceData, + ) -> DriftResult<(u128, u128)> { + let in_price = in_oracle.price.cast::()?; + let out_price = out_oracle.price.cast::()?; + + let (prec_diff_numerator, prec_diff_denominator) = if out_decimals > in_decimals { + (10_u128.pow(out_decimals - in_decimals), 1) + } else { + (1, 10_u128.pow(in_decimals - out_decimals)) + }; + + let swap_price_num = in_price.safe_mul(prec_diff_numerator)?; + let swap_price_denom = out_price.safe_mul(prec_diff_denominator)?; + + Ok((swap_price_num, swap_price_denom)) + } + + /// in the respective token units. Amounts are gross fees and in + /// token mint precision. + /// Positive fees are paid, negative fees are rebated + /// Returns (in_amount out_amount, in_fee, out_fee) + pub fn get_swap_amount( + &self, + in_target_position_slot_delay: u64, + out_target_position_slot_delay: u64, + in_target_oracle_slot_delay: u64, + out_target_oracle_slot_delay: u64, + in_oracle: &OraclePriceData, + out_oracle: &OraclePriceData, + in_constituent: &Constituent, + out_constituent: &Constituent, + in_spot_market: &SpotMarket, + out_spot_market: &SpotMarket, + in_target_weight: i64, + out_target_weight: i64, + in_amount: u128, + correlation: i64, + ) -> DriftResult<(u128, u128, i128, i128)> { + let (swap_price_num, swap_price_denom) = self.get_swap_price( + in_spot_market.decimals, + out_spot_market.decimals, + in_oracle, + out_oracle, + )?; + + let (mut in_fee, mut out_fee) = self.get_swap_fees( + in_spot_market, + in_oracle.price, + in_constituent, + in_amount.cast::()?, + in_target_weight, + Some(out_spot_market), + Some(out_oracle.price), + Some(out_constituent), + Some(out_target_weight), + correlation, + )?; + + in_fee = in_fee.safe_add(self.get_target_uncertainty_fees( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + )?)?; + out_fee = out_fee.safe_add(self.get_target_uncertainty_fees( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + )?)?; + + in_fee = in_fee.min(MAX_SWAP_FEE); + out_fee = out_fee.min(MAX_SWAP_FEE); + + let in_fee_amount = in_amount + .cast::()? + .safe_mul(in_fee)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let out_amount = in_amount + .cast::()? + .safe_sub(in_fee_amount)? + .cast::()? + .safe_mul(swap_price_num)? + .safe_div(swap_price_denom)?; + + let out_fee_amount = out_amount + .cast::()? + .safe_mul(out_fee)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + Ok((in_amount, out_amount, in_fee_amount, out_fee_amount)) + } + + /// Calculates the amount of LP tokens to mint for a given input of constituent tokens. + /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision + pub fn get_add_liquidity_mint_amount( + &self, + in_target_position_slot_delay: u64, + in_target_oracle_slot_delay: u64, + in_spot_market: &SpotMarket, + in_constituent: &Constituent, + in_amount: u128, + in_oracle: &OraclePriceData, + in_target_weight: i64, + dlp_total_supply: u64, + ) -> DriftResult<(u64, u128, i64, i128)> { + let (mut in_fee_pct, out_fee_pct) = if self.last_aum == 0 { + (0, 0) + } else { + self.get_swap_fees( + in_spot_market, + in_oracle.price, + in_constituent, + in_amount.cast::()?, + in_target_weight, + None, + None, + None, + None, + 0, + )? + }; + in_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + in_fee_pct += self.get_target_uncertainty_fees( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + )?; + in_fee_pct = in_fee_pct.min(MAX_SWAP_FEE * 2); + + let in_fee_amount = in_amount + .cast::()? + .safe_mul(in_fee_pct)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let in_amount_less_fees = in_amount + .cast::()? + .safe_sub(in_fee_amount)? + .max(0) + .cast::()?; + + let token_precision_denominator = 10_u128.pow(in_spot_market.decimals); + let token_amount_usd = in_oracle + .price + .cast::()? + .safe_mul(in_amount_less_fees)?; + let lp_amount = if dlp_total_supply == 0 { + token_amount_usd.safe_div(token_precision_denominator)? + } else { + token_amount_usd + .safe_mul(dlp_total_supply as u128)? + .safe_div(self.last_aum)? + .safe_div(token_precision_denominator)? + }; + + let lp_fee_to_charge_pct = self.min_mint_fee; + // let lp_fee_to_charge_pct = self.get_mint_redeem_fee(now, true)?; + let lp_fee_to_charge = lp_amount + .safe_mul(lp_fee_to_charge_pct as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .cast::()?; + + Ok(( + lp_amount.cast::()?, + in_amount, + lp_fee_to_charge, + in_fee_amount, + )) + } + + /// Calculates the amount of constituent tokens to receive for a given amount of LP tokens to burn + /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision + pub fn get_remove_liquidity_amount( + &self, + out_target_position_slot_delay: u64, + out_target_oracle_slot_delay: u64, + out_spot_market: &SpotMarket, + out_constituent: &Constituent, + lp_to_burn: u64, + out_oracle: &OraclePriceData, + out_target_weight: i64, + dlp_total_supply: u64, + ) -> DriftResult<(u64, u128, i64, i128)> { + let lp_fee_to_charge_pct = self.min_mint_fee; + let mut lp_burn_amount = lp_to_burn; + if dlp_total_supply.saturating_sub(lp_burn_amount) <= QUOTE_PRECISION_U64 { + lp_burn_amount = dlp_total_supply.saturating_sub(QUOTE_PRECISION_U64); + } + + let lp_fee_to_charge = lp_burn_amount + .cast::()? + .safe_mul(lp_fee_to_charge_pct.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .cast::()?; + + let lp_amount_less_fees = (lp_burn_amount as i128).safe_sub(lp_fee_to_charge as i128)?; + + let token_precision_denominator = 10_u128.pow(out_spot_market.decimals); + + // Calculate proportion of LP tokens being burned + let proportion = lp_amount_less_fees + .cast::()? + .safe_mul(10u128.pow(3))? + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(dlp_total_supply as u128)?; + + // Apply proportion to AUM and convert to token amount + let out_amount = self + .last_aum + .safe_mul(proportion)? + .safe_mul(token_precision_denominator)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(10u128.pow(3))? + .safe_div(out_oracle.price.cast::()?)?; + + let (in_fee_pct, mut out_fee_pct) = self.get_swap_fees( + out_spot_market, + out_oracle.price, + out_constituent, + out_amount.cast::()?.safe_mul(-1_i128)?, + out_target_weight, + None, + None, + None, + None, + 0, + )?; + + out_fee_pct += self.get_target_uncertainty_fees( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + )?; + out_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + out_fee_pct = out_fee_pct.min(MAX_SWAP_FEE * 2); + + let out_fee_amount = out_amount + .cast::()? + .safe_mul(out_fee_pct)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + Ok((lp_burn_amount, out_amount, lp_fee_to_charge, out_fee_amount)) + } + + pub fn get_quadratic_fee_inventory( + &self, + gamma_covar: [[i128; 2]; 2], + pre_notional_errors: [i128; 2], + post_notional_errors: [i128; 2], + trade_notional: u128, + ) -> DriftResult<(i128, i128)> { + let gamma_covar_error_pre_in = gamma_covar[0][0] + .safe_mul(pre_notional_errors[0])? + .safe_add(gamma_covar[0][1].safe_mul(pre_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let gamma_covar_error_pre_out = gamma_covar[1][0] + .safe_mul(pre_notional_errors[0])? + .safe_add(gamma_covar[1][1].safe_mul(pre_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let gamma_covar_error_post_in = gamma_covar[0][0] + .safe_mul(post_notional_errors[0])? + .safe_add(gamma_covar[0][1].safe_mul(post_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let gamma_covar_error_post_out = gamma_covar[1][0] + .safe_mul(post_notional_errors[0])? + .safe_add(gamma_covar[1][1].safe_mul(post_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let c_pre_in: i128 = gamma_covar_error_pre_in + .safe_mul(pre_notional_errors[0])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + let c_pre_out = gamma_covar_error_pre_out + .safe_mul(pre_notional_errors[1])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + + let c_post_in: i128 = gamma_covar_error_post_in + .safe_mul(post_notional_errors[0])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + let c_post_out = gamma_covar_error_post_out + .safe_mul(post_notional_errors[1])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + + let in_fee = c_post_in + .safe_sub(c_pre_in)? + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(trade_notional.cast::()?)? + .safe_mul(QUOTE_PRECISION_I128)? + .safe_div(self.last_aum.cast::()?)?; + let out_fee = c_post_out + .safe_sub(c_pre_out)? + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(trade_notional.cast::()?)? + .safe_mul(QUOTE_PRECISION_I128)? + .safe_div(self.last_aum.cast::()?)?; + + Ok((in_fee, out_fee)) + } + + pub fn get_linear_fee_execution( + &self, + trade_ratio: i128, + kappa_execution: u128, + xi: u8, + ) -> DriftResult { + trade_ratio + .safe_mul(kappa_execution.safe_mul(xi as u128)?.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128) + } + + pub fn get_quadratic_fee_execution( + &self, + trade_ratio: i128, + kappa_execution: u128, + xi: u8, + ) -> DriftResult { + kappa_execution + .cast::()? + .safe_mul(xi.safe_mul(xi)?.cast::()?)? + .safe_mul(trade_ratio.safe_mul(trade_ratio)?)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_div(PERCENTAGE_PRECISION_I128) + } + + /// returns fee in PERCENTAGE_PRECISION + pub fn get_swap_fees( + &self, + in_spot_market: &SpotMarket, + in_oracle_price: i64, + in_constituent: &Constituent, + in_amount: i128, + in_target_weight: i64, + out_spot_market: Option<&SpotMarket>, + out_oracle_price: Option, + out_constituent: Option<&Constituent>, + out_target_weight: Option, + correlation: i64, + ) -> DriftResult<(i128, i128)> { + let notional_trade_size = in_constituent.get_notional(in_oracle_price, in_amount)?; + let in_volatility = in_constituent.volatility; + + let ( + mint_redeem, + out_volatility, + out_gamma_execution, + out_gamma_inventory, + out_xi, + out_notional_target, + out_notional_pre, + out_notional_post, + ) = if let Some(out_constituent) = out_constituent { + let out_spot_market = out_spot_market.unwrap(); + let out_oracle_price = out_oracle_price.unwrap(); + let out_amount = notional_trade_size + .safe_mul(10_i128.pow(out_spot_market.decimals))? + .safe_div(out_oracle_price.cast::()?)?; + ( + false, + out_constituent.volatility, + out_constituent.gamma_execution, + out_constituent.gamma_inventory, + out_constituent.xi, + out_target_weight + .unwrap() + .cast::()? + .safe_mul(self.last_aum.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?, + out_constituent.get_notional_with_delta(out_oracle_price, out_spot_market, 0)?, + out_constituent.get_notional_with_delta( + out_oracle_price, + out_spot_market, + out_amount.safe_mul(-1)?, + )?, + ) + } else { + ( + true, + self.volatility, + self.gamma_execution, + 0, + self.xi, + 0, + 0, + 0, + ) + }; + + let in_kappa_execution: u128 = (in_volatility as u128) + .safe_mul(in_volatility as u128)? + .safe_mul(in_constituent.gamma_execution as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(2u128)?; + + let out_kappa_execution: u128 = (out_volatility as u128) + .safe_mul(out_volatility as u128)? + .safe_mul(out_gamma_execution as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(2u128)?; + + // Compute notional targets and errors + let in_notional_target = in_target_weight + .cast::()? + .safe_mul(self.last_aum.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let in_notional_pre = + in_constituent.get_notional_with_delta(in_oracle_price, in_spot_market, 0)?; + let in_notional_post = + in_constituent.get_notional_with_delta(in_oracle_price, in_spot_market, in_amount)?; + let in_notional_error_pre = in_notional_pre.safe_sub(in_notional_target)?; + + // keep aum fixed if it's a swap for calculating post error, othwerise + // increase aum first + let in_notional_error_post = if !mint_redeem { + in_notional_post.safe_sub(in_notional_target)? + } else { + let adjusted_aum = self + .last_aum + .cast::()? + .safe_add(notional_trade_size)?; + let in_notional_target_post_mint_redeem = in_target_weight + .cast::()? + .safe_mul(adjusted_aum)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + in_notional_post.safe_sub(in_notional_target_post_mint_redeem)? + }; + + let out_notional_error_pre = out_notional_pre.safe_sub(out_notional_target)?; + let out_notional_error_post = out_notional_post.safe_sub(out_notional_target)?; + + let trade_ratio: i128 = notional_trade_size + .abs() + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(self.last_aum.max(MIN_AUM_EXECUTION_FEE).cast::()?)?; + + // Linear fee computation amount + let in_fee_execution_linear = + self.get_linear_fee_execution(trade_ratio, in_kappa_execution, in_constituent.xi)?; + + let out_fee_execution_linear = + self.get_linear_fee_execution(trade_ratio, out_kappa_execution, out_xi)?; + + // Quadratic fee components + let in_fee_execution_quadratic = + self.get_quadratic_fee_execution(trade_ratio, in_kappa_execution, in_constituent.xi)?; + let out_fee_execution_quadratic = + self.get_quadratic_fee_execution(trade_ratio, out_kappa_execution, out_xi)?; + let (in_quadratic_inventory_fee, out_quadratic_inventory_fee) = self + .get_quadratic_fee_inventory( + get_gamma_covar_matrix( + correlation, + in_constituent.gamma_inventory, + out_gamma_inventory, + in_constituent.volatility, + out_volatility, + )?, + [in_notional_error_pre, out_notional_error_pre], + [in_notional_error_post, out_notional_error_post], + notional_trade_size.abs().cast::()?, + )?; + + msg!( + "fee breakdown - in_exec_linear: {}, in_exec_quad: {}, in_inv_quad: {}, out_exec_linear: {}, out_exec_quad: {}, out_inv_quad: {}", + in_fee_execution_linear, + in_fee_execution_quadratic, + in_quadratic_inventory_fee, + out_fee_execution_linear, + out_fee_execution_quadratic, + out_quadratic_inventory_fee + ); + let total_in_fee = in_fee_execution_linear + .safe_add(in_fee_execution_quadratic)? + .safe_add(in_quadratic_inventory_fee)? + .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; + let total_out_fee = out_fee_execution_linear + .safe_add(out_fee_execution_quadratic)? + .safe_add(out_quadratic_inventory_fee)? + .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; + + Ok((total_in_fee, total_out_fee)) + } + + pub fn get_target_uncertainty_fees( + self, + target_position_slot_delay: u64, + target_oracle_slot_delay: u64, + ) -> DriftResult { + // Gives an uncertainty fee in bps if the oracle or position was stale when calcing target. + // Uses a step function that goes up every 10 slots beyond a threshold where we consider it okay + // - delay 0 (<= threshold) = 0 bps + // - delay 1..10 = 10 bps (1 block) + // - delay 11..20 = 20 bps (2 blocks) + fn step_fee(delay: u64, threshold: u64, per_10_slot_bps: u8) -> DriftResult { + if delay <= threshold || per_10_slot_bps == 0 { + return Ok(0); + } + let elapsed = delay.saturating_sub(threshold); + let blocks = elapsed.safe_add(9)?.safe_div(10)?; + let fee_bps = (blocks as u128).safe_mul(per_10_slot_bps as u128)?; + let fee = fee_bps + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(10_000u128)?; + Ok(fee) + } + + let oracle_uncertainty_fee = step_fee( + target_oracle_slot_delay, + MAX_ORACLE_STALENESS_FOR_TARGET_CALC, + self.target_oracle_delay_fee_bps_per_10_slots, + )?; + let position_uncertainty_fee = step_fee( + target_position_slot_delay, + MAX_STALENESS_FOR_TARGET_CALC, + self.target_position_delay_fee_bps_per_10_slots, + )?; + + Ok(oracle_uncertainty_fee + .safe_add(position_uncertainty_fee)? + .cast::()?) + } + + pub fn record_mint_redeem_fees(&mut self, amount: i64) -> DriftResult { + self.total_mint_redeem_fees_paid = self + .total_mint_redeem_fees_paid + .safe_add(amount.cast::()?)?; + Ok(()) + } + + pub fn update_aum( + &mut self, + slot: u64, + constituent_map: &ConstituentMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + constituent_target_base: &AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, + amm_cache: &AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed>, + ) -> DriftResult<(u128, i128, BTreeMap>)> { + let mut aum: i128 = 0; + let mut crypto_delta = 0_i128; + let mut derivative_groups: BTreeMap> = BTreeMap::new(); + for i in 0..self.constituents as usize { + let constituent = constituent_map.get_ref(&(i as u16))?; + if slot.saturating_sub(constituent.last_oracle_slot) + > constituent.oracle_staleness_threshold + { + msg!( + "Constituent {} oracle slot is too stale: {}, current slot: {}", + constituent.constituent_index, + constituent.last_oracle_slot, + slot + ); + return Err(ErrorCode::ConstituentOracleStale.into()); + } + + if constituent.constituent_derivative_index >= 0 && constituent.derivative_weight != 0 { + if !derivative_groups + .contains_key(&(constituent.constituent_derivative_index as u16)) + { + derivative_groups.insert( + constituent.constituent_derivative_index as u16, + vec![constituent.constituent_index], + ); + } else { + derivative_groups + .get_mut(&(constituent.constituent_derivative_index as u16)) + .unwrap() + .push(constituent.constituent_index); + } + } + + let spot_market = spot_market_map.get_ref(&constituent.spot_market_index)?; + let oracle_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + constituent.spot_market_index, + &spot_market.oracle_id(), + spot_market.historical_oracle_data.last_oracle_price_twap, + spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + if !is_oracle_valid_for_action( + oracle_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Constituent {} oracle is not valid for action", + constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + let constituent_aum = constituent + .get_full_token_amount(&spot_market)? + .safe_mul(oracle_and_validity.0.price as i128)? + .safe_div(10_i128.pow(spot_market.decimals))?; + msg!( + "constituent: {}, balance: {}, aum: {}, deriv index: {}, bl token balance {}, bl balance type {}, vault balance: {}", + constituent.constituent_index, + constituent.get_full_token_amount(&spot_market)?, + constituent_aum, + constituent.constituent_derivative_index, + constituent.spot_balance.get_token_amount(&spot_market)?, + constituent.spot_balance.balance_type, + constituent.vault_token_balance + ); + + // sum up crypto deltas (notional exposures for all non-stablecoins) + if constituent.constituent_index != self.quote_consituent_index + && constituent.constituent_derivative_index != self.quote_consituent_index as i16 + { + let constituent_target_notional = constituent_target_base + .get(constituent.constituent_index as u32) + .target_base + .cast::()? + .safe_mul(oracle_and_validity.0.price.cast::()?)? + .safe_div(10_i128.pow(constituent.decimals as u32))? + .cast::()?; + crypto_delta = crypto_delta.safe_add(constituent_target_notional.cast()?)?; + } + aum = aum.saturating_add(constituent_aum); + } + + msg!("Aum before quote owed from lp pool: {}", aum); + + let mut total_quote_owed: i128 = 0; + for cache_datum in amm_cache.iter() { + total_quote_owed = + total_quote_owed.safe_add(cache_datum.quote_owed_from_lp_pool as i128)?; + } + + if total_quote_owed > 0 { + aum = aum + .saturating_sub(total_quote_owed) + .max(QUOTE_PRECISION_I128); + } else if total_quote_owed < 0 { + aum = aum.saturating_add(-total_quote_owed); + } + + let aum_u128 = aum.max(0).cast::()?; + self.last_aum = aum_u128; + self.last_aum_slot = slot; + + Ok((aum_u128, crypto_delta, derivative_groups)) + } + + pub fn get_lp_pool_signer_seeds<'a>(lp_pool_id: &'a u8, bump: &'a u8) -> [&'a [u8]; 3] { + [ + LP_POOL_PDA_SEED.as_ref(), + bytemuck::bytes_of(lp_pool_id), + bytemuck::bytes_of(bump), + ] + } +} + +#[zero_copy(unsafe)] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct ConstituentSpotBalance { + /// The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow + /// interest of corresponding market. + /// precision: token precision + pub scaled_balance: u128, + /// The cumulative deposits/borrows a user has made into a market + /// precision: token mint precision + pub cumulative_deposits: i64, + /// The market index of the corresponding spot market + pub market_index: u16, + /// Whether the position is deposit or borrow + pub balance_type: SpotBalanceType, + pub padding: [u8; 5], +} + +impl SpotBalance for ConstituentSpotBalance { + fn market_index(&self) -> u16 { + self.market_index + } + + fn balance_type(&self) -> &SpotBalanceType { + &self.balance_type + } + + fn balance(&self) -> u128 { + self.scaled_balance + } + + fn increase_balance(&mut self, delta: u128) -> DriftResult { + self.scaled_balance = self.scaled_balance.safe_add(delta)?; + Ok(()) + } + + fn decrease_balance(&mut self, delta: u128) -> DriftResult { + self.scaled_balance = self.scaled_balance.safe_sub(delta)?; + Ok(()) + } + + fn update_balance_type(&mut self, balance_type: SpotBalanceType) -> DriftResult { + self.balance_type = balance_type; + Ok(()) + } +} + +impl ConstituentSpotBalance { + pub fn get_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + get_token_amount(self.scaled_balance, spot_market, &self.balance_type) + } + + pub fn get_signed_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + let token_amount = self.get_token_amount(spot_market)?; + get_signed_token_amount(token_amount, &self.balance_type) + } +} + +#[account(zero_copy(unsafe))] +#[derive(Debug)] +#[repr(C)] +pub struct Constituent { + /// address of the constituent + pub pubkey: Pubkey, + pub mint: Pubkey, + pub lp_pool: Pubkey, + pub vault: Pubkey, + + /// total fees received by the constituent. Positive = fees received, Negative = fees paid + pub total_swap_fees: i128, + + /// spot borrow-lend balance for constituent + pub spot_balance: ConstituentSpotBalance, // should be in constituent base asset + + pub last_spot_balance_token_amount: i64, // token precision + pub cumulative_spot_interest_accrued_token_amount: i64, // token precision + + /// max deviation from target_weight allowed for the constituent + /// precision: PERCENTAGE_PRECISION + pub max_weight_deviation: i64, + /// min fee charged on swaps to/from this constituent + /// precision: PERCENTAGE_PRECISION + pub swap_fee_min: i64, + /// max fee charged on swaps to/from this constituent + /// precision: PERCENTAGE_PRECISION + pub swap_fee_max: i64, + + /// Max Borrow amount: + /// precision: token precision + pub max_borrow_token_amount: u64, + + /// ata token balance in token precision + pub vault_token_balance: u64, + + pub last_oracle_price: i64, + pub last_oracle_slot: u64, + + /// Delay allowed for valid AUM calculation + pub oracle_staleness_threshold: u64, + + pub flash_loan_initial_token_amount: u64, + /// Every swap to/from this constituent has a monotonically increasing id. This is the next id to use + pub next_swap_id: u64, + + /// percentable of derivatve weight to go to this specific derivative PERCENTAGE_PRECISION. Zero if no derivative weight + pub derivative_weight: u64, + + pub volatility: u64, // volatility in PERCENTAGE_PRECISION 1=1% + + // depeg threshold in relation top parent in PERCENTAGE_PRECISION + pub constituent_derivative_depeg_threshold: u64, + + /// The `constituent_index` of the parent constituent. -1 if it is a parent index + /// Example: if in a pool with SOL (parent) and dSOL (derivative), + /// SOL.constituent_index = 1, SOL.constituent_derivative_index = -1, + /// dSOL.constituent_index = 2, dSOL.constituent_derivative_index = 1 + pub constituent_derivative_index: i16, + + pub spot_market_index: u16, + pub constituent_index: u16, + + pub decimals: u8, + pub bump: u8, + pub vault_bump: u8, + + // Fee params + pub gamma_inventory: u8, + pub gamma_execution: u8, + pub xi: u8, + + // Status + pub status: u8, + pub paused_operations: u8, + pub _padding: [u8; 162], +} + +impl Default for Constituent { + fn default() -> Self { + Self { + pubkey: Pubkey::default(), + mint: Pubkey::default(), + lp_pool: Pubkey::default(), + vault: Pubkey::default(), + total_swap_fees: 0, + spot_balance: ConstituentSpotBalance::default(), + last_spot_balance_token_amount: 0, + cumulative_spot_interest_accrued_token_amount: 0, + max_weight_deviation: 0, + swap_fee_min: 0, + swap_fee_max: 0, + max_borrow_token_amount: 0, + vault_token_balance: 0, + last_oracle_price: 0, + last_oracle_slot: 0, + oracle_staleness_threshold: 0, + flash_loan_initial_token_amount: 0, + next_swap_id: 0, + derivative_weight: 0, + volatility: 0, + constituent_derivative_depeg_threshold: 0, + constituent_derivative_index: -1, + spot_market_index: 0, + constituent_index: 0, + decimals: 0, + bump: 0, + vault_bump: 0, + gamma_inventory: 0, + gamma_execution: 0, + xi: 0, + status: 0, + paused_operations: 0, + _padding: [0; 162], + } + } +} + +impl Size for Constituent { + const SIZE: usize = 480; +} + +#[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)] +pub enum ConstituentStatus { + /// fills only able to reduce liability + ReduceOnly = 0b00000001, + /// market has no remaining participants + Decommissioned = 0b00000010, +} + +impl Constituent { + pub fn get_status(&self) -> DriftResult> { + BitFlags::::from_bits(usize::from(self.status)).safe_unwrap() + } + + pub fn is_decommissioned(&self) -> DriftResult { + Ok(self + .get_status()? + .contains(ConstituentStatus::Decommissioned)) + } + + pub fn is_reduce_only(&self) -> DriftResult { + Ok(self.get_status()?.contains(ConstituentStatus::ReduceOnly)) + } + + pub fn does_constituent_allow_operation( + &self, + operation: ConstituentLpOperation, + ) -> DriftResult<()> { + if self.is_decommissioned()? { + msg!( + "Constituent {:?}, spot market {}, is decommissioned", + self.pubkey, + self.spot_market_index + ); + Err(ErrorCode::InvalidConstituentOperation) + } else if ConstituentLpOperation::is_operation_paused(self.paused_operations, operation) { + msg!( + "Constituent {:?}, spot market {}, is paused for operation {:?}", + self.pubkey, + self.spot_market_index, + operation + ); + Err(ErrorCode::InvalidConstituentOperation) + } else { + Ok(()) + } + } + + pub fn is_operation_reducing( + &self, + spot_market: &SpotMarket, + is_increasing: bool, + ) -> DriftResult { + let current_balance_sign = self.get_full_token_amount(spot_market)?.signum(); + if current_balance_sign > 0 { + Ok(!is_increasing) + } else { + Ok(is_increasing) + } + } + + /// Returns the full balance of the Constituent, the total of the amount in Constituent's token + /// account and in Drift Borrow-Lend. + pub fn get_full_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + let token_amount = self.spot_balance.get_signed_token_amount(spot_market)?; + let vault_balance = self.vault_token_balance.cast::()?; + token_amount.safe_add(vault_balance) + } + + pub fn record_swap_fees(&mut self, amount: i128) -> DriftResult { + self.total_swap_fees = self.total_swap_fees.safe_add(amount)?; + Ok(()) + } + + /// Current weight of this constituent = price * token_balance / lp_pool_aum + /// Note: lp_pool_aum is from LPPool.last_aum, which is a lagged value updated via crank + pub fn get_weight( + &self, + price: i64, + spot_market: &SpotMarket, + token_amount_delta: i128, + lp_pool_aum: u128, + ) -> DriftResult { + if lp_pool_aum == 0 { + return Ok(0); + } + let value_usd = self.get_notional_with_delta(price, spot_market, token_amount_delta)?; + + value_usd + .safe_mul(PERCENTAGE_PRECISION_I64.cast::()?)? + .safe_div(lp_pool_aum.cast::()?)? + .cast::() + } + + pub fn get_notional(&self, price: i64, token_amount: i128) -> DriftResult { + let token_precision = 10_i128.pow(self.decimals as u32); + let value_usd = token_amount.safe_mul(price.cast::()?)?; + value_usd.safe_div(token_precision) + } + + pub fn get_notional_with_delta( + &self, + price: i64, + spot_market: &SpotMarket, + token_amount: i128, + ) -> DriftResult { + let token_precision = 10_i128.pow(self.decimals as u32); + let balance = self.get_full_token_amount(spot_market)?.cast::()?; + let amount = balance.safe_add(token_amount)?; + let value_usd = amount.safe_mul(price.cast::()?)?; + value_usd.safe_div(token_precision) + } + + pub fn sync_token_balance(&mut self, token_account_amount: u64) { + self.vault_token_balance = token_account_amount; + } + + pub fn get_vault_signer_seeds<'a>( + lp_pool: &'a Pubkey, + spot_market_index: &'a u16, + bump: &'a u8, + ) -> [&'a [u8]; 4] { + [ + CONSTITUENT_VAULT_PDA_SEED.as_ref(), + lp_pool.as_ref(), + bytemuck::bytes_of(spot_market_index), + bytemuck::bytes_of(bump), + ] + } +} + +#[zero_copy] +#[derive(Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct AmmConstituentDatum { + pub perp_market_index: u16, + pub constituent_index: u16, + pub _padding: [u8; 4], + pub last_slot: u64, + /// PERCENTAGE_PRECISION. The weight this constituent has on the perp market + pub weight: i64, +} + +impl Default for AmmConstituentDatum { + fn default() -> Self { + AmmConstituentDatum { + perp_market_index: u16::MAX, + constituent_index: u16::MAX, + _padding: [0; 4], + last_slot: 0, + weight: 0, + } + } +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct AmmConstituentMappingFixed { + pub lp_pool: Pubkey, + pub bump: u8, + pub _pad: [u8; 3], + pub len: u32, +} + +impl HasLen for AmmConstituentMappingFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct AmmConstituentMapping { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. Each datum represents the target weight for a single (AMM, Constituent) pair. + // An AMM may be partially backed by multiple Constituents + pub weights: Vec, +} + +impl AmmConstituentMapping { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * 24 + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.weights.len() <= 128, + ErrorCode::DefaultError, + "Number of constituents len must be between 1 and 128" + )?; + Ok(()) + } + + pub fn sort(&mut self) { + self.weights.sort_by_key(|datum| datum.constituent_index); + } +} + +impl_zero_copy_loader!( + AmmConstituentMapping, + crate::id, + AmmConstituentMappingFixed, + AmmConstituentDatum +); + +#[zero_copy] +#[derive(Debug, Default, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct TargetsDatum { + pub cost_to_trade_bps: i32, + pub _padding: [u8; 4], + pub target_base: i64, + pub last_oracle_slot: u64, + pub last_position_slot: u64, +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct ConstituentTargetBaseFixed { + pub lp_pool: Pubkey, + pub bump: u8, + _pad: [u8; 3], + /// total elements in the flattened `data` vec + pub len: u32, +} + +impl HasLen for ConstituentTargetBaseFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct ConstituentTargetBase { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. The weights of the target weight matrix. Updated async + pub targets: Vec, +} + +impl ConstituentTargetBase { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * 32 + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.targets.len() <= 128, + ErrorCode::DefaultError, + "Number of constituents len must be between 1 and 128" + )?; + + validate!( + !self.targets.iter().any(|t| t.cost_to_trade_bps == 0), + ErrorCode::DefaultError, + "cost_to_trade_bps must be non-zero" + )?; + + Ok(()) + } +} + +impl_zero_copy_loader!( + ConstituentTargetBase, + crate::id, + ConstituentTargetBaseFixed, + TargetsDatum +); + +impl Default for ConstituentTargetBase { + fn default() -> Self { + ConstituentTargetBase { + lp_pool: Pubkey::default(), + bump: 0, + _padding: [0; 3], + targets: Vec::with_capacity(0), + } + } +} + +impl<'a> AccountZeroCopy<'a, TargetsDatum, ConstituentTargetBaseFixed> { + pub fn get_target_weight( + &self, + constituent_index: u16, + spot_market: &SpotMarket, + price: i64, + aum: u128, + ) -> DriftResult { + validate!( + constituent_index < self.len() as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index = {}, ConstituentTargetBase len = {}", + constituent_index, + self.len() + )?; + + // TODO: validate spot market + let datum = self.get(constituent_index as u32); + let target_weight = calculate_target_weight(datum.target_base, spot_market, price, aum)?; + Ok(target_weight) + } +} + +pub fn calculate_target_weight( + target_base: i64, + spot_market: &SpotMarket, + price: i64, + lp_pool_aum: u128, +) -> DriftResult { + if lp_pool_aum == 0 { + return Ok(0); + } + let notional: i128 = (target_base as i128) + .safe_mul(price as i128)? + .safe_div(10_i128.pow(spot_market.decimals))?; + + let target_weight = notional + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(lp_pool_aum.cast::()?)? + .cast::()? + .clamp(-PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_I64); + + Ok(target_weight) +} + +/// Update target base based on amm_inventory and mapping +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AmmInventoryAndPricesAndSlots { + pub inventory: i64, + pub price: i64, + pub last_oracle_slot: u64, + pub last_position_slot: u64, +} + +pub struct ConstituentIndexAndDecimalAndPrice { + pub constituent_index: u16, + pub decimals: u8, + pub price: i64, +} + +impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { + pub fn update_target_base( + &mut self, + mapping: &AccountZeroCopy<'a, AmmConstituentDatum, AmmConstituentMappingFixed>, + amm_inventory_and_prices: &[AmmInventoryAndPricesAndSlots], + constituents_indexes_and_decimals_and_prices: &mut [ConstituentIndexAndDecimalAndPrice], + slot: u64, + ) -> DriftResult<()> { + // Sorts by constituent index + constituents_indexes_and_decimals_and_prices.sort_by_key(|c| c.constituent_index); + + // Precompute notional by perp market index + let mut notionals_and_slots: Vec<(i128, u64, u64)> = + Vec::with_capacity(amm_inventory_and_prices.len()); + for &AmmInventoryAndPricesAndSlots { + inventory, + price, + last_oracle_slot, + last_position_slot, + } in amm_inventory_and_prices.iter() + { + let notional = (inventory as i128) + .safe_mul(price as i128)? + .safe_div(BASE_PRECISION_I128)?; + notionals_and_slots.push((notional, last_oracle_slot, last_position_slot)); + } + + let mut mapping_index = 0; + for ( + i, + &ConstituentIndexAndDecimalAndPrice { + constituent_index, + decimals, + price, + }, + ) in constituents_indexes_and_decimals_and_prices + .iter() + .enumerate() + { + let mut target_notional = 0i128; + + let mut j = mapping_index; + let mut oldest_oracle_slot = u64::MAX; + let mut oldest_position_slot = u64::MAX; + while j < mapping.len() { + let d = mapping.get(j); + if d.constituent_index != constituent_index { + while j < mapping.len() && mapping.get(j).constituent_index < constituent_index + { + j += 1; + } + break; + } + if let Some((perp_notional, perp_last_oracle_slot, perp_last_position_slot)) = + notionals_and_slots.get(d.perp_market_index as usize) + { + target_notional = target_notional + .saturating_add(perp_notional.saturating_mul(d.weight as i128)); + + oldest_oracle_slot = oldest_oracle_slot.min(*perp_last_oracle_slot); + oldest_position_slot = oldest_position_slot.min(*perp_last_position_slot); + } + j += 1; + } + mapping_index = j; + + let cell = self.get_mut(i as u32); + let target_base = -target_notional + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_mul(10_i128.pow(decimals as u32))? + .safe_div(price as i128)?; // Want to target opposite sign of total scaled notional inventory + + msg!( + "updating constituent index {} target base to {} from aggregated perp notional {}", + constituent_index, + target_base, + target_notional, + ); + cell.target_base = target_base.cast::()?; + + if slot.saturating_sub(oldest_position_slot) == MAX_STALENESS_FOR_TARGET_CALC { + cell.last_position_slot = slot; + } else { + msg!( + "not updating last_position_slot for target base constituent_index {}: oldest_position_slot {}, current slot {}", + constituent_index, + oldest_position_slot, + slot + ); + } + + if slot.saturating_sub(oldest_oracle_slot) <= MAX_ORACLE_STALENESS_FOR_TARGET_CALC { + cell.last_oracle_slot = slot; + } else { + msg!( + "not updating last_oracle_slot for target base constituent_index {}: oldest_oracle_slot {}, current slot {}", + constituent_index, + oldest_oracle_slot, + slot + ); + } + } + + Ok(()) + } +} + +impl<'a> AccountZeroCopyMut<'a, AmmConstituentDatum, AmmConstituentMappingFixed> { + #[cfg(test)] + pub fn add_amm_constituent_datum(&mut self, datum: AmmConstituentDatum) -> DriftResult<()> { + let len = self.len(); + + let mut open_slot_index: Option = None; + for i in 0..len { + let cell = self.get(i); + if cell.constituent_index == datum.constituent_index + && cell.perp_market_index == datum.perp_market_index + { + return Err(ErrorCode::DefaultError); + } + if cell.last_slot == 0 && open_slot_index.is_none() { + open_slot_index = Some(i); + } + } + let open_slot = open_slot_index.ok_or(ErrorCode::DefaultError)?; + + let cell = self.get_mut(open_slot); + *cell = datum; + + self.sort()?; + Ok(()) + } + + #[cfg(test)] + pub fn sort(&mut self) -> DriftResult<()> { + let len = self.len(); + let mut data: Vec = Vec::with_capacity(len as usize); + for i in 0..len { + data.push(*self.get(i)); + } + data.sort_by_key(|datum| datum.constituent_index); + for i in 0..len { + let cell = self.get_mut(i); + *cell = data[i as usize]; + } + Ok(()) + } +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct ConstituentCorrelationsFixed { + pub lp_pool: Pubkey, + pub bump: u8, + _pad: [u8; 3], + /// total elements in the flattened `data` vec + pub len: u32, +} + +impl HasLen for ConstituentCorrelationsFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct ConstituentCorrelations { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. The weights of the target weight matrix. Updated async + pub correlations: Vec, +} + +impl HasLen for ConstituentCorrelations { + fn len(&self) -> u32 { + self.correlations.len() as u32 + } +} + +impl_zero_copy_loader!( + ConstituentCorrelations, + crate::id, + ConstituentCorrelationsFixed, + i64 +); + +impl ConstituentCorrelations { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * num_constituents * 8 + } + + pub fn validate(&self) -> DriftResult<()> { + let len = self.correlations.len(); + let num_constituents = (len as f32).sqrt() as usize; // f32 is plenty precise for matrix dims < 2^16 + validate!( + num_constituents * num_constituents == self.correlations.len(), + ErrorCode::DefaultError, + "ConstituentCorrelation correlations len must be a perfect square" + )?; + + for i in 0..num_constituents { + for j in 0..num_constituents { + let corr = self.correlations[i * num_constituents + j]; + validate!( + corr <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" + )?; + let corr_ji = self.correlations[j * num_constituents + i]; + validate!( + corr == corr_ji, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be symmetric" + )?; + } + let corr_ii = self.correlations[i * num_constituents + i]; + validate!( + corr_ii == PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations diagonal must be PERCENTAGE_PRECISION" + )?; + } + + Ok(()) + } + + pub fn add_new_constituent(&mut self, new_constituent_correlations: &[i64]) -> DriftResult { + // Add a new constituent at index N (where N = old size), + // given a slice `new_corrs` of length `N` such that + // new_corrs[i] == correlation[i, N]. + // + // On entry: + // self.correlations.len() == N*N + // + // After: + // self.correlations.len() == (N+1)*(N+1) + let len = self.correlations.len(); + let n = (len as f64).sqrt() as usize; + validate!( + n * n == len, + ErrorCode::DefaultError, + "existing correlations len must be a perfect square" + )?; + validate!( + new_constituent_correlations.len() == n, + ErrorCode::DefaultError, + "new_corrs length must equal number of number of other constituents ({})", + n + )?; + for &c in new_constituent_correlations { + validate!( + c <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "correlation must be ≤ PERCENTAGE_PRECISION" + )?; + } + + let new_n = n + 1; + let mut buf = Vec::with_capacity(new_n * new_n); + + for i in 0..n { + buf.extend_from_slice(&self.correlations[i * n..i * n + n]); + buf.push(new_constituent_correlations[i]); + } + + buf.extend_from_slice(new_constituent_correlations); + buf.push(PERCENTAGE_PRECISION_I64); + + self.correlations = buf; + + debug_assert_eq!(self.correlations.len(), new_n * new_n); + + Ok(()) + } + + pub fn set_correlation(&mut self, i: u16, j: u16, corr: i64) -> DriftResult { + let num_constituents = (self.correlations.len() as f64).sqrt() as usize; + validate!( + i < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index i = {}, ConstituentCorrelation len = {}", + i, + num_constituents + )?; + validate!( + j < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index j = {}, ConstituentCorrelation len = {}", + j, + num_constituents + )?; + validate!( + corr <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" + )?; + + self.correlations[i as usize * num_constituents + j as usize] = corr; + self.correlations[j as usize * num_constituents + i as usize] = corr; + + self.validate()?; + + Ok(()) + } +} + +impl<'a> AccountZeroCopy<'a, i64, ConstituentCorrelationsFixed> { + pub fn get_correlation(&self, i: u16, j: u16) -> DriftResult { + let num_constituents = (self.len() as f64).sqrt() as usize; + validate!( + i < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index i = {}, ConstituentCorrelation len = {}", + i, + num_constituents + )?; + validate!( + j < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index j = {}, ConstituentCorrelation len = {}", + j, + num_constituents + )?; + + let corr = self.get((i as usize * num_constituents + j as usize) as u32); + Ok(*corr) + } +} + +pub fn get_gamma_covar_matrix( + correlation_ij: i64, + gamma_i: u8, + gamma_j: u8, + vol_i: u64, + vol_j: u64, +) -> DriftResult<[[i128; 2]; 2]> { + // Build the covariance matrix + let mut covar_matrix = [[0i128; 2]; 2]; + let scaled_vol_i = vol_i as i128; + let scaled_vol_j = vol_j as i128; + covar_matrix[0][0] = scaled_vol_i + .safe_mul(scaled_vol_i)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[1][1] = scaled_vol_j + .safe_mul(scaled_vol_j)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[0][1] = scaled_vol_i + .safe_mul(scaled_vol_j)? + .safe_mul(correlation_ij as i128)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[1][0] = covar_matrix[0][1]; + + // Build the gamma matrix as a diagonal matrix + let gamma_matrix = [[gamma_i as i128, 0i128], [0i128, gamma_j as i128]]; + + // Multiply gamma_matrix with covar_matrix: product = gamma_matrix * covar_matrix + let mut product = [[0i128; 2]; 2]; + for i in 0..2 { + for j in 0..2 { + for k in 0..2 { + product[i][j] = product[i][j] + .checked_add( + gamma_matrix[i][k] + .checked_mul(covar_matrix[k][j]) + .ok_or(ErrorCode::MathError)?, + ) + .ok_or(ErrorCode::MathError)?; + } + } + } + + Ok(product) +} + +pub fn update_constituent_target_base_for_derivatives( + aum: u128, + derivative_groups: &BTreeMap>, + constituent_map: &ConstituentMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + constituent_target_base: &mut AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, +) -> DriftResult<()> { + for (parent_index, constituent_indexes) in derivative_groups.iter() { + let parent_constituent = constituent_map.get_ref(parent_index)?; + + let parent_spot_market = spot_market_map.get_ref(&parent_constituent.spot_market_index)?; + let parent_oracle_price_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + parent_spot_market.market_index, + &parent_spot_market.oracle_id(), + parent_spot_market + .historical_oracle_data + .last_oracle_price_twap, + parent_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + if !is_oracle_valid_for_action( + parent_oracle_price_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Parent constituent {} oracle is invalid", + parent_constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle); + } + let parent_constituent_price = parent_oracle_price_and_validity.0.price; + + let parent_target_base = constituent_target_base + .get(*parent_index as u32) + .target_base; + let target_parent_weight = calculate_target_weight( + parent_target_base, + &*spot_market_map.get_ref(&parent_constituent.spot_market_index)?, + parent_oracle_price_and_validity.0.price, + aum, + )?; + let mut derivative_weights_sum: u64 = 0; + for constituent_index in constituent_indexes { + let constituent = constituent_map.get_ref(constituent_index)?; + let constituent_spot_market = + spot_market_map.get_ref(&constituent.spot_market_index)?; + let constituent_oracle_price_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + constituent.spot_market_index, + &constituent_spot_market.oracle_id(), + constituent_spot_market + .historical_oracle_data + .last_oracle_price_twap, + constituent_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + if !is_oracle_valid_for_action( + constituent_oracle_price_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Constituent {} oracle is invalid", + constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle); + } + + if constituent_oracle_price_and_validity.0.price + < parent_constituent_price + .safe_mul(constituent.constituent_derivative_depeg_threshold as i64)? + .safe_div(PERCENTAGE_PRECISION_I64)? + { + msg!( + "Constituent {} last oracle price {} is too low compared to parent constituent {} last oracle price {}. Assuming depegging and setting target base to 0.", + constituent.constituent_index, + constituent_oracle_price_and_validity.0.price, + parent_constituent.constituent_index, + parent_constituent_price + ); + constituent_target_base + .get_mut(*constituent_index as u32) + .target_base = 0_i64; + continue; + } + + derivative_weights_sum = + derivative_weights_sum.saturating_add(constituent.derivative_weight); + + let target_weight = (target_parent_weight as i128) + .safe_mul(constituent.derivative_weight.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + msg!( + "constituent: {}, target weight: {}", + constituent_index, + target_weight, + ); + let target_base = aum + .cast::()? + .safe_mul(target_weight)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_mul(10_i128.pow(constituent.decimals as u32))? + .safe_div(constituent_oracle_price_and_validity.0.price as i128)?; + + msg!( + "constituent: {}, target base: {}", + constituent_index, + target_base + ); + constituent_target_base + .get_mut(*constituent_index as u32) + .target_base = target_base.cast::()?; + } + + validate!( + derivative_weights_sum <= PERCENTAGE_PRECISION_U64, + ErrorCode::InvalidConstituentDerivativeWeights, + "derivative_weights_sum for parent constituent {} must be less than or equal to 100%", + parent_index + )?; + + constituent_target_base + .get_mut(*parent_index as u32) + .target_base = parent_target_base + .safe_mul(PERCENTAGE_PRECISION_U64.safe_sub(derivative_weights_sum)? as i64)? + .safe_div(PERCENTAGE_PRECISION_I64)?; + } + + Ok(()) +} diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs new file mode 100644 index 0000000000..ef9153b723 --- /dev/null +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -0,0 +1,3860 @@ +#[cfg(test)] +mod tests { + use crate::math::constants::{ + BASE_PRECISION_I64, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I64, QUOTE_PRECISION, + }; + use crate::state::lp_pool::*; + use std::{cell::RefCell, marker::PhantomData, vec}; + + fn amm_const_datum( + perp_market_index: u16, + constituent_index: u16, + weight: i64, + last_slot: u64, + ) -> AmmConstituentDatum { + AmmConstituentDatum { + perp_market_index, + constituent_index, + weight, + last_slot, + ..AmmConstituentDatum::default() + } + } + + #[test] + fn test_complex_implementation() { + // Constituents are BTC, SOL, ETH, USDC + + let slot = 20202020 as u64; + let amm_data = [ + amm_const_datum(0, 0, PERCENTAGE_PRECISION_I64, slot), // BTC-PERP + amm_const_datum(1, 1, PERCENTAGE_PRECISION_I64, slot), // SOL-PERP + amm_const_datum(2, 2, PERCENTAGE_PRECISION_I64, slot), // ETH-PERP + amm_const_datum(3, 0, 46 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for BTC + amm_const_datum(3, 1, 132 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for SOL + amm_const_datum(3, 2, 35 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for ETH + ]; + + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 6, + ..AmmConstituentMappingFixed::default() + }); + const LEN: usize = 6; + const DATA_SIZE: usize = std::mem::size_of::() * LEN; + let defaults: [AmmConstituentDatum; LEN] = [AmmConstituentDatum::default(); LEN]; + let mapping_data = RefCell::new(unsafe { + std::mem::transmute::<[AmmConstituentDatum; LEN], [u8; DATA_SIZE]>(defaults) + }); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + for amm_datum in amm_data { + println!("Adding AMM Constituent Datum: {:?}", amm_datum); + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_price: Vec = vec![ + AmmInventoryAndPricesAndSlots { + inventory: 4 * BASE_PRECISION_I64, + price: 100_000 * PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, + }, // $400k BTC + AmmInventoryAndPricesAndSlots { + inventory: 2000 * BASE_PRECISION_I64, + price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, + }, // $400k SOL + AmmInventoryAndPricesAndSlots { + inventory: 200 * BASE_PRECISION_I64, + price: 1500 * PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, + }, // $300k ETH + AmmInventoryAndPricesAndSlots { + inventory: 16500 * BASE_PRECISION_I64, + price: PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, + }, // $16.5k FARTCOIN + ]; + let mut constituents_indexes_and_decimals_and_prices = vec![ + ConstituentIndexAndDecimalAndPrice { + constituent_index: 0, + decimals: 6, + price: 100_000 * PRICE_PRECISION_I64, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 200 * PRICE_PRECISION_I64, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 2, + decimals: 6, + price: 1500 * PRICE_PRECISION_I64, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 3, + decimals: 6, + price: PRICE_PRECISION_I64, + }, // USDC + ]; + let aum = 2_000_000 * QUOTE_PRECISION; // $2M AUM + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 4, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 4 * 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_price, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + + let target_weights: Vec = target_zc_mut + .iter() + .enumerate() + .map(|(index, datum)| { + calculate_target_weight( + datum.target_base.cast::().unwrap(), + &SpotMarket::default_quote_market(), + amm_inventory_and_price.get(index).unwrap().price, + aum, + ) + .unwrap() + }) + .collect(); + + println!("Target Weights: {:?}", target_weights); + assert_eq!(target_weights.len(), 4); + assert_eq!(target_weights[0], -203795); // 20.3% BTC + assert_eq!(target_weights[1], -210890); // 21.1% SOL + assert_eq!(target_weights[2], -152887); // 15.3% ETH + assert_eq!(target_weights[3], 0); // USDC not set if it's not in AUM update + } + + #[test] + fn test_single_zero_weight() { + let slot = 20202020 as u64; + let amm_datum = amm_const_datum(0, 1, 0, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: 1_000_000, + price: 1_000_000, + last_oracle_slot: slot, + last_position_slot: slot, + }]; + let mut constituents_indexes_and_decimals_and_prices = + vec![ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 1_000_000, + }]; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + + assert!(target_zc_mut.iter().all(|&x| x.target_base == 0)); + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).target_base, 0); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, slot); + } + + #[test] + fn test_single_full_weight() { + let slot = 20202020 as u64; + let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let price = PRICE_PRECISION_I64; + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: BASE_PRECISION_I64, + price, + last_oracle_slot: slot, + last_position_slot: slot, + }]; + let mut constituents_indexes_and_decimals_and_prices = + vec![ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price, + }]; + let aum = 1_000_000; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Should succeed but not update the target's slot if clock slot is too recent + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + 4040404040440404404, // far future slot + ) + .unwrap(); + + let weight = calculate_target_weight( + target_zc_mut.get(0).target_base as i64, + &SpotMarket::default(), + price, + aum, + ) + .unwrap(); + + assert_eq!( + target_zc_mut.get(0).target_base as i128, + -1 * 10_i128.pow(6_u32) + ); + assert_eq!(weight, -1000000); + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, 0); + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, slot); // still not updated + } + + #[test] + fn test_multiple_constituents_partial_weights() { + let slot = 20202020 as u64; + let amm_mapping_data = vec![ + amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64 / 2, 111), + amm_const_datum(0, 2, PERCENTAGE_PRECISION_I64 / 2, 111), + ]; + + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: amm_mapping_data.len() as u32, + ..AmmConstituentMappingFixed::default() + }); + + // 48 = size_of::() * amm_mapping_data.len() + let mapping_data = RefCell::new([0u8; 48]); + + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + for amm_datum in &amm_mapping_data { + mapping_zc_mut + .add_amm_constituent_datum(*amm_datum) + .unwrap(); + } + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: 1_000_000_000, + price: 1_000_000, + last_oracle_slot: slot, + last_position_slot: slot, + }]; + let mut constituents_indexes_and_decimals_and_prices = vec![ + ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 1_000_000, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 2, + decimals: 6, + price: 1_000_000, + }, + ]; + + let aum = 1_000_000; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: amm_mapping_data.len() as u32, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 2 * 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + + assert_eq!(target_zc_mut.len(), 2); + + for i in 0..target_zc_mut.len() { + assert_eq!( + calculate_target_weight( + target_zc_mut.get(i).target_base, + &SpotMarket::default_quote_market(), + constituents_indexes_and_decimals_and_prices + .get(i as usize) + .unwrap() + .price, + aum, + ) + .unwrap(), + -1 * PERCENTAGE_PRECISION_I64 / 2 + ); + assert_eq!(target_zc_mut.get(i).last_oracle_slot, slot); + } + } + + #[test] + fn test_zero_aum_safe() { + let slot = 20202020 as u64; + let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: 1_000_000, + price: 142_000_000, + last_oracle_slot: slot, + last_position_slot: slot, + }]; + let mut constituents_indexes_and_decimals_and_prices = + vec![ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 9, + price: 142_000_000, + }]; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).target_base, -1_000_000); // despite no aum, desire to reach target + assert_eq!(target_zc_mut.get(0).last_oracle_slot, slot); + } +} + +#[cfg(test)] +mod swap_tests { + use crate::math::constants::{ + PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I128, PRICE_PRECISION_I64, + SPOT_BALANCE_PRECISION, + }; + use crate::state::lp_pool::*; + + #[test] + fn test_get_swap_price() { + let lp_pool = LPPool::default(); + + let in_oracle = OraclePriceData { + price: 1_000_000, + ..OraclePriceData::default() + }; + let out_oracle = OraclePriceData { + price: 233_400_000, + ..OraclePriceData::default() + }; + + // same decimals + let (price_num, price_denom) = lp_pool + .get_swap_price(6, 6, &in_oracle, &out_oracle) + .unwrap(); + assert_eq!(price_num, 1_000_000); + assert_eq!(price_denom, 233_400_000); + + let (price_num, price_denom) = lp_pool + .get_swap_price(6, 6, &out_oracle, &in_oracle) + .unwrap(); + assert_eq!(price_num, 233_400_000); + assert_eq!(price_denom, 1_000_000); + } + + fn get_swap_amount_decimals_scenario( + in_target_position_delay: u64, + out_target_position_delay: u64, + in_target_oracle_delay: u64, + out_target_oracle_delay: u64, + in_current_weight: u64, + out_current_weight: u64, + in_decimals: u32, + out_decimals: u32, + in_amount: u64, + expected_in_amount: u128, + expected_out_amount: u128, + expected_in_fee: i128, + expected_out_fee: i128, + in_xi: u8, + out_xi: u8, + in_gamma_inventory: u8, + out_gamma_inventory: u8, + in_gamma_execution: u8, + out_gamma_execution: u8, + in_volatility: u64, + out_volatility: u64, + ) { + let lp_pool = LPPool { + last_aum: 1_000_000_000_000, + target_oracle_delay_fee_bps_per_10_slots: 2, + target_position_delay_fee_bps_per_10_slots: 10, + ..LPPool::default() + }; + + let oracle_0 = OraclePriceData { + price: 1_000_000, + ..OraclePriceData::default() + }; + let oracle_1 = OraclePriceData { + price: 233_400_000, + ..OraclePriceData::default() + }; + + let in_notional = (in_current_weight as u128) * lp_pool.last_aum / PERCENTAGE_PRECISION; + let in_token_amount = in_notional * 10_u128.pow(in_decimals) / oracle_0.price as u128; + + let out_notional = (out_current_weight as u128) * lp_pool.last_aum / PERCENTAGE_PRECISION; + let out_token_amount = out_notional * 10_u128.pow(out_decimals) / oracle_1.price as u128; + + let constituent_0 = Constituent { + decimals: in_decimals as u8, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: in_gamma_execution, + gamma_inventory: in_gamma_inventory, + xi: in_xi, + volatility: in_volatility, + vault_token_balance: in_token_amount as u64, + // max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + ..Constituent::default() + }; + let constituent_1 = Constituent { + decimals: out_decimals as u8, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: out_gamma_execution, + gamma_inventory: out_gamma_inventory, + xi: out_xi, + volatility: out_volatility, + vault_token_balance: out_token_amount as u64, + // max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + ..Constituent::default() + }; + let spot_market_0 = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + let spot_market_1 = SpotMarket { + decimals: out_decimals, + ..SpotMarket::default() + }; + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool + .get_swap_amount( + in_target_position_delay, + out_target_position_delay, + in_target_oracle_delay, + out_target_oracle_delay, + &oracle_0, + &oracle_1, + &constituent_0, + &constituent_1, + &spot_market_0, + &spot_market_1, + 500_000, + 500_000, + in_amount.cast::().unwrap(), + 0, + ) + .unwrap(); + assert_eq!(in_amount, expected_in_amount); + assert_eq!(out_amount, expected_out_amount); + assert_eq!(in_fee, expected_in_fee); + assert_eq!(out_fee, expected_out_fee); + } + + #[test] + fn test_get_swap_amount_in_6_out_6() { + get_swap_amount_decimals_scenario( + 0, + 0, + 0, + 0, + 500_000, + 500_000, + 6, + 6, + 150_000_000_000, + 150_000_000_000, + 642577120, + 22500000, // 1 bps + 281448, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_swap_amount_in_6_out_9() { + get_swap_amount_decimals_scenario( + 0, + 0, + 0, + 0, + 500_000, + 500_000, + 6, + 9, + 150_000_000_000, + 150_000_000_000, + 642577120822, + 22500000, + 282091356, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_swap_amount_in_9_out_6() { + get_swap_amount_decimals_scenario( + 0, + 0, + 0, + 0, + 500_000, + 500_000, + 9, + 6, + 150_000_000_000_000, + 150_000_000_000_000, + 642577120, + 22500000000, // 1 bps + 281448, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_weight() { + let c = Constituent { + swap_fee_min: -1 * PERCENTAGE_PRECISION_I64 / 10000, // -1 bps (rebate) + swap_fee_max: PERCENTAGE_PRECISION_I64 / 100, // 100 bps + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, // 10% + spot_market_index: 0, + spot_balance: ConstituentSpotBalance { + scaled_balance: 500_000, + cumulative_deposits: 1_000_000, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + }, + vault_token_balance: 500_000, + decimals: 6, + ..Constituent::default() + }; + + let spot_market = SpotMarket { + market_index: 0, + decimals: 6, + cumulative_deposit_interest: 10_000_000_000_000, + ..SpotMarket::default() + }; + + let full_balance = c.get_full_token_amount(&spot_market).unwrap(); + assert_eq!(full_balance, 1_000_000); + + // 1/10 = 10% + let weight = c + .get_weight( + 1_000_000, // $1 + &spot_market, + 0, + 10_000_000, + ) + .unwrap(); + assert_eq!(weight, 100_000); + + // (1+1)/10 = 20% + let weight = c + .get_weight(1_000_000, &spot_market, 1_000_000, 10_000_000) + .unwrap(); + assert_eq!(weight, 200_000); + + // (1-0.5)/10 = 0.5% + let weight = c + .get_weight(1_000_000, &spot_market, -500_000, 10_000_000) + .unwrap(); + assert_eq!(weight, 50_000); + } + + fn get_add_liquidity_mint_amount_scenario( + in_target_position_delay: u64, + in_target_oracle_delay: u64, + last_aum: u128, + _now: i64, + in_decimals: u32, + in_amount: u128, + dlp_total_supply: u64, + expected_lp_amount: u64, + expected_lp_fee: i64, + expected_in_fee_amount: i128, + xi: u8, + gamma_inventory: u8, + gamma_execution: u8, + volatility: u64, + ) { + let lp_pool = LPPool { + last_aum, + _padding: 0, + min_mint_fee: 0, + ..LPPool::default() + }; + + let spot_market = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + + let token_balance = if in_decimals > 6 { + last_aum.safe_mul(10_u128.pow(in_decimals - 6)).unwrap() + } else { + last_aum.safe_div(10_u128.pow(6 - in_decimals)).unwrap() + }; + + let constituent = Constituent { + decimals: in_decimals as u8, + swap_fee_min: 0, + swap_fee_max: 0, + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + spot_market_index: 0, + spot_balance: ConstituentSpotBalance { + scaled_balance: 0, + cumulative_deposits: 0, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + }, + vault_token_balance: token_balance as u64, + xi, + gamma_inventory, + gamma_execution, + volatility, + ..Constituent::default() + }; + + let oracle = OraclePriceData { + price: PRICE_PRECISION_I64, // $1 + ..OraclePriceData::default() + }; + + let (lp_amount, in_amount_1, lp_fee, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + in_target_position_delay, + in_target_oracle_delay, + &spot_market, + &constituent, + in_amount, + &oracle, + PERCENTAGE_PRECISION_I64, // 100% target weight, to minimize fee for this test + dlp_total_supply, + ) + .unwrap(); + + assert_eq!(lp_amount, expected_lp_amount); + assert_eq!(lp_fee, expected_lp_fee); + assert_eq!(in_amount_1, in_amount); + assert_eq!(in_fee_amount, expected_in_fee_amount); + } + + // test with 6 decimal constituent (matches dlp precision) + #[test] + fn test_get_add_liquidity_mint_amount_zero_aum() { + get_add_liquidity_mint_amount_scenario( + 0, 0, 0, // last_aum + 0, // now + 6, // in_decimals + 1_000_000, // in_amount + 0, // dlp_total_supply (non-zero to avoid MathError) + 1_000_000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, 2, 2, 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum() { + get_add_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 6, // in_decimals + 1_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount + 0, // expected_lp_fee + 300, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + // test with 8 decimal constituent + #[test] + fn test_get_add_liquidity_mint_amount_with_zero_aum_8_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, + 0, + 0, // last_aum + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 0, // dlp_total_supply + 1_000_000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum_8_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount in lp decimals + 0, // expected_lp_fee + 30000, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + // test with 4 decimal constituent + #[test] + fn test_get_add_liquidity_mint_amount_with_zero_aum_4_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, 0, 0, // last_aum + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = $1 + 0, // dlp_total_supply + 1000000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, 2, 2, 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum_4_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount + 0, // expected_lp_fee + 3, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + fn get_remove_liquidity_mint_amount_scenario( + out_target_position_delay: u64, + _out_target_oracle_delay: u64, + last_aum: u128, + _now: i64, + in_decimals: u32, + lp_burn_amount: u64, + dlp_total_supply: u64, + expected_out_amount: u128, + expected_lp_fee: i64, + expected_out_fee_amount: i128, + xi: u8, + gamma_inventory: u8, + gamma_execution: u8, + volatility: u64, + ) { + let lp_pool = LPPool { + last_aum, + _padding: 0, + min_mint_fee: 100, // 1 bps + ..LPPool::default() + }; + + let spot_market = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + + let token_balance = if in_decimals > 6 { + last_aum.safe_mul(10_u128.pow(in_decimals - 6)).unwrap() + } else { + last_aum.safe_div(10_u128.pow(6 - in_decimals)).unwrap() + }; + + let constituent = Constituent { + decimals: in_decimals as u8, + swap_fee_min: 0, + swap_fee_max: 0, + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + spot_market_index: 0, + spot_balance: ConstituentSpotBalance { + scaled_balance: 0, + cumulative_deposits: 0, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + }, + vault_token_balance: token_balance as u64, + xi, + gamma_inventory, + gamma_execution, + volatility, + ..Constituent::default() + }; + + let oracle = OraclePriceData { + price: PRICE_PRECISION_I64, // $1 + ..OraclePriceData::default() + }; + + let (lp_amount_1, out_amount, lp_fee, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + out_target_position_delay, + out_target_position_delay, + &spot_market, + &constituent, + lp_burn_amount, + &oracle, + PERCENTAGE_PRECISION_I64, // 100% target weight, to minimize fee for this test + dlp_total_supply, + ) + .unwrap(); + + assert_eq!(lp_amount_1, lp_burn_amount); + assert_eq!(lp_fee, expected_lp_fee); + assert_eq!(out_amount, expected_out_amount); + assert_eq!(out_fee_amount, expected_out_fee_amount); + } + + // test with 6 decimal constituent (matches dlp precision) + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 6, // in_decimals + 1_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999900, // expected_out_amount + 100, // expected_lp_fee + 299, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + // test with 8 decimal constituent + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 9999000000, // expected_out_amount + 10000, // expected_lp_fee + 2999700, + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + // test with 4 decimal constituent + // there will be a problem with 4 decimal constituents with aum ~10M + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_4_decimals() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = 1/10000 + 10_000_000_000, // dlp_total_supply + 99, // expected_out_amount + 1, // expected_lp_fee + 0, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_5_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 5, // in_decimals + 100_000_000_000 * 100_000, // in_amount + 100_000_000_000 * 1_000_000, // dlp_total_supply + 999900000000000, // expected_out_amount + 1000000000000, // expected_lp_fee + 473952600000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_6_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 6, // in_decimals + 100_000_000_000 * 1_000_000 - 1_000_000, // Leave in QUOTE AMOUNT + 100_000_000_000 * 1_000_000, // dlp_total_supply + 99989999900000000, // expected_out_amount + 9999999999900, // expected_lp_fee + 349765019650200, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000_000_000, // last_aum ($10,000,000,000) + 0, // now + 8, // in_decimals + 10_000_000_000 * 1_000_000 - 1_000_000, // in_amount + 10_000_000_000 * 1_000_000, // dlp_total_supply + 999899999000000000, // expected_out_amount + (10_000_000_000 * 1_000_000 - 1_000_000) / 10000, // expected_lp_fee + 3497650196502000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + fn round_to_sig(x: i128, sig: u32) -> i128 { + if x == 0 { + return 0; + } + let digits = (x.abs() as f64).log10().floor() as u32 + 1; + let factor = 10_i128.pow(digits - sig); + ((x + factor / 2) / factor) * factor + } + + fn get_swap_amounts( + in_target_position_delay: u64, + out_target_position_delay: u64, + in_target_oracle_delay: u64, + out_target_oracle_delay: u64, + in_oracle_price: i64, + out_oracle_price: i64, + in_current_weight: i64, + out_current_weight: i64, + in_amount: u64, + in_volatility: u64, + out_volatility: u64, + in_target_weight: i64, + out_target_weight: i64, + ) -> (u128, u128, i128, i128, i128, i128) { + let lp_pool = LPPool { + last_aum: 1_000_000_000_000, + ..LPPool::default() + }; + + let oracle_0 = OraclePriceData { + price: in_oracle_price, + ..OraclePriceData::default() + }; + let oracle_1 = OraclePriceData { + price: out_oracle_price, + ..OraclePriceData::default() + }; + + let in_notional = (in_current_weight as i128) * lp_pool.last_aum.cast::().unwrap() + / PERCENTAGE_PRECISION_I128; + let in_token_amount = in_notional * 10_i128.pow(6) / oracle_0.price as i128; + let in_spot_balance = if in_token_amount > 0 { + ConstituentSpotBalance { + scaled_balance: (in_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + } + } else { + ConstituentSpotBalance { + scaled_balance: (in_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Borrow, + market_index: 0, + ..ConstituentSpotBalance::default() + } + }; + + let out_notional = (out_current_weight as i128) * lp_pool.last_aum.cast::().unwrap() + / PERCENTAGE_PRECISION_I128; + let out_token_amount = out_notional * 10_i128.pow(6) / oracle_1.price as i128; + let out_spot_balance = if out_token_amount > 0 { + ConstituentSpotBalance { + scaled_balance: (out_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + } + } else { + ConstituentSpotBalance { + scaled_balance: (out_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + } + }; + + let constituent_0 = Constituent { + decimals: 6, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: 1, + gamma_inventory: 1, + xi: 1, + volatility: in_volatility, + spot_balance: in_spot_balance, + ..Constituent::default() + }; + let constituent_1 = Constituent { + decimals: 6, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: 2, + gamma_inventory: 2, + xi: 2, + volatility: out_volatility, + spot_balance: out_spot_balance, + ..Constituent::default() + }; + let spot_market_0 = SpotMarket { + decimals: 6, + ..SpotMarket::default() + }; + let spot_market_1 = SpotMarket { + decimals: 6, + ..SpotMarket::default() + }; + + let (in_amount_result, out_amount, in_fee, out_fee) = lp_pool + .get_swap_amount( + in_target_position_delay, + out_target_position_delay, + in_target_oracle_delay, + out_target_oracle_delay, + &oracle_0, + &oracle_1, + &constituent_0, + &constituent_1, + &spot_market_0, + &spot_market_1, + in_target_weight, + out_target_weight, + in_amount.cast::().unwrap(), + 0, + ) + .unwrap(); + + return ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount, + out_token_amount, + ); + } + + #[test] + fn grid_search_swap() { + let weights: [i64; 20] = [ + -100_000, -200_000, -300_000, -400_000, -500_000, -600_000, -700_000, -800_000, + -900_000, -1_000_000, 100_000, 200_000, 300_000, 400_000, 500_000, 600_000, 700_000, + 800_000, 900_000, 1_000_000, + ]; + let in_amounts: Vec = (0..=10) + .map(|i| (1000 + i * 20000) * 10_u64.pow(6)) + .collect(); + + let volatilities: Vec = (1..=10) + .map(|i| PERCENTAGE_PRECISION_U64 * i / 100) + .collect(); + + let in_oracle_price = PRICE_PRECISION_I64; // $1 + let out_oracle_price = 233_400_000; // $233.4 + + // Assert monotonically increasing fees in in_amounts + for in_current_weight in weights.iter() { + let out_current_weight = 1_000_000 - *in_current_weight; + for out_volatility in volatilities.iter() { + let mut prev_in_fee_bps = 0_i128; + let mut prev_out_fee_bps = 0_i128; + for in_amount in in_amounts.iter() { + let (in_amount_result, out_amount, in_fee, out_fee, _, _) = get_swap_amounts( + 0, + 0, + 0, + 0, + in_oracle_price, + out_oracle_price, + *in_current_weight, + out_current_weight, + *in_amount, + 0, + *out_volatility, + PERCENTAGE_PRECISION_I64, // 100% target weight + PERCENTAGE_PRECISION_I64, // 100% target weight + ); + + // Calculate fee in basis points with precision + let in_fee_bps = if in_amount_result > 0 { + (in_fee * 10_000 * 1_000_000) / in_amount_result as i128 + } else { + 0 + }; + + let out_fee_bps = if out_amount > 0 { + (out_fee * 10_000 * 1_000_000) / out_amount as i128 + } else { + 0 + }; + + // Assert monotonically increasing fees + if in_amounts.iter().position(|&x| x == *in_amount).unwrap() > 0 { + assert!( + in_fee_bps >= prev_in_fee_bps, + "in_fee should be monotonically increasing. Current: {} bps, Previous: {} bps, weight: {}, amount: {}, volatility: {}", + in_fee_bps as f64 / 1_000_000.0, + prev_in_fee_bps as f64 / 1_000_000.0, + in_current_weight, + in_amount, + out_volatility + ); + assert!( + out_fee_bps >= prev_out_fee_bps, + "out_fee should be monotonically increasing. Current: {} bps, Previous: {} bps, weight: {}, amount: {}, volatility: {}", + out_fee_bps as f64 / 1_000_000.0, + prev_out_fee_bps as f64 / 1_000_000.0, + out_current_weight, + in_amount, + out_volatility + ); + } + + println!( + "in_weight: {}, out_weight: {}, in_amount: {}, out_amount: {}, in_fee: {:.6} bps, out_fee: {:.6} bps", + in_current_weight, + out_current_weight, + in_amount_result, + out_amount, + in_fee_bps as f64 / 1_000_000.0, + out_fee_bps as f64 / 1_000_000.0 + ); + + prev_in_fee_bps = in_fee_bps; + prev_out_fee_bps = out_fee_bps; + } + } + } + + // Assert monotonically increasing fees based on error improvement + for in_amount in in_amounts.iter() { + for in_current_weight in weights.iter() { + let out_current_weight = 1_000_000 - *in_current_weight; + let fixed_volatility = PERCENTAGE_PRECISION_U64 * 5 / 100; + let target_weights: Vec = (1..=20).map(|i| i * 50_000).collect(); + + let mut results: Vec<(i128, i128, i128, i128, i128, i128)> = Vec::new(); + + for target_weight in target_weights.iter() { + let in_target_weight = *target_weight; + let out_target_weight = 1_000_000 - in_target_weight; + + let ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount_pre, + out_token_amount_pre, + ) = get_swap_amounts( + 0, + 0, + 0, + 0, + in_oracle_price, + out_oracle_price, + *in_current_weight, + out_current_weight, + *in_amount, + fixed_volatility, + fixed_volatility, + in_target_weight, + out_target_weight, + ); + + // Calculate weights after swap + + let out_token_after = out_token_amount_pre - out_amount as i128 + out_fee; + let in_token_after = in_token_amount_pre + in_amount_result as i128; + + let out_notional_after = + out_token_after * (out_oracle_price as i128) / PRICE_PRECISION_I128; + let in_notional_after = + in_token_after * (in_oracle_price as i128) / PRICE_PRECISION_I128; + let total_notional_after = in_notional_after + out_notional_after; + + let out_weight_after = + (out_notional_after * PERCENTAGE_PRECISION_I128) / (total_notional_after); + let in_weight_after = + (in_notional_after * PERCENTAGE_PRECISION_I128) / (total_notional_after); + + // Calculate error improvement (positive means improvement) + let in_error_before = (*in_current_weight - in_target_weight).abs() as i128; + let out_error_before = (out_current_weight - out_target_weight).abs() as i128; + + let in_error_after = (in_weight_after - in_target_weight as i128).abs(); + let out_error_after = (out_weight_after - out_target_weight as i128).abs(); + + let in_error_improvement = round_to_sig(in_error_before - in_error_after, 2); + let out_error_improvement = round_to_sig(out_error_before - out_error_after, 2); + + let in_fee_bps = if in_amount_result > 0 { + (in_fee * 10_000 * 1_000_000) / in_amount_result as i128 + } else { + 0 + }; + + let out_fee_bps = if out_amount > 0 { + (out_fee * 10_000 * 1_000_000) / out_amount as i128 + } else { + 0 + }; + + results.push(( + in_error_improvement, + out_error_improvement, + in_fee_bps, + out_fee_bps, + in_target_weight as i128, + out_target_weight as i128, + )); + + println!( + "in_weight: {}, out_weight: {}, in_target: {}, out_target: {}, in_error_improvement: {}, out_error_improvement: {}, in_fee: {:.6} bps, out_fee: {:.6} bps", + in_current_weight, + out_current_weight, + in_target_weight, + out_target_weight, + in_error_improvement, + out_error_improvement, + in_fee_bps as f64 / 1_000_000.0, + out_fee_bps as f64 / 1_000_000.0 + ); + } + + // Sort by in_error_improvement and check monotonicity + results.sort_by_key(|&(in_error_improvement, _, _, _, _, _)| -in_error_improvement); + + for i in 1..results.len() { + let (prev_in_improvement, _, prev_in_fee_bps, _, _, _) = results[i - 1]; + let (curr_in_improvement, _, curr_in_fee_bps, _, in_target, _) = results[i]; + + // Less improvement should mean higher fees + if curr_in_improvement < prev_in_improvement { + assert!( + curr_in_fee_bps >= prev_in_fee_bps, + "in_fee should increase as error improvement decreases. Current improvement: {}, Previous improvement: {}, Current fee: {:.6} bps, Previous fee: {:.6} bps, in_weight: {}, in_target: {}", + curr_in_improvement, + prev_in_improvement, + curr_in_fee_bps as f64 / 1_000_000.0, + prev_in_fee_bps as f64 / 1_000_000.0, + in_current_weight, + in_target + ); + } + } + + // Sort by out_error_improvement and check monotonicity + results + .sort_by_key(|&(_, out_error_improvement, _, _, _, _)| -out_error_improvement); + + for i in 1..results.len() { + let (_, prev_out_improvement, _, prev_out_fee_bps, _, _) = results[i - 1]; + let (_, curr_out_improvement, _, curr_out_fee_bps, _, out_target) = results[i]; + + // Less improvement should mean higher fees + if curr_out_improvement < prev_out_improvement { + assert!( + curr_out_fee_bps >= prev_out_fee_bps, + "out_fee should increase as error improvement decreases. Current improvement: {}, Previous improvement: {}, Current fee: {:.6} bps, Previous fee: {:.6} bps, out_weight: {}, out_target: {}", + curr_out_improvement, + prev_out_improvement, + curr_out_fee_bps as f64 / 1_000_000.0, + prev_out_fee_bps as f64 / 1_000_000.0, + out_current_weight, + out_target + ); + } + } + } + } + } +} + +#[cfg(test)] +mod swap_fee_tests { + use crate::math::constants::{ + PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, QUOTE_PRECISION, + }; + use crate::state::lp_pool::*; + + #[test] + fn test_get_gamma_covar_matrix() { + // in = sol, out = btc + let covar_matrix = get_gamma_covar_matrix( + PERCENTAGE_PRECISION_I64, + 2, // gamma sol + 2, // gamma btc + 4 * PERCENTAGE_PRECISION_U64 / 100, // vol sol + 3 * PERCENTAGE_PRECISION_U64 / 100, // vol btc + ) + .unwrap(); + assert_eq!(covar_matrix, [[3200, 2400], [2400, 1800]]); + } + + #[test] + fn test_lp_pool_get_linear_fee_execution() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let trade_ratio = 5_000_000 * QUOTE_PRECISION_I128 * PERCENTAGE_PRECISION_I128 + / (15_000_000 * QUOTE_PRECISION_I128); + + let fee_execution_linear = lp_pool + .get_linear_fee_execution( + trade_ratio, + 1600, // 0.0016 + 2, + ) + .unwrap(); + + assert_eq!(fee_execution_linear, 1066); // 10.667 bps + } + + #[test] + fn test_lp_pool_get_quadratic_fee_execution() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let trade_ratio = 5_000_000 * QUOTE_PRECISION_I128 * PERCENTAGE_PRECISION_I128 + / (15_000_000 * QUOTE_PRECISION_I128); + + let fee_execution_quadratic = lp_pool + .get_quadratic_fee_execution( + trade_ratio, + 1600, // 0.0016 + 2, + ) + .unwrap(); + + assert_eq!(fee_execution_quadratic, 711); // 7.1 bps + } + + #[test] + fn test_lp_pool_get_quadratic_fee_inventory() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let (fee_in, fee_out) = lp_pool + .get_quadratic_fee_inventory( + [[3200, 2400], [2400, 1800]], + [ + 1_000_000 * QUOTE_PRECISION_I128, + -500_000 * QUOTE_PRECISION_I128, + ], + [ + -4_000_000 * QUOTE_PRECISION_I128, + 4_500_000 * QUOTE_PRECISION_I128, + ], + 5_000_000 * QUOTE_PRECISION, + ) + .unwrap(); + + assert_eq!(fee_in, 6 * PERCENTAGE_PRECISION_I128 / 100000); // 0.6 bps + assert_eq!(fee_out, -6 * PERCENTAGE_PRECISION_I128 / 100000); // -0.6 bps + } + + #[test] + fn test_target_delays() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + target_oracle_delay_fee_bps_per_10_slots: 2, + target_position_delay_fee_bps_per_10_slots: 10, + ..LPPool::default() + }; + + // Even a small delay in the position incurs a larger fee + let uncertainty_fee = lp_pool.get_target_uncertainty_fees(1, 0).unwrap(); + assert_eq!( + uncertainty_fee, + PERCENTAGE_PRECISION_I128 / 10000i128 + * lp_pool.target_position_delay_fee_bps_per_10_slots as i128 + ); + } +} + +#[cfg(test)] +mod settle_tests { + use crate::math::constants::{QUOTE_PRECISION, QUOTE_PRECISION_I64, QUOTE_PRECISION_U64}; + use crate::math::lp_pool::perp_lp_pool_settlement::{ + calculate_settlement_amount, update_cache_info, SettlementContext, SettlementDirection, + SettlementResult, + }; + use crate::state::amm_cache::CacheInfo; + use crate::state::spot_market::SpotMarket; + + fn create_mock_spot_market() -> SpotMarket { + SpotMarket::default() + } + + #[test] + fn test_calculate_settlement_no_amount_owed() { + let ctx = SettlementContext { + quote_owed_from_lp: 0, + quote_constituent_token_balance: 1000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::None); + assert_eq!(result.amount_transferred, 0); + } + + #[test] + fn test_lp_to_perp_settlement_sufficient_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 500 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 1000 * QUOTE_PRECISION_U64, + fee_pool_balance: 300 * QUOTE_PRECISION, + pnl_pool_balance: 200 * QUOTE_PRECISION, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000 * QUOTE_PRECISION_U64, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 500 * QUOTE_PRECISION_U64); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_lp_to_perp_settlement_insufficient_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 1500 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 1000 * QUOTE_PRECISION_U64, + fee_pool_balance: 300 * QUOTE_PRECISION, + pnl_pool_balance: 200 * QUOTE_PRECISION, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000 * QUOTE_PRECISION_U64, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!( + result.amount_transferred, + 1000 * QUOTE_PRECISION_U64 - QUOTE_PRECISION_U64 + ); // Limited by LP balance + } + + #[test] + fn test_lp_to_perp_settlement_no_lp_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 500, + quote_constituent_token_balance: 0, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::None); + assert_eq!(result.amount_transferred, 0); + } + + #[test] + fn test_perp_to_lp_settlement_fee_pool_sufficient() { + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 800, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_perp_to_lp_settlement_needs_both_pools() { + let ctx = SettlementContext { + quote_owed_from_lp: -1000, + quote_constituent_token_balance: 2000, + fee_pool_balance: 300, + pnl_pool_balance: 800, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 700); + } + + #[test] + fn test_perp_to_lp_settlement_insufficient_pools() { + let ctx = SettlementContext { + quote_owed_from_lp: -1500, + quote_constituent_token_balance: 2000, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 500); // Limited by pool balances + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 200); + } + + #[test] + fn test_settlement_edge_cases() { + // Test with zero fee pool + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 0, + pnl_pool_balance: 800, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 500); + + // Test with zero pnl pool + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 300, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.amount_transferred, 300); + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_update_cache_info_to_lp_pool() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: -400, + last_fee_pool_token_amount: 2_000, + last_net_pnl_pool_token_amount: 500, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 200, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 120, + pnl_pool_used: 80, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool + result.amount_transferred as i64; + let ts = 99; + let slot = 100; + + update_cache_info(&mut cache, &result, new_quote_owed, slot, ts).unwrap(); + + // quote_owed updated + assert_eq!(cache.quote_owed_from_lp_pool, new_quote_owed); + // settle fields updated + assert_eq!(cache.last_settle_amount, 200); + assert_eq!(cache.last_settle_slot, slot); + // fee pool decreases by fee_pool_used + assert_eq!(cache.last_fee_pool_token_amount, 2_000 - 120); + // pnl pool decreases by pnl_pool_used + assert_eq!(cache.last_net_pnl_pool_token_amount, 500 - 80); + } + + #[test] + fn test_update_cache_info_from_lp_pool() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 500, + last_fee_pool_token_amount: 1_000, + last_net_pnl_pool_token_amount: 200, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 150, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool - result.amount_transferred as i64; + let ts = 42; + let slot = 100; + + update_cache_info(&mut cache, &result, new_quote_owed, slot, ts).unwrap(); + + // quote_owed updated + assert_eq!(cache.quote_owed_from_lp_pool, new_quote_owed); + // settle fields updated + assert_eq!(cache.last_settle_amount, 150); + assert_eq!(cache.last_settle_slot, slot); + // fee pool increases by amount_transferred + assert_eq!(cache.last_fee_pool_token_amount, 1_000 + 150); + // pnl pool untouched + assert_eq!(cache.last_net_pnl_pool_token_amount, 200); + } + + #[test] + fn test_large_settlement_amounts() { + // Test with very large amounts to check for overflow + let ctx = SettlementContext { + quote_owed_from_lp: i64::MAX / 2, + quote_constituent_token_balance: u64::MAX / 2, + fee_pool_balance: u128::MAX / 4, + pnl_pool_balance: u128::MAX / 4, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert!(result.amount_transferred > 0); + } + + #[test] + fn test_negative_large_settlement_amounts() { + let ctx = SettlementContext { + quote_owed_from_lp: i64::MIN / 2, + quote_constituent_token_balance: u64::MAX / 2, + fee_pool_balance: u128::MAX / 4, + pnl_pool_balance: u128::MAX / 4, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert!(result.amount_transferred > 0); + } + + #[test] + fn test_exact_boundary_settlements() { + // Test when quote_owed exactly equals LP balance + let ctx = SettlementContext { + quote_owed_from_lp: 1000 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 1000 * QUOTE_PRECISION_U64, + fee_pool_balance: 500 * QUOTE_PRECISION, + pnl_pool_balance: 300 * QUOTE_PRECISION, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000 * QUOTE_PRECISION_U64, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!( + result.amount_transferred, + 1000 * QUOTE_PRECISION_U64 - QUOTE_PRECISION_U64 + ); // Leave QUOTE PRECISION + + // Test when negative quote_owed exactly equals total pool balance + let ctx = SettlementContext { + quote_owed_from_lp: -800, + quote_constituent_token_balance: 2000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 300); + } + + #[test] + fn test_minimal_settlement_amounts() { + // Test with minimal positive amount + let ctx = SettlementContext { + quote_owed_from_lp: 1, + quote_constituent_token_balance: 1, + fee_pool_balance: 1, + pnl_pool_balance: 1, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 0); // Cannot transfer if less than QUOTE_PRECISION + + // Test with minimal negative amount + let ctx = SettlementContext { + quote_owed_from_lp: -1, + quote_constituent_token_balance: 1, + fee_pool_balance: 1, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1); + assert_eq!(result.fee_pool_used, 1); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_all_zero_balances() { + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 0, + fee_pool_balance: 0, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 0); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_cache_info_update_none_direction() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 100, + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: 500, + last_settle_amount: 50, + last_settle_slot: 12345, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = 100; // No change + let ts = 67890; + let slot = 100000000; + + update_cache_info(&mut cache, &result, new_quote_owed, slot, ts).unwrap(); + + // quote_owed unchanged + assert_eq!(cache.quote_owed_from_lp_pool, 100); + // settle fields updated with new timestamp but zero amount + assert_eq!(cache.last_settle_amount, 0); + assert_eq!(cache.last_settle_slot, slot); + // pool amounts unchanged + assert_eq!(cache.last_fee_pool_token_amount, 1000); + assert_eq!(cache.last_net_pnl_pool_token_amount, 500); + } + + #[test] + fn test_cache_info_update_maximum_values() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: i64::MAX / 2, + last_fee_pool_token_amount: u128::MAX / 2, + last_net_pnl_pool_token_amount: i128::MAX / 2, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: u64::MAX / 4, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool - (result.amount_transferred as i64); + let slot = u64::MAX / 2; + let ts = i64::MAX / 2; + + let update_result = update_cache_info(&mut cache, &result, new_quote_owed, slot, ts); + assert!(update_result.is_ok()); + } + + #[test] + fn test_cache_info_update_minimum_values() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: i64::MIN / 2, + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: i128::MIN / 2, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 500, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 200, + pnl_pool_used: 300, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool + (result.amount_transferred as i64); + let slot = u64::MAX / 2; + let ts = 42; + + let update_result = update_cache_info(&mut cache, &result, new_quote_owed, slot, ts); + assert!(update_result.is_ok()); + } + + #[test] + fn test_sequential_settlement_updates() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 1000, + last_fee_pool_token_amount: 5000, + last_net_pnl_pool_token_amount: 3000, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + // First settlement: From LP pool + let result1 = SettlementResult { + amount_transferred: 300, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed1 = cache.quote_owed_from_lp_pool - (result1.amount_transferred as i64); + update_cache_info(&mut cache, &result1, new_quote_owed1, 101010101, 100).unwrap(); + + assert_eq!(cache.quote_owed_from_lp_pool, 700); + assert_eq!(cache.last_fee_pool_token_amount, 5300); + assert_eq!(cache.last_net_pnl_pool_token_amount, 3000); + + // Second settlement: To LP pool + let result2 = SettlementResult { + amount_transferred: 400, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 250, + pnl_pool_used: 150, + }; + let new_quote_owed2 = cache.quote_owed_from_lp_pool + (result2.amount_transferred as i64); + update_cache_info(&mut cache, &result2, new_quote_owed2, 10101010, 200).unwrap(); + + assert_eq!(cache.quote_owed_from_lp_pool, 1100); + assert_eq!(cache.last_fee_pool_token_amount, 5050); + assert_eq!(cache.last_net_pnl_pool_token_amount, 2850); + assert_eq!(cache.last_settle_slot, 10101010); + } + + #[test] + fn test_perp_to_lp_with_only_pnl_pool() { + let ctx = SettlementContext { + quote_owed_from_lp: -1000, + quote_constituent_token_balance: 2000, + fee_pool_balance: 0, // No fee pool + pnl_pool_balance: 1200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 1000); + } + + #[test] + fn test_perp_to_lp_capped_with_max() { + let ctx = SettlementContext { + quote_owed_from_lp: -1100, + quote_constituent_token_balance: 2000, + fee_pool_balance: 500, // No fee pool + pnl_pool_balance: 700, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 1000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 500); + } + + #[test] + fn test_lp_to_perp_capped_with_max() { + let ctx = SettlementContext { + quote_owed_from_lp: 1100 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 2000 * QUOTE_PRECISION_U64, + fee_pool_balance: 0, // No fee pool + pnl_pool_balance: 1200 * QUOTE_PRECISION, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 1000 * QUOTE_PRECISION_U64, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 1000 * QUOTE_PRECISION_U64); // Leave QUOTE PRECISION in the balance + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_perp_to_lp_with_only_fee_pool() { + let ctx = SettlementContext { + quote_owed_from_lp: -800, + quote_constituent_token_balance: 1500, + fee_pool_balance: 1000, + pnl_pool_balance: 0, // No PnL pool + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); + assert_eq!(result.fee_pool_used, 800); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_fractional_settlement_coverage() { + // Test when pools can only partially cover the needed amount + let ctx = SettlementContext { + quote_owed_from_lp: -2000, + quote_constituent_token_balance: 5000, + fee_pool_balance: 300, + pnl_pool_balance: 500, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); // Only what pools can provide + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 500); + } + + #[test] + fn test_settlement_direction_consistency() { + // Positive quote_owed should always result in FromLpPool or None + for quote_owed in [1, 100, 1000, 10000] { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: 500, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert!( + result.direction == SettlementDirection::FromLpPool + || result.direction == SettlementDirection::None + ); + } + + // Negative quote_owed should always result in ToLpPool or None + for quote_owed in [-1, -100, -1000, -10000] { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: 500, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert!( + result.direction == SettlementDirection::ToLpPool + || result.direction == SettlementDirection::None + ); + } + } + + #[test] + fn test_cache_info_timestamp_progression() { + let mut cache = CacheInfo::default(); + + let timestamps = [1000, 2000, 3000, 1500, 5000]; // Including out-of-order + + for (_, &ts) in timestamps.iter().enumerate() { + let result = SettlementResult { + amount_transferred: 100, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + + update_cache_info(&mut cache, &result, 0, 1010101, ts).unwrap(); + assert_eq!(cache.last_settle_ts, ts); + assert_eq!(cache.last_settle_amount, 100); + } + } + + #[test] + fn test_settlement_amount_conservation() { + // Test that fee_pool_used + pnl_pool_used = amount_transferred for ToLpPool + let test_cases = [ + (-500, 1000, 300, 400), // Normal case + (-1000, 2000, 600, 500), // Uses both pools + (-200, 500, 0, 300), // Only PnL pool + (-150, 400, 200, 0), // Only fee pool + ]; + + for (quote_owed, lp_balance, fee_pool, pnl_pool) in test_cases { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: lp_balance, + fee_pool_balance: fee_pool, + pnl_pool_balance: pnl_pool, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + + if result.direction == SettlementDirection::ToLpPool { + assert_eq!( + result.amount_transferred as u128, + result.fee_pool_used + result.pnl_pool_used, + "Amount transferred should equal sum of pool usage for case: {:?}", + (quote_owed, lp_balance, fee_pool, pnl_pool) + ); + } + } + } + + #[test] + fn test_cache_pool_balance_tracking() { + let mut cache = CacheInfo { + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: 500, + ..Default::default() + }; + + // Multiple settlements that should maintain balance consistency + let settlements = [ + (SettlementDirection::ToLpPool, 200, 120, 80), // Uses both pools + (SettlementDirection::FromLpPool, 150, 0, 0), // Adds to fee pool + (SettlementDirection::ToLpPool, 100, 100, 0), // Uses only fee pool + (SettlementDirection::ToLpPool, 50, 30, 20), // Uses both pools again + ]; + + let mut expected_fee_pool = cache.last_fee_pool_token_amount; + let mut expected_pnl_pool = cache.last_net_pnl_pool_token_amount; + + for (direction, amount, fee_used, pnl_used) in settlements { + let result = SettlementResult { + amount_transferred: amount, + direction, + fee_pool_used: fee_used, + pnl_pool_used: pnl_used, + }; + + match direction { + SettlementDirection::FromLpPool => { + expected_fee_pool += amount as u128; + } + SettlementDirection::ToLpPool => { + expected_fee_pool -= fee_used; + expected_pnl_pool -= pnl_used as i128; + } + SettlementDirection::None => {} + } + + update_cache_info(&mut cache, &result, 0, 1000, 0).unwrap(); + + assert_eq!(cache.last_fee_pool_token_amount, expected_fee_pool); + assert_eq!(cache.last_net_pnl_pool_token_amount, expected_pnl_pool); + } + } +} + +#[cfg(test)] +mod update_aum_tests { + use crate::{ + create_anchor_account_info, + math::constants::SPOT_CUMULATIVE_INTEREST_PRECISION, + math::constants::{PRICE_PRECISION_I64, QUOTE_PRECISION}, + state::amm_cache::{AmmCacheFixed, CacheInfo}, + state::lp_pool::*, + state::oracle::HistoricalOracleData, + state::oracle::OracleSource, + state::oracle_map::OracleMap, + state::pyth_lazer_oracle::PythLazerOracle, + state::spot_market::SpotMarket, + state::spot_market_map::SpotMarketMap, + state::zero_copy::AccountZeroCopyMut, + test_utils::{create_account_info, get_anchor_account_bytes}, + }; + use anchor_lang::prelude::Pubkey; + use std::{cell::RefCell, marker::PhantomData}; + + fn test_aum_with_balances( + usdc_balance: u64, // USDC balance in tokens (6 decimals) + sol_balance: u64, // SOL balance in tokens (9 decimals) + btc_balance: u64, // BTC balance in tokens (8 decimals) + bonk_balance: u64, // BONK balance in tokens (5 decimals) + expected_aum_usd: u64, + test_name: &str, + ) { + let mut lp_pool = LPPool::default(); + lp_pool.constituents = 4; + lp_pool.quote_consituent_index = 0; + + // Create constituents with specified token balances + let mut constituent_usdc = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 0, + constituent_index: 0, + last_oracle_price: PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 6, + vault_token_balance: usdc_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_usdc, Constituent, constituent_usdc_account_info); + + let mut constituent_sol = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 1, + constituent_index: 1, + last_oracle_price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + vault_token_balance: sol_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_sol, Constituent, constituent_sol_account_info); + + let mut constituent_btc = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 2, + constituent_index: 2, + last_oracle_price: 100_000 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 8, + vault_token_balance: btc_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_btc, Constituent, constituent_btc_account_info); + + let mut constituent_bonk = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 3, + constituent_index: 3, + last_oracle_price: 22, // $0.000022 in PRICE_PRECISION_I64 + last_oracle_slot: 100, + decimals: 5, + vault_token_balance: bonk_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_bonk, Constituent, constituent_bonk_account_info); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &constituent_usdc_account_info, + &constituent_sol_account_info, + &constituent_btc_account_info, + &constituent_bonk_account_info, + ], + true, + ) + .unwrap(); + + // Create simple PythLazer oracle accounts for non-quote assets with prices matching constituents + // Use exponent -6 so values are already in PRICE_PRECISION units + let sol_oracle_pubkey = Pubkey::new_unique(); + let mut sol_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + sol_oracle, + &sol_oracle_pubkey, + PythLazerOracle, + sol_oracle_account_info + ); + + let btc_oracle_pubkey = Pubkey::new_unique(); + let mut btc_oracle = PythLazerOracle { + price: 100_000 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + btc_oracle, + &btc_oracle_pubkey, + PythLazerOracle, + btc_oracle_account_info + ); + + let bonk_oracle_pubkey = Pubkey::new_unique(); + let mut bonk_oracle = PythLazerOracle { + price: 22, // $0.000022 in PRICE_PRECISION + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + bonk_oracle, + &bonk_oracle_pubkey, + PythLazerOracle, + bonk_oracle_account_info + ); + + // Create spot markets + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::PythLazer, + oracle: sol_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(200 * PRICE_PRECISION_I64), + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + + let mut btc_spot_market = SpotMarket { + market_index: 2, + oracle_source: OracleSource::PythLazer, + oracle: btc_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 8, + historical_oracle_data: HistoricalOracleData::default_price( + 100_000 * PRICE_PRECISION_I64, + ), + ..SpotMarket::default() + }; + create_anchor_account_info!(btc_spot_market, SpotMarket, btc_spot_market_account_info); + + let mut bonk_spot_market = SpotMarket { + market_index: 3, + oracle_source: OracleSource::PythLazer, + oracle: bonk_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 5, + historical_oracle_data: HistoricalOracleData::default_price(22), + ..SpotMarket::default() + }; + create_anchor_account_info!(bonk_spot_market, SpotMarket, bonk_spot_market_account_info); + + let spot_market_account_infos = vec![ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + &btc_spot_market_account_info, + &bonk_spot_market_account_info, + ]; + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + // Build an oracle map containing the three non-quote oracles + let oracle_accounts = vec![ + sol_oracle_account_info.clone(), + btc_oracle_account_info.clone(), + bonk_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + msg!( + "oracle map entry 0 {:?}", + oracle_map + .get_price_data(&sol_spot_market.oracle_id()) + .unwrap() + ); + + // Create constituent target base + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 4, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 128]); // 4 * 32 bytes per TargetsDatum + let constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Create AMM cache + let mut cache_fixed_default = AmmCacheFixed::default(); + cache_fixed_default.len = 0; // No perp markets for this test + let cache_fixed = RefCell::new(cache_fixed_default); + let cache_data = RefCell::new([0u8; 0]); // Empty cache data + let amm_cache = AccountZeroCopyMut::<'_, CacheInfo, AmmCacheFixed> { + fixed: cache_fixed.borrow_mut(), + data: cache_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Call update_aum + let result = lp_pool.update_aum( + 101, // slot + &constituent_map, + &spot_market_map, + &mut oracle_map, + &constituent_target_base, + &amm_cache, + ); + + assert!(result.is_ok(), "{}: update_aum should succeed", test_name); + let (aum, crypto_delta, derivative_groups) = result.unwrap(); + + // Convert expected USD to quote precision + let expected_aum = expected_aum_usd as u128 * QUOTE_PRECISION; + + println!( + "{}: AUM = ${}, Expected = ${}", + test_name, + aum / QUOTE_PRECISION, + expected_aum / QUOTE_PRECISION + ); + + // Verify the results (allow small rounding differences) + let aum_diff = if aum > expected_aum { + aum - expected_aum + } else { + expected_aum - aum + }; + assert!( + aum_diff <= QUOTE_PRECISION, // Allow up to $1 difference for rounding + "{}: AUM mismatch. Got: ${}, Expected: ${}, Diff: ${}", + test_name, + aum / QUOTE_PRECISION, + expected_aum / QUOTE_PRECISION, + aum_diff / QUOTE_PRECISION + ); + + assert_eq!(crypto_delta, 0, "{}: crypto_delta should be 0", test_name); + assert!( + derivative_groups.is_empty(), + "{}: derivative_groups should be empty", + test_name + ); + + // Verify LP pool state was updated + assert_eq!( + lp_pool.last_aum, aum, + "{}: last_aum should match calculated AUM", + test_name + ); + assert_eq!( + lp_pool.last_aum_slot, 101, + "{}: last_aum_slot should be updated", + test_name + ); + } + + #[test] + fn test_aum_zero() { + test_aum_with_balances( + 0, // 0 USDC + 0, // 0 SOL + 0, // 0 BTC + 0, // 0 BONK + 0, // $0 expected AUM + "Zero AUM", + ); + } + + #[test] + fn test_aum_low_1k() { + test_aum_with_balances( + 1_000_000_000, // 1,000 USDC (6 decimals) = $1,000 + 0, // 0 SOL + 0, // 0 BTC + 0, // 0 BONK + 1_000, // $1,000 expected AUM + "Low AUM (~$1k)", + ); + } + + #[test] + fn test_aum_reasonable() { + test_aum_with_balances( + 1_000_000_000_000, // 1M USDC (6 decimals) = $1M + 5_000_000_000_000, // 5k SOL (9 decimals) = $1M at $200/SOL + 800_000_000, // 8 BTC (8 decimals) = $800k at $100k/BTC + 0, // 0 BONK + 2_800_000, // Expected AUM based on actual calculation + "Reasonable AUM (~$2.8M)", + ); + } + + #[test] + fn test_aum_high() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 0, // 0 BONK + 210_000_000_000, // Expected AUM based on actual calculation + "High AUM (~$210b)", + ); + } + + #[test] + fn test_aum_with_small_bonk_balance() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 100_000_000_000_000, // 1B BONK (5 decimals) = $22k at $0.000022/BONK + 210_000_022_000, // Expected AUM based on actual calculation + "High AUM (~$210b) with BONK", + ); + } + + #[test] + fn test_aum_with_large_bonk_balance() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 100_000_000_000_000_000, // 1T BONK (5 decimals) = $22M at $0.000022/BONK + 210_022_000_000, // Expected AUM based on actual calculation + "High AUM (~$210b) with BONK", + ); + } +} + +#[cfg(test)] +mod update_constituent_target_base_for_derivatives_tests { + use super::super::update_constituent_target_base_for_derivatives; + use crate::create_anchor_account_info; + use crate::math::constants::{ + PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, QUOTE_PRECISION, + SPOT_CUMULATIVE_INTEREST_PRECISION, + }; + use crate::state::constituent_map::ConstituentMap; + use crate::state::lp_pool::{Constituent, ConstituentTargetBaseFixed, TargetsDatum}; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::pyth_lazer_oracle::PythLazerOracle; + use crate::state::spot_market::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::zero_copy::AccountZeroCopyMut; + use crate::test_utils::{create_account_info, get_anchor_account_bytes}; + use anchor_lang::prelude::Pubkey; + use anchor_lang::Owner; + use std::collections::BTreeMap; + use std::{cell::RefCell, marker::PhantomData}; + + fn test_derivative_weights_scenario( + derivative_weights: Vec, + test_name: &str, + should_succeed: bool, + ) { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + // Create parent constituent (SOL) - parent_index must not be 0 + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 SOL + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, // Parent index + derivative_weight: 0, // Parent doesn't have derivative weight + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // Create first derivative constituent + let derivative1_index = parent_index + 1; // 2 + let mut derivative1_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative1_index, + constituent_index: derivative1_index, + last_oracle_price: 195 * PRICE_PRECISION_I64, // $195 (slightly below parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(0).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative1_constituent, + Constituent, + derivative1_constituent_account_info + ); + + // Create second derivative constituent + let derivative2_index = parent_index + 2; // 3 + let mut derivative2_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative2_index, + constituent_index: derivative2_index, + last_oracle_price: 205 * PRICE_PRECISION_I64, // $205 (slightly above parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(1).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative2_constituent, + Constituent, + derivative2_constituent_account_info + ); + + // Create third derivative constituent + let derivative3_index = parent_index + 3; // 4 + let mut derivative3_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative3_index, + constituent_index: derivative3_index, + last_oracle_price: 210 * PRICE_PRECISION_I64, // $210 (slightly above parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(2).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative3_constituent, + Constituent, + derivative3_constituent_account_info + ); + + let constituents_list = vec![ + &parent_constituent_account_info, + &derivative1_constituent_account_info, + &derivative2_constituent_account_info, + &derivative3_constituent_account_info, + ]; + let constituent_map = ConstituentMap::load_multiple(constituents_list, true).unwrap(); + + // Create oracles for parent and derivatives, with prices matching their last_oracle_price + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative1_oracle_pubkey = Pubkey::new_unique(); + let mut derivative1_oracle = PythLazerOracle { + price: 195 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative1_oracle, + &derivative1_oracle_pubkey, + PythLazerOracle, + derivative1_oracle_account_info + ); + + let derivative2_oracle_pubkey = Pubkey::new_unique(); + let mut derivative2_oracle = PythLazerOracle { + price: 205 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative2_oracle, + &derivative2_oracle_pubkey, + PythLazerOracle, + derivative2_oracle_account_info + ); + + let derivative3_oracle_pubkey = Pubkey::new_unique(); + let mut derivative3_oracle = PythLazerOracle { + price: 210 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative3_oracle, + &derivative3_oracle_pubkey, + PythLazerOracle, + derivative3_oracle_account_info + ); + + // Create spot markets bound to the above oracles + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative1_spot_market = SpotMarket { + market_index: derivative1_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative1_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative1_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative1_spot_market, + SpotMarket, + derivative1_spot_market_account_info + ); + + let mut derivative2_spot_market = SpotMarket { + market_index: derivative2_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative2_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative2_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative2_spot_market, + SpotMarket, + derivative2_spot_market_account_info + ); + + let mut derivative3_spot_market = SpotMarket { + market_index: derivative3_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative3_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative3_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative3_spot_market, + SpotMarket, + derivative3_spot_market_account_info + ); + + let spot_market_list = vec![ + &parent_spot_market_account_info, + &derivative1_spot_market_account_info, + &derivative2_spot_market_account_info, + &derivative3_spot_market_account_info, + ]; + let spot_market_map = SpotMarketMap::load_multiple(spot_market_list, true).unwrap(); + + // Build an oracle map for parent and derivatives + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative1_oracle_account_info.clone(), + derivative2_oracle_account_info.clone(), + derivative3_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + // Create constituent target base + let num_constituents = 4; // Fixed: parent + 3 derivatives + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: num_constituents as u32, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 5 * 32]); // 4+1 constituents * 32 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set initial parent target base (targeting 10% of total AUM worth of SOL tokens) + // For 10M AUM and $200 SOL price with 9 decimals: (10M * 0.1) / 200 * 10^9 = 5,000,000,000,000 tokens + let initial_parent_target_base = 5_000_000_000_000i64; // ~$1M worth of SOL tokens + constituent_target_base + .get_mut(parent_index as u32) + .target_base = initial_parent_target_base; + constituent_target_base + .get_mut(parent_index as u32) + .last_oracle_slot = 100; + + // Initialize derivative target bases to 0 + constituent_target_base + .get_mut(derivative1_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative1_index as u32) + .last_oracle_slot = 100; + constituent_target_base + .get_mut(derivative2_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative2_index as u32) + .last_oracle_slot = 100; + constituent_target_base + .get_mut(derivative3_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative3_index as u32) + .last_oracle_slot = 100; + + // Create derivative groups + let mut derivative_groups = BTreeMap::new(); + let mut active_derivatives = Vec::new(); + for (i, _) in derivative_weights.iter().enumerate() { + // Add all derivatives regardless of weight (they may have zero weight for testing) + let derivative_index = match i { + 0 => derivative1_index, + 1 => derivative2_index, + 2 => derivative3_index, + _ => continue, + }; + active_derivatives.push(derivative_index); + } + if !active_derivatives.is_empty() { + derivative_groups.insert(parent_index, active_derivatives); + } + + // Call the function + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok() == should_succeed, + "{}: update_constituent_target_base_for_derivatives should succeed", + test_name + ); + + if !should_succeed { + return; + } + + // Verify results + let parent_target_base_after = constituent_target_base.get(parent_index as u32).target_base; + let total_derivative_weight: u64 = derivative_weights.iter().sum(); + let remaining_parent_weight = PERCENTAGE_PRECISION_U64 - total_derivative_weight; + + // Expected parent target base after scaling down + let expected_parent_target_base = initial_parent_target_base + * (remaining_parent_weight as i64) + / (PERCENTAGE_PRECISION_I64); + + println!( + "{}: Original parent target base: {}, After: {}, Expected: {}", + test_name, + initial_parent_target_base, + parent_target_base_after, + expected_parent_target_base + ); + + assert_eq!( + parent_target_base_after, expected_parent_target_base, + "{}: Parent target base should be scaled down correctly", + test_name + ); + + // Verify derivative target bases + for (i, derivative_weight) in derivative_weights.iter().enumerate() { + let derivative_index = match i { + 0 => derivative1_index, + 1 => derivative2_index, + 2 => derivative3_index, + _ => continue, + }; + + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + + if *derivative_weight == 0 { + // If derivative weight is 0, target base should remain 0 + assert_eq!( + derivative_target_base, 0, + "{}: Derivative {} with zero weight should have target base 0", + test_name, derivative_index + ); + continue; + } + + // For simplicity, just verify that the derivative target base is positive and reasonable + // The exact calculation is complex and depends on the internal implementation + println!( + "{}: Derivative {} target base: {}, Weight: {}", + test_name, derivative_index, derivative_target_base, derivative_weight + ); + + assert!( + derivative_target_base > 0, + "{}: Derivative {} target base should be positive", + test_name, + derivative_index + ); + + // Verify that target base is reasonable (not too large or too small) + assert!( + derivative_target_base < 10_000_000_000_000i64, + "{}: Derivative {} target base should be reasonable", + test_name, + derivative_index + ); + } + } + + fn test_depeg_scenario() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + // Create parent constituent (SOL) - parent_index must not be 0 + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 SOL + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, // Parent index + derivative_weight: 0, + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // Create derivative constituent that's depegged - must have different index than parent + let derivative_index = parent_index + 1; // 2 + let mut derivative_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative_index, + constituent_index: derivative_index, + last_oracle_price: 180 * PRICE_PRECISION_I64, // $180 (below 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 500_000, // 50% weight + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative_constituent, + Constituent, + derivative_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative_constituent_account_info, + ], + true, + ) + .unwrap(); + + // Create PythLazer oracles corresponding to prices + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative_oracle_pubkey = Pubkey::new_unique(); + let mut derivative_oracle = PythLazerOracle { + price: 180 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative_oracle, + &derivative_oracle_pubkey, + PythLazerOracle, + derivative_oracle_account_info + ); + + // Create spot markets + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative_spot_market = SpotMarket { + market_index: derivative_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative_spot_market, + SpotMarket, + derivative_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative_spot_market_account_info, + ], + true, + ) + .unwrap(); + + // Build oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + // Create constituent target base + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 2, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 3 * 32]); // 2+1 constituents * 24 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set initial values + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 2_500_000_000_000i64; // ~$500k worth of SOL + constituent_target_base + .get_mut(derivative_index as u32) + .target_base = 1_250_000_000_000i64; // ~$250k worth + + // Create derivative groups + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative_index]); + + // Call the function + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "depeg scenario: update_constituent_target_base_for_derivatives should succeed" + ); + + // Verify that depegged derivative has target base set to 0 + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + assert_eq!( + derivative_target_base, 0, + "depeg scenario: Depegged derivative should have target base 0" + ); + + // Verify that parent target base is unchanged since derivative weight is 0 now + let parent_target_base = constituent_target_base.get(parent_index as u32).target_base; + assert_eq!( + parent_target_base, 2_500_000_000_000i64, + "depeg scenario: Parent target base should remain unchanged" + ); + } + + #[test] + fn test_derivative_depeg_scenario() { + // Test case: Test depeg scenario + test_depeg_scenario(); + } + + #[test] + fn test_derivative_weights_sum_to_110_percent() { + // Test case: Derivative constituents with weights that sum to 1.1 (110%) + test_derivative_weights_scenario( + vec![ + 500_000, // 50% weight + 300_000, // 30% weight + 300_000, // 30% weight + ], + "weights sum to 110%", + false, + ); + } + + #[test] + fn test_derivative_weights_sum_to_100_percent() { + // Test case: Derivative constituents with weights that sum to 1 (100%) + test_derivative_weights_scenario( + vec![ + 500_000, // 50% weight + 300_000, // 30% weight + 200_000, // 20% weight + ], + "weights sum to 100%", + true, + ); + } + + #[test] + fn test_derivative_weights_sum_to_75_percent() { + // Test case: Derivative constituents with weights that sum to < 1 (75%) + test_derivative_weights_scenario( + vec![ + 400_000, // 40% weight + 200_000, // 20% weight + 150_000, // 15% weight + ], + "weights sum to 75%", + true, + ); + } + + #[test] + fn test_single_derivative_60_percent_weight() { + // Test case: Single derivative with partial weight + test_derivative_weights_scenario( + vec![ + 600_000, // 60% weight + ], + "single derivative 60% weight", + true, + ); + } + + #[test] + fn test_single_derivative_100_percent_weight() { + // Test case: Single derivative with 100% weight - parent should become 0 + test_derivative_weights_scenario( + vec![ + 1_000_000, // 100% weight + ], + "single derivative 100% weight", + true, + ); + } + + #[test] + fn test_mixed_zero_and_nonzero_weights() { + // Test case: Mix of zero and non-zero weights + test_derivative_weights_scenario( + vec![ + 0, // 0% weight + 400_000, // 40% weight + 0, // 0% weight + ], + "mixed zero and non-zero weights", + true, + ); + } + + #[test] + fn test_very_small_weights() { + // Test case: Very small weights (1 basis point = 0.01%) + test_derivative_weights_scenario( + vec![ + 100, // 0.01% weight + 200, // 0.02% weight + 300, // 0.03% weight + ], + "very small weights", + true, + ); + } + + #[test] + fn test_zero_parent_target_base() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, + derivative_weight: 0, + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + let derivative_index = parent_index + 1; + let mut derivative_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative_index, + constituent_index: derivative_index, + last_oracle_price: 195 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 500_000, // 50% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative_constituent, + Constituent, + derivative_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative_constituent_account_info, + ], + true, + ) + .unwrap(); + + // Create PythLazer oracles so update_constituent_target_base_for_derivatives can fetch current prices + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative_oracle_pubkey = Pubkey::new_unique(); + let mut derivative_oracle = PythLazerOracle { + price: 195 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative_oracle, + &derivative_oracle_pubkey, + PythLazerOracle, + derivative_oracle_account_info + ); + + // Spot markets bound to the test oracles + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative_spot_market = SpotMarket { + market_index: derivative_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative_spot_market, + SpotMarket, + derivative_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative_spot_market_account_info, + ], + true, + ) + .unwrap(); + + // Build oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 2, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 3 * 32]); + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set parent target base to 0 + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 0i64; + constituent_target_base + .get_mut(derivative_index as u32) + .target_base = 0i64; + + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative_index]); + + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "zero parent target base scenario should succeed" + ); + + // With zero parent target base, derivative should also be 0 + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + assert_eq!( + derivative_target_base, 0, + "zero parent target base: derivative target base should be 0" + ); + } + + #[test] + fn test_mixed_depegged_and_valid_derivatives() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, + derivative_weight: 0, + constituent_derivative_depeg_threshold: 949_999, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // First derivative - depegged + let derivative1_index = parent_index + 1; + let mut derivative1_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative1_index, + constituent_index: derivative1_index, + last_oracle_price: 180 * PRICE_PRECISION_I64, // $180 (below 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 300_000, // 30% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative1_constituent, + Constituent, + derivative1_constituent_account_info + ); + + // Second derivative - valid + let derivative2_index = parent_index + 2; + let mut derivative2_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative2_index, + constituent_index: derivative2_index, + last_oracle_price: 198 * PRICE_PRECISION_I64, // $198 (above 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 400_000, // 40% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative2_constituent, + Constituent, + derivative2_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative1_constituent_account_info, + &derivative2_constituent_account_info, + ], + true, + ) + .unwrap(); + + // Oracles + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + let derivative1_oracle_pubkey = Pubkey::new_unique(); + let mut derivative1_oracle = PythLazerOracle { + price: 180 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative1_oracle, + &derivative1_oracle_pubkey, + PythLazerOracle, + derivative1_oracle_account_info + ); + let derivative2_oracle_pubkey = Pubkey::new_unique(); + let mut derivative2_oracle = PythLazerOracle { + price: 198 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative2_oracle, + &derivative2_oracle_pubkey, + PythLazerOracle, + derivative2_oracle_account_info + ); + + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative1_spot_market = SpotMarket { + market_index: derivative1_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative1_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative1_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative1_spot_market, + SpotMarket, + derivative1_spot_market_account_info + ); + + let mut derivative2_spot_market = SpotMarket { + market_index: derivative2_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative2_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative2_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative2_spot_market, + SpotMarket, + derivative2_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative1_spot_market_account_info, + &derivative2_spot_market_account_info, + ], + true, + ) + .unwrap(); + + // Oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative1_oracle_account_info.clone(), + derivative2_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 3, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 4 * 32]); + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 5_000_000_000_000i64; + constituent_target_base + .get_mut(derivative1_index as u32) + .target_base = 0i64; + constituent_target_base + .get_mut(derivative2_index as u32) + .target_base = 0i64; + + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative1_index, derivative2_index]); + + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "mixed depegged and valid derivatives scenario should succeed" + ); + + // First derivative should be depegged (target base = 0) + let derivative1_target_base = constituent_target_base + .get(derivative1_index as u32) + .target_base; + assert_eq!( + derivative1_target_base, 0, + "mixed scenario: depegged derivative should have target base 0" + ); + + // Second derivative should have positive target base + let derivative2_target_base = constituent_target_base + .get(derivative2_index as u32) + .target_base; + assert!( + derivative2_target_base > 0, + "mixed scenario: valid derivative should have positive target base" + ); + + // Parent should be scaled down by only the valid derivative's weight (40%) + let parent_target_base = constituent_target_base.get(parent_index as u32).target_base; + let expected_parent_target_base = 5_000_000_000_000i64 * (1_000_000 - 400_000) / 1_000_000; + assert_eq!( + parent_target_base, expected_parent_target_base, + "mixed scenario: parent should be scaled by valid derivative weight only" + ); + } +} diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 4a0c299e4e..cf2a39ac29 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -1,5 +1,8 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; +use crate::math::constants::{ + AMM_RESERVE_PRECISION_I128, MARGIN_PRECISION_I128, MARGIN_PRECISION_U128, +}; use crate::math::fuel::{calculate_perp_fuel_bonus, calculate_spot_fuel_bonus}; use crate::math::margin::MarginRequirementType; use crate::math::safe_math::SafeMath; @@ -8,9 +11,7 @@ use crate::state::oracle::StrictOraclePrice; use crate::state::perp_market::PerpMarket; use crate::state::spot_market::SpotMarket; use crate::state::user::{PerpPosition, User}; -use crate::{ - validate, MarketType, AMM_RESERVE_PRECISION_I128, MARGIN_PRECISION_I128, MARGIN_PRECISION_U128, -}; +use crate::{validate, MarketType}; use anchor_lang::{prelude::*, solana_program::msg}; #[derive(Clone, Copy, Debug)] diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index 9237c008cd..db5c115036 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -1,3 +1,5 @@ +pub mod amm_cache; +pub mod constituent_map; pub mod events; pub mod fill_mode; pub mod fulfillment; @@ -6,6 +8,7 @@ pub mod high_leverage_mode_config; pub mod if_rebalance_config; pub mod insurance_fund_stake; pub mod load_ref; +pub mod lp_pool; pub mod margin_calculation; pub mod oracle; pub mod oracle_map; @@ -27,3 +30,4 @@ pub mod state; pub mod traits; pub mod user; pub mod user_map; +pub mod zero_copy; diff --git a/programs/drift/src/state/oracle.rs b/programs/drift/src/state/oracle.rs index 3becf945f6..0275a5549a 100644 --- a/programs/drift/src/state/oracle.rs +++ b/programs/drift/src/state/oracle.rs @@ -1,5 +1,6 @@ use anchor_lang::prelude::*; use std::cell::Ref; +use std::convert::TryFrom; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; @@ -172,6 +173,55 @@ impl OracleSource { } } +impl TryFrom for OracleSource { + type Error = ErrorCode; + + fn try_from(v: u8) -> DriftResult { + match v { + 0 => Ok(OracleSource::Pyth), + 1 => Ok(OracleSource::Switchboard), + 2 => Ok(OracleSource::QuoteAsset), + 3 => Ok(OracleSource::Pyth1K), + 4 => Ok(OracleSource::Pyth1M), + 5 => Ok(OracleSource::PythStableCoin), + 6 => Ok(OracleSource::Prelaunch), + 7 => Ok(OracleSource::PythPull), + 8 => Ok(OracleSource::Pyth1KPull), + 9 => Ok(OracleSource::Pyth1MPull), + 10 => Ok(OracleSource::PythStableCoinPull), + 11 => Ok(OracleSource::SwitchboardOnDemand), + 12 => Ok(OracleSource::PythLazer), + 13 => Ok(OracleSource::PythLazer1K), + 14 => Ok(OracleSource::PythLazer1M), + 15 => Ok(OracleSource::PythLazerStableCoin), + _ => Err(ErrorCode::InvalidOracle), + } + } +} + +impl From for u8 { + fn from(src: OracleSource) -> u8 { + match src { + OracleSource::Pyth => 0, + OracleSource::Switchboard => 1, + OracleSource::QuoteAsset => 2, + OracleSource::Pyth1K => 3, + OracleSource::Pyth1M => 4, + OracleSource::PythStableCoin => 5, + OracleSource::Prelaunch => 6, + OracleSource::PythPull => 7, + OracleSource::Pyth1KPull => 8, + OracleSource::Pyth1MPull => 9, + OracleSource::PythStableCoinPull => 10, + OracleSource::SwitchboardOnDemand => 11, + OracleSource::PythLazer => 12, + OracleSource::PythLazer1K => 13, + OracleSource::PythLazer1M => 14, + OracleSource::PythLazerStableCoin => 15, + } + } +} + const MM_EXCHANGE_FALLBACK_THRESHOLD: u128 = PERCENTAGE_PRECISION / 100; // 1% #[derive(Default, Clone, Copy, Debug)] pub struct MMOraclePriceData { diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 95ccf8424d..422857fed2 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -1,15 +1,15 @@ use crate::controller::position::PositionDirection; use crate::error::DriftResult; use crate::math::casting::Cast; +use crate::math::constants::{ + MAX_PREDICTION_MARKET_PRICE_I64, ONE_HUNDRED_THOUSAND_QUOTE, PERCENTAGE_PRECISION_I64, + PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, +}; use crate::math::safe_math::SafeMath; use crate::math::safe_unwrap::SafeUnwrap; use crate::state::events::OrderActionExplanation; use crate::state::perp_market::{ContractTier, PerpMarket}; use crate::state::user::{MarketType, OrderTriggerCondition, OrderType}; -use crate::{ - MAX_PREDICTION_MARKET_PRICE_I64, ONE_HUNDRED_THOUSAND_QUOTE, PERCENTAGE_PRECISION_I64, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, -}; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use std::ops::Div; diff --git a/programs/drift/src/state/paused_operations.rs b/programs/drift/src/state/paused_operations.rs index 81a6ec2a3a..516470460a 100644 --- a/programs/drift/src/state/paused_operations.rs +++ b/programs/drift/src/state/paused_operations.rs @@ -97,3 +97,55 @@ impl InsuranceFundOperation { } } } + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum PerpLpOperation { + TrackAmmRevenue = 0b00000001, + SettleQuoteOwed = 0b00000010, +} + +const ALL_PERP_LP_OPERATIONS: [PerpLpOperation; 2] = [ + PerpLpOperation::TrackAmmRevenue, + PerpLpOperation::SettleQuoteOwed, +]; + +impl PerpLpOperation { + pub fn is_operation_paused(current: u8, operation: PerpLpOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_PERP_LP_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum ConstituentLpOperation { + Swap = 0b00000001, + Deposit = 0b00000010, + Withdraw = 0b00000100, +} + +const ALL_CONSTITUENT_LP_OPERATIONS: [ConstituentLpOperation; 3] = [ + ConstituentLpOperation::Swap, + ConstituentLpOperation::Deposit, + ConstituentLpOperation::Withdraw, +]; + +impl ConstituentLpOperation { + pub fn is_operation_paused(current: u8, operation: ConstituentLpOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_CONSTITUENT_LP_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 15d6462c83..5a6b441c1c 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -9,12 +9,10 @@ use std::cmp::max; use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; -use crate::math::amm; +use crate::math::amm::{self}; use crate::math::casting::Cast; #[cfg(test)] -use crate::math::constants::{ - AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64, -}; +use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT}; use crate::math::constants::{ AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, @@ -24,7 +22,6 @@ use crate::math::constants::{ PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, }; -use crate::math::helpers::get_proportion_u128; use crate::math::margin::{ calc_high_leverage_mode_initial_margin_ratio_from_size, calculate_size_discount_asset_weight, calculate_size_premium_liability_weight, MarginRequirementType, @@ -94,6 +91,23 @@ impl MarketStatus { } } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] +pub enum LpStatus { + /// Not considered + #[default] + Uncollateralized, + /// all operations allowed + Active, + /// Decommissioning + Decommissioning, +} + +impl LpStatus { + pub fn is_collateralized(&self) -> bool { + !matches!(self, LpStatus::Uncollateralized) + } +} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] pub enum ContractType { #[default] @@ -232,9 +246,13 @@ pub struct PerpMarket { pub high_leverage_margin_ratio_maintenance: u16, pub protected_maker_limit_price_divisor: u8, pub protected_maker_dynamic_divisor: u8, - pub padding1: u32, + pub lp_fee_transfer_scalar: u8, + pub lp_status: u8, + pub lp_paused_operations: u8, + pub lp_exchange_fee_excluscion_scalar: u8, pub last_fill_price: u64, - pub padding: [u8; 24], + pub lp_pool_id: u8, + pub padding: [u8; 23], } impl Default for PerpMarket { @@ -276,9 +294,13 @@ impl Default for PerpMarket { high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding1: 0, + lp_fee_transfer_scalar: 0, + lp_status: 0, + lp_exchange_fee_excluscion_scalar: 0, + lp_paused_operations: 0, last_fill_price: 0, - padding: [0; 24], + lp_pool_id: 0, + padding: [0; 23], } } } @@ -1728,6 +1750,8 @@ impl AMM { #[cfg(test)] impl AMM { pub fn default_test() -> Self { + use crate::math::constants::PRICE_PRECISION_I64; + let default_reserves = 100 * AMM_RESERVE_PRECISION; // make sure tests dont have the default sqrt_k = 0 AMM { @@ -1753,6 +1777,8 @@ impl AMM { } pub fn default_btc_test() -> Self { + use crate::math::constants::PRICE_PRECISION_I64; + AMM { base_asset_reserve: 65 * AMM_RESERVE_PRECISION, quote_asset_reserve: 63015384615, diff --git a/programs/drift/src/state/perp_market/tests.rs b/programs/drift/src/state/perp_market/tests.rs index 24e34a9e14..3f09e5c58c 100644 --- a/programs/drift/src/state/perp_market/tests.rs +++ b/programs/drift/src/state/perp_market/tests.rs @@ -460,7 +460,7 @@ mod amm_can_fill_order_tests { market.paused_operations = PerpOperation::AmmFill as u8; let order = base_order(); let state = base_state(); - let (mm, guard) = mm_oracle_ok_and_as_recent(); + let (mm, _) = mm_oracle_ok_and_as_recent(); let can = market .amm_can_fill_order( diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 8891e1a99b..c42000a7b1 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -9,7 +9,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::constants::{ - AMM_RESERVE_PRECISION, FIVE_MINUTE, MARGIN_PRECISION, ONE_HOUR, SPOT_WEIGHT_PRECISION_U128, + AMM_RESERVE_PRECISION, FIVE_MINUTE, MARGIN_PRECISION, ONE_HOUR, PERCENTAGE_PRECISION, + SPOT_WEIGHT_PRECISION_U128, }; #[cfg(test)] use crate::math::constants::{PRICE_PRECISION_I64, SPOT_CUMULATIVE_INTEREST_PRECISION}; @@ -25,7 +26,7 @@ use crate::state::oracle::{HistoricalIndexData, HistoricalOracleData, OracleSour use crate::state::paused_operations::{InsuranceFundOperation, SpotOperation}; use crate::state::perp_market::{MarketStatus, PoolBalance}; use crate::state::traits::{MarketIndexOffset, Size}; -use crate::{validate, PERCENTAGE_PRECISION}; +use crate::validate; use super::oracle_map::OracleIdentifier; diff --git a/programs/drift/src/state/state.rs b/programs/drift/src/state/state.rs index 331a7ffa38..07e9213396 100644 --- a/programs/drift/src/state/state.rs +++ b/programs/drift/src/state/state.rs @@ -3,12 +3,12 @@ use enumflags2::BitFlags; use crate::error::DriftResult; use crate::math::constants::{ - FEE_DENOMINATOR, FEE_PERCENTAGE_DENOMINATOR, MAX_REFERRER_REWARD_EPOCH_UPPER_BOUND, + FEE_DENOMINATOR, FEE_PERCENTAGE_DENOMINATOR, LAMPORTS_PER_SOL_U64, + MAX_REFERRER_REWARD_EPOCH_UPPER_BOUND, PERCENTAGE_PRECISION_U64, }; use crate::math::safe_math::SafeMath; use crate::math::safe_unwrap::SafeUnwrap; use crate::state::traits::Size; -use crate::{LAMPORTS_PER_SOL_U64, PERCENTAGE_PRECISION_U64}; #[cfg(test)] mod tests; @@ -42,7 +42,8 @@ pub struct State { pub max_number_of_sub_accounts: u16, pub max_initialize_user_fee: u16, pub feature_bit_flags: u8, - pub padding: [u8; 9], + pub lp_pool_feature_bit_flags: u8, + pub padding: [u8; 8], } #[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)] @@ -128,6 +129,18 @@ impl State { pub fn builder_referral_enabled(&self) -> bool { (self.feature_bit_flags & (FeatureBitFlags::BuilderReferral as u8)) > 0 } + + pub fn allow_settle_lp_pool(&self) -> bool { + (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::SettleLpPool as u8)) > 0 + } + + pub fn allow_swap_lp_pool(&self) -> bool { + (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::SwapLpPool as u8)) > 0 + } + + pub fn allow_mint_redeem_lp_pool(&self) -> bool { + (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::MintRedeemLpPool as u8)) > 0 + } } #[derive(Clone, Copy, PartialEq, Debug, Eq)] @@ -138,6 +151,13 @@ pub enum FeatureBitFlags { BuilderReferral = 0b00001000, } +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum LpPoolFeatureBitFlags { + SettleLpPool = 0b00000001, + SwapLpPool = 0b00000010, + MintRedeemLpPool = 0b00000100, +} + impl Size for State { const SIZE: usize = 992; } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index ef702e90e1..a89467cce3 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -3,8 +3,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::auction::{calculate_auction_price, is_auction_complete}; use crate::math::casting::Cast; use crate::math::constants::{ - EPOCH_DURATION, FUEL_OVERFLOW_THRESHOLD_U32, FUEL_START_TS, OPEN_ORDER_MARGIN_REQUIREMENT, - QUOTE_SPOT_MARKET_INDEX, THIRTY_DAY, + EPOCH_DURATION, FUEL_OVERFLOW_THRESHOLD_U32, FUEL_START_TS, MAX_PREDICTION_MARKET_PRICE, + OPEN_ORDER_MARGIN_REQUIREMENT, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, + SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_I128, THIRTY_DAY, }; use crate::math::margin::MarginRequirementType; use crate::math::orders::{ @@ -18,15 +19,15 @@ use crate::math::spot_balance::{ get_signed_token_amount, get_strict_token_value, get_token_amount, get_token_value, }; use crate::math::stats::calculate_rolling_sum; +use crate::math_error; use crate::msg; +use crate::safe_increment; use crate::state::oracle::StrictOraclePrice; use crate::state::perp_market::ContractType; use crate::state::spot_market::{SpotBalance, SpotBalanceType, SpotMarket}; use crate::state::traits::Size; -use crate::{get_then_update_id, ID, QUOTE_PRECISION_U64}; -use crate::{math_error, SPOT_WEIGHT_PRECISION_I128}; -use crate::{safe_increment, SPOT_WEIGHT_PRECISION}; -use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; +use crate::validate; +use crate::{get_then_update_id, ID}; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use bytemuck::{Pod, Zeroable}; diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 5bd2b154f5..4b2392d1ba 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2319,9 +2319,7 @@ mod update_referrer_status { } mod update_open_bids_and_asks { - use crate::state::user::{ - Order, OrderBitFlag, OrderTriggerCondition, OrderType, PositionDirection, - }; + use crate::state::user::{Order, OrderBitFlag, OrderTriggerCondition, OrderType}; #[test] fn test_regular_limit_order() { diff --git a/programs/drift/src/state/zero_copy.rs b/programs/drift/src/state/zero_copy.rs new file mode 100644 index 0000000000..b6a11a383f --- /dev/null +++ b/programs/drift/src/state/zero_copy.rs @@ -0,0 +1,181 @@ +use crate::error::ErrorCode; +use crate::math::safe_unwrap::SafeUnwrap; +use anchor_lang::prelude::{AccountInfo, Pubkey}; +use bytemuck::{from_bytes, from_bytes_mut}; +use bytemuck::{Pod, Zeroable}; +use std::cell::{Ref, RefMut}; +use std::marker::PhantomData; + +use crate::error::DriftResult; +use crate::msg; +use crate::validate; + +pub trait HasLen { + fn len(&self) -> u32; +} + +pub struct AccountZeroCopy<'a, T, F> { + pub fixed: Ref<'a, F>, + pub data: Ref<'a, [u8]>, + pub _marker: PhantomData, +} + +impl<'a, T, F> AccountZeroCopy<'a, T, F> +where + T: Pod + Zeroable + Clone + Copy, + F: Pod + HasLen, +{ + pub fn len(&self) -> u32 { + self.fixed.len() + } + + pub fn get(&self, index: u32) -> &T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes(&self.data[start..start + size]) + } + + pub fn iter(&self) -> impl Iterator + '_ { + (0..self.len()).map(move |i| self.get(i)) + } +} + +pub struct AccountZeroCopyMut<'a, T, F> { + pub fixed: RefMut<'a, F>, + pub data: RefMut<'a, [u8]>, + pub _marker: PhantomData, +} + +impl<'a, T, F> AccountZeroCopyMut<'a, T, F> +where + T: Pod + Zeroable + Clone + Copy, + F: Pod + HasLen, +{ + pub fn len(&self) -> u32 { + self.fixed.len() + } + + pub fn get_mut(&mut self, index: u32) -> &mut T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes_mut(&mut self.data[start..start + size]) + } + + pub fn get(&self, index: u32) -> &T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes(&self.data[start..start + size]) + } + + pub fn iter(&self) -> impl Iterator + '_ { + (0..self.len()).map(move |i| self.get(i)) + } +} + +pub trait ZeroCopyLoader<'a, T, F> { + fn load_zc(&'a self) -> DriftResult>; + fn load_zc_mut(&'a self) -> DriftResult>; +} + +pub fn load_generic<'a, 'info, F, T>( + acct: &'a AccountInfo<'info>, + expected_disc: [u8; 8], + program_id: Pubkey, +) -> DriftResult> +where + F: Pod + HasLen, + T: Pod, +{ + validate!( + acct.owner == &program_id, + ErrorCode::DefaultError, + "invalid owner {}, program_id: {}", + acct.owner, + program_id, + )?; + + let data = acct.try_borrow_data().safe_unwrap()?; + let (disc, rest) = Ref::map_split(data, |d| d.split_at(8)); + + validate!( + *disc == expected_disc, + ErrorCode::DefaultError, + "invalid discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (hdr_bytes, body) = Ref::map_split(rest, |d| d.split_at(hdr_size)); + let fixed = Ref::map(hdr_bytes, |b| from_bytes::(b)); + Ok(AccountZeroCopy { + fixed, + data: body, + _marker: PhantomData, + }) +} + +pub fn load_generic_mut<'a, 'info, F, T>( + acct: &'a AccountInfo<'info>, + expected_disc: [u8; 8], + program_id: Pubkey, +) -> DriftResult> +where + F: Pod + HasLen, + T: Pod, +{ + validate!( + acct.owner == &program_id, + ErrorCode::DefaultError, + "invalid owner", + )?; + + let data = acct.try_borrow_mut_data().safe_unwrap()?; + let (disc, rest) = RefMut::map_split(data, |d| d.split_at_mut(8)); + + validate!( + *disc == expected_disc, + ErrorCode::DefaultError, + "invalid discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (hdr_bytes, body) = RefMut::map_split(rest, |d| d.split_at_mut(hdr_size)); + let fixed = RefMut::map(hdr_bytes, |b| from_bytes_mut::(b)); + Ok(AccountZeroCopyMut { + fixed, + data: body, + _marker: PhantomData, + }) +} + +#[macro_export] +macro_rules! impl_zero_copy_loader { + ($Acc:ty, $ID:path, $Fixed:ty, $Elem:ty) => { + impl<'info> crate::state::zero_copy::ZeroCopyLoader<'_, $Elem, $Fixed> + for AccountInfo<'info> + { + fn load_zc<'a>( + self: &'a Self, + ) -> crate::error::DriftResult< + crate::state::zero_copy::AccountZeroCopy<'a, $Elem, $Fixed>, + > { + crate::state::zero_copy::load_generic::<$Fixed, $Elem>( + self, + <$Acc as anchor_lang::Discriminator>::discriminator(), + $ID(), + ) + } + + fn load_zc_mut<'a>( + self: &'a Self, + ) -> crate::error::DriftResult< + crate::state::zero_copy::AccountZeroCopyMut<'a, $Elem, $Fixed>, + > { + crate::state::zero_copy::load_generic_mut::<$Fixed, $Elem>( + self, + <$Acc as anchor_lang::Discriminator>::discriminator(), + $ID(), + ) + } + } + }; +} diff --git a/programs/drift/src/validation/margin.rs b/programs/drift/src/validation/margin.rs index f790d31121..49c993c89f 100644 --- a/programs/drift/src/validation/margin.rs +++ b/programs/drift/src/validation/margin.rs @@ -1,10 +1,10 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::constants::{ - LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, MAX_MARGIN_RATIO, MIN_MARGIN_RATIO, - SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, + HIGH_LEVERAGE_MIN_MARGIN_RATIO, LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, MAX_MARGIN_RATIO, + MIN_MARGIN_RATIO, SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::msg; -use crate::{validate, HIGH_LEVERAGE_MIN_MARGIN_RATIO}; +use crate::validate; pub fn validate_margin( margin_ratio_initial: u32, diff --git a/programs/drift/src/validation/order.rs b/programs/drift/src/validation/order.rs index 92dd4da465..74452a020f 100644 --- a/programs/drift/src/validation/order.rs +++ b/programs/drift/src/validation/order.rs @@ -4,13 +4,14 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; +use crate::math::constants::MAX_PREDICTION_MARKET_PRICE; use crate::math::orders::{ calculate_base_asset_amount_to_fill_up_to_limit_price, is_multiple_of_step_size, }; use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::PerpMarket; use crate::state::user::{Order, OrderTriggerCondition, OrderType}; -use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; +use crate::validate; #[cfg(test)] mod test; diff --git a/programs/drift/src/validation/perp_market.rs b/programs/drift/src/validation/perp_market.rs index e55257659f..30bce4dedd 100644 --- a/programs/drift/src/validation/perp_market.rs +++ b/programs/drift/src/validation/perp_market.rs @@ -1,12 +1,12 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; -use crate::math::constants::MAX_BASE_ASSET_AMOUNT_WITH_AMM; +use crate::math::constants::{BID_ASK_SPREAD_PRECISION, MAX_BASE_ASSET_AMOUNT_WITH_AMM}; use crate::math::safe_math::SafeMath; use crate::msg; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; -use crate::{validate, BID_ASK_SPREAD_PRECISION}; +use crate::validate; #[allow(clippy::comparison_chain)] pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { @@ -81,10 +81,10 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { market.amm.quote_asset_reserve )?; - let invariant_sqrt_u192 = crate::bn::U192::from(market.amm.sqrt_k); + let invariant_sqrt_u192 = crate::math::bn::U192::from(market.amm.sqrt_k); let invariant = invariant_sqrt_u192.safe_mul(invariant_sqrt_u192)?; let quote_asset_reserve = invariant - .safe_div(crate::bn::U192::from(market.amm.base_asset_reserve))? + .safe_div(crate::math::bn::U192::from(market.amm.base_asset_reserve))? .try_to_u128()?; let rounding_diff = quote_asset_reserve diff --git a/programs/drift/src/validation/user.rs b/programs/drift/src/validation/user.rs index 3f527fed0f..999b342f45 100644 --- a/programs/drift/src/validation/user.rs +++ b/programs/drift/src/validation/user.rs @@ -1,8 +1,9 @@ use crate::error::{DriftResult, ErrorCode}; +use crate::math::constants::THIRTEEN_DAY; use crate::msg; use crate::state::spot_market::SpotBalanceType; use crate::state::user::{User, UserStats}; -use crate::{validate, State, THIRTEEN_DAY}; +use crate::{validate, State}; pub fn validate_user_deletion( user: &User, diff --git a/sdk/src/accounts/types.ts b/sdk/src/accounts/types.ts index c572ae2798..db9cf2112b 100644 --- a/sdk/src/accounts/types.ts +++ b/sdk/src/accounts/types.ts @@ -6,6 +6,7 @@ import { UserAccount, UserStatsAccount, InsuranceFundStake, + ConstituentAccount, HighLeverageModeConfig, } from '../types'; import StrictEventEmitter from 'strict-event-emitter-types'; @@ -260,3 +261,22 @@ export interface HighLeverageModeConfigAccountEvents { update: void; error: (e: Error) => void; } + +export interface ConstituentAccountSubscriber { + eventEmitter: StrictEventEmitter; + isSubscribed: boolean; + + subscribe(constituentAccount?: ConstituentAccount): Promise; + sync(): Promise; + unsubscribe(): Promise; +} + +export interface ConstituentAccountEvents { + onAccountUpdate: ( + account: ConstituentAccount, + pubkey: PublicKey, + slot: number + ) => void; + update: void; + error: (e: Error) => void; +} diff --git a/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts new file mode 100644 index 0000000000..1176b86589 --- /dev/null +++ b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts @@ -0,0 +1,596 @@ +import { BufferAndSlot, ProgramAccountSubscriber, ResubOpts } from './types'; +import { AnchorProvider, Program } from '@coral-xyz/anchor'; +import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; +import { + AccountInfoBase, + AccountInfoWithBase58EncodedData, + AccountInfoWithBase64EncodedData, + createSolanaClient, + isAddress, + type Address, + type Commitment as GillCommitment, +} from 'gill'; +import bs58 from 'bs58'; + +export class WebSocketProgramAccountSubscriberV2 + implements ProgramAccountSubscriber +{ + subscriptionName: string; + accountDiscriminator: string; + bufferAndSlot?: BufferAndSlot; + bufferAndSlotMap: Map = new Map(); + program: Program; + decodeBuffer: (accountName: string, ix: Buffer) => T; + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer + ) => void; + listenerId?: number; + resubOpts?: ResubOpts; + isUnsubscribing = false; + timeoutId?: ReturnType; + options: { filters: MemcmpFilter[]; commitment?: Commitment }; + + receivingData = false; + + // Gill client components + private rpc: ReturnType['rpc']; + private rpcSubscriptions: ReturnType< + typeof createSolanaClient + >['rpcSubscriptions']; + private abortController?: AbortController; + + // Polling logic for specific accounts + private accountsToMonitor: Set = new Set(); + private pollingIntervalMs: number = 30000; // 30 seconds + private pollingTimeouts: Map> = + new Map(); + private lastWsNotificationTime: Map = new Map(); // Track last WS notification time per account + private accountsCurrentlyPolling: Set = new Set(); // Track which accounts are being polled + private batchPollingTimeout?: ReturnType; // Single timeout for batch polling + + public constructor( + subscriptionName: string, + accountDiscriminator: string, + program: Program, + decodeBufferFn: (accountName: string, ix: Buffer) => T, + options: { filters: MemcmpFilter[]; commitment?: Commitment } = { + filters: [], + }, + resubOpts?: ResubOpts, + accountsToMonitor?: PublicKey[] // Optional list of accounts to poll + ) { + this.subscriptionName = subscriptionName; + this.accountDiscriminator = accountDiscriminator; + this.program = program; + this.decodeBuffer = decodeBufferFn; + this.resubOpts = resubOpts; + if (this.resubOpts?.resubTimeoutMs < 1000) { + console.log( + 'resubTimeoutMs should be at least 1000ms to avoid spamming resub' + ); + } + this.options = options; + this.receivingData = false; + + // Initialize accounts to monitor + if (accountsToMonitor) { + accountsToMonitor.forEach((account) => { + this.accountsToMonitor.add(account.toBase58()); + }); + } + + // Initialize gill client using the same RPC URL as the program provider + const rpcUrl = (this.program.provider as AnchorProvider).connection + .rpcEndpoint; + const { rpc, rpcSubscriptions } = createSolanaClient({ + urlOrMoniker: rpcUrl, + }); + this.rpc = rpc; + this.rpcSubscriptions = rpcSubscriptions; + } + + async subscribe( + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer + ) => void + ): Promise { + if (this.listenerId != null || this.isUnsubscribing) { + return; + } + + this.onChange = onChange; + + // Create abort controller for proper cleanup + const abortController = new AbortController(); + this.abortController = abortController; + + // Subscribe to program account changes using gill's rpcSubscriptions + const programId = this.program.programId.toBase58(); + if (isAddress(programId)) { + const subscription = await this.rpcSubscriptions + .programNotifications(programId, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + filters: this.options.filters.map((filter) => ({ + memcmp: { + offset: BigInt(filter.memcmp.offset), + bytes: filter.memcmp.bytes as any, + encoding: 'base64' as const, + }, + })), + }) + .subscribe({ + abortSignal: abortController.signal, + }); + + for await (const notification of subscription) { + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + this.handleRpcResponse( + notification.context, + notification.value.account + ); + this.setTimeout(); + } else { + this.handleRpcResponse( + notification.context, + notification.value.account + ); + } + } + } + + this.listenerId = Math.random(); // Unique ID for logging purposes + + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + this.setTimeout(); + } + + // Start monitoring for accounts that may need polling if no WS event is received + this.startMonitoringForAccounts(); + } + + protected setTimeout(): void { + if (!this.onChange) { + throw new Error('onChange callback function must be set'); + } + this.timeoutId = setTimeout( + async () => { + if (this.isUnsubscribing) { + // If we are in the process of unsubscribing, do not attempt to resubscribe + return; + } + + if (this.receivingData) { + if (this.resubOpts?.logResubMessages) { + console.log( + `No ws data from ${this.subscriptionName} in ${this.resubOpts?.resubTimeoutMs}ms, resubscribing` + ); + } + await this.unsubscribe(true); + this.receivingData = false; + await this.subscribe(this.onChange); + } + }, + this.resubOpts?.resubTimeoutMs + ); + } + + handleRpcResponse( + context: { slot: bigint }, + accountInfo?: AccountInfoBase & + (AccountInfoWithBase58EncodedData | AccountInfoWithBase64EncodedData) + ): void { + const newSlot = Number(context.slot); + let newBuffer: Buffer | undefined = undefined; + + if (accountInfo) { + // Extract data from gill response + if (accountInfo.data) { + // Handle different data formats from gill + if (Array.isArray(accountInfo.data)) { + // If it's a tuple [data, encoding] + const [data, encoding] = accountInfo.data; + + if (encoding === ('base58' as any)) { + // Convert base58 to buffer using bs58 + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + } + + // Convert gill's account key to PublicKey + // Note: accountInfo doesn't have a key property, we need to get it from the notification + // For now, we'll use a placeholder - this needs to be fixed based on the actual gill API + const accountId = new PublicKey('11111111111111111111111111111111'); // Placeholder + const accountIdString = accountId.toBase58(); + + const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); + + // Track WebSocket notification time for this account + this.lastWsNotificationTime.set(accountIdString, Date.now()); + + // If this account was being polled, stop polling it + if (this.accountsCurrentlyPolling.has(accountIdString)) { + this.accountsCurrentlyPolling.delete(accountIdString); + + // If no more accounts are being polled, stop batch polling + if ( + this.accountsCurrentlyPolling.size === 0 && + this.batchPollingTimeout + ) { + clearTimeout(this.batchPollingTimeout); + this.batchPollingTimeout = undefined; + } + } + + if (!existingBufferAndSlot) { + if (newBuffer) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: newSlot, + }); + const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); + this.onChange(accountId, account, { slot: newSlot }, newBuffer); + } + return; + } + + if (newSlot < existingBufferAndSlot.slot) { + return; + } + + const oldBuffer = existingBufferAndSlot.buffer; + if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: newSlot, + }); + const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); + this.onChange(accountId, account, { slot: newSlot }, newBuffer); + } + } + + private startMonitoringForAccounts(): void { + // Clear any existing polling timeouts + this.clearPollingTimeouts(); + + // Start monitoring for each account in the accountsToMonitor set + this.accountsToMonitor.forEach((accountIdString) => { + this.startMonitoringForAccount(accountIdString); + }); + } + + private startMonitoringForAccount(accountIdString: string): void { + // Clear existing timeout for this account + const existingTimeout = this.pollingTimeouts.get(accountIdString); + if (existingTimeout) { + clearTimeout(existingTimeout); + } + + // Set up monitoring timeout - only start polling if no WS notification in 30s + const timeoutId = setTimeout(async () => { + // Check if we've received a WS notification for this account recently + const lastNotificationTime = + this.lastWsNotificationTime.get(accountIdString); + const currentTime = Date.now(); + + if ( + !lastNotificationTime || + currentTime - lastNotificationTime >= this.pollingIntervalMs + ) { + // No recent WS notification, start polling + await this.pollAccount(accountIdString); + // Schedule next poll + this.startPollingForAccount(accountIdString); + } else { + // We received a WS notification recently, continue monitoring + this.startMonitoringForAccount(accountIdString); + } + }, this.pollingIntervalMs); + + this.pollingTimeouts.set(accountIdString, timeoutId); + } + + private startPollingForAccount(accountIdString: string): void { + // Add account to polling set + this.accountsCurrentlyPolling.add(accountIdString); + + // If this is the first account being polled, start batch polling + if (this.accountsCurrentlyPolling.size === 1) { + this.startBatchPolling(); + } + } + + private startBatchPolling(): void { + // Clear existing batch polling timeout + if (this.batchPollingTimeout) { + clearTimeout(this.batchPollingTimeout); + } + + // Set up batch polling interval + this.batchPollingTimeout = setTimeout(async () => { + await this.pollAllAccounts(); + // Schedule next batch poll + this.startBatchPolling(); + }, this.pollingIntervalMs); + } + + private async pollAllAccounts(): Promise { + try { + // Get all accounts currently being polled + const accountsToPoll = Array.from(this.accountsCurrentlyPolling); + if (accountsToPoll.length === 0) { + return; + } + + // Fetch all accounts in a single batch request + const accountAddresses = accountsToPoll.map( + (accountId) => accountId as Address + ); + const rpcResponse = await this.rpc + .getMultipleAccounts(accountAddresses, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + }) + .send(); + + const currentSlot = Number(rpcResponse.context.slot); + + // Process each account response + for (let i = 0; i < accountsToPoll.length; i++) { + const accountIdString = accountsToPoll[i]; + const accountInfo = rpcResponse.value[i]; + + if (!accountInfo) { + continue; + } + + const existingBufferAndSlot = + this.bufferAndSlotMap.get(accountIdString); + + if (!existingBufferAndSlot) { + // Account not in our map yet, add it + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + newBuffer = Buffer.from(data, encoding); + } + } + + if (newBuffer) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: currentSlot, + }); + const account = this.decodeBuffer( + this.accountDiscriminator, + newBuffer + ); + const accountId = new PublicKey(accountIdString); + this.onChange(accountId, account, { slot: currentSlot }, newBuffer); + } + continue; + } + + // Check if we missed an update + if (currentSlot > existingBufferAndSlot.slot) { + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + if (encoding === ('base58' as any)) { + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + + // Check if buffer has changed + if ( + newBuffer && + (!existingBufferAndSlot.buffer || + !newBuffer.equals(existingBufferAndSlot.buffer)) + ) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing` + ); + } + // We missed an update, resubscribe + await this.unsubscribe(true); + this.receivingData = false; + await this.subscribe(this.onChange); + return; + } + } + } + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error batch polling accounts:`, + error + ); + } + } + } + + private async pollAccount(accountIdString: string): Promise { + try { + // Fetch current account data using gill's rpc + const accountAddress = accountIdString as Address; + const rpcResponse = await this.rpc + .getAccountInfo(accountAddress, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + }) + .send(); + + const currentSlot = Number(rpcResponse.context.slot); + const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); + + if (!existingBufferAndSlot) { + // Account not in our map yet, add it + if (rpcResponse.value) { + let newBuffer: Buffer | undefined = undefined; + if (rpcResponse.value.data) { + if (Array.isArray(rpcResponse.value.data)) { + const [data, encoding] = rpcResponse.value.data; + newBuffer = Buffer.from(data, encoding); + } + } + + if (newBuffer) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: currentSlot, + }); + const account = this.decodeBuffer( + this.accountDiscriminator, + newBuffer + ); + const accountId = new PublicKey(accountIdString); + this.onChange(accountId, account, { slot: currentSlot }, newBuffer); + } + } + return; + } + + // Check if we missed an update + if (currentSlot > existingBufferAndSlot.slot) { + let newBuffer: Buffer | undefined = undefined; + if (rpcResponse.value) { + if (rpcResponse.value.data) { + if (Array.isArray(rpcResponse.value.data)) { + const [data, encoding] = rpcResponse.value.data; + if (encoding === ('base58' as any)) { + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + } + + // Check if buffer has changed + if ( + newBuffer && + (!existingBufferAndSlot.buffer || + !newBuffer.equals(existingBufferAndSlot.buffer)) + ) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Polling detected missed update for account ${accountIdString}, resubscribing` + ); + } + // We missed an update, resubscribe + await this.unsubscribe(true); + this.receivingData = false; + await this.subscribe(this.onChange); + return; + } + } + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error polling account ${accountIdString}:`, + error + ); + } + } + } + + private clearPollingTimeouts(): void { + this.pollingTimeouts.forEach((timeoutId) => { + clearTimeout(timeoutId); + }); + this.pollingTimeouts.clear(); + + // Clear batch polling timeout + if (this.batchPollingTimeout) { + clearTimeout(this.batchPollingTimeout); + this.batchPollingTimeout = undefined; + } + + // Clear accounts currently polling + this.accountsCurrentlyPolling.clear(); + } + + unsubscribe(onResub = false): Promise { + if (!onResub) { + this.resubOpts.resubTimeoutMs = undefined; + } + this.isUnsubscribing = true; + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + + // Clear polling timeouts + this.clearPollingTimeouts(); + + // Abort the WebSocket subscription + if (this.abortController) { + this.abortController.abort('unsubscribing'); + this.abortController = undefined; + } + + this.listenerId = undefined; + this.isUnsubscribing = false; + + return Promise.resolve(); + } + + // Method to add accounts to the polling list + addAccountToMonitor(accountId: PublicKey): void { + const accountIdString = accountId.toBase58(); + this.accountsToMonitor.add(accountIdString); + + // If already subscribed, start monitoring for this account + if (this.listenerId != null && !this.isUnsubscribing) { + this.startMonitoringForAccount(accountIdString); + } + } + + // Method to remove accounts from the polling list + removeAccountFromMonitor(accountId: PublicKey): void { + const accountIdString = accountId.toBase58(); + this.accountsToMonitor.delete(accountIdString); + + // Clear monitoring timeout for this account + const timeoutId = this.pollingTimeouts.get(accountIdString); + if (timeoutId) { + clearTimeout(timeoutId); + this.pollingTimeouts.delete(accountIdString); + } + + // Remove from currently polling set if it was being polled + this.accountsCurrentlyPolling.delete(accountIdString); + + // If no more accounts are being polled, stop batch polling + if (this.accountsCurrentlyPolling.size === 0 && this.batchPollingTimeout) { + clearTimeout(this.batchPollingTimeout); + this.batchPollingTimeout = undefined; + } + } + + // Method to set polling interval + setPollingInterval(intervalMs: number): void { + this.pollingIntervalMs = intervalMs; + // Restart monitoring with new interval if already subscribed + if (this.listenerId != null && !this.isUnsubscribing) { + this.startMonitoringForAccounts(); + } + } +} diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index 098e37dde2..13d7bde79f 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -1,7 +1,11 @@ import { PublicKey } from '@solana/web3.js'; import * as anchor from '@coral-xyz/anchor'; import { BN } from '@coral-xyz/anchor'; -import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { + getAssociatedTokenAddress, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; import { SpotMarketAccount, TokenProgramFlag } from '../types'; export async function getDriftStateAccountPublicKeyAndNonce( @@ -420,3 +424,113 @@ export function getRevenueShareEscrowAccountPublicKey( programId )[0]; } + +export function getLpPoolPublicKey( + programId: PublicKey, + lpPoolId: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('lp_pool')), + new anchor.BN(lpPoolId).toArrayLike(Buffer, 'le', 1), + ], + programId + )[0]; +} + +export function getLpPoolTokenVaultPublicKey( + programId: PublicKey, + lpPool: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('LP_POOL_TOKEN_VAULT')), + lpPool.toBuffer(), + ], + programId + )[0]; +} +export function getAmmConstituentMappingPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('AMM_MAP')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export function getConstituentTargetBasePublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from( + anchor.utils.bytes.utf8.encode('constituent_target_base_seed') + ), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export function getConstituentPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey, + spotMarketIndex: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('CONSTITUENT')), + lpPoolPublicKey.toBuffer(), + new anchor.BN(spotMarketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + )[0]; +} + +export function getConstituentVaultPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey, + spotMarketIndex: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('CONSTITUENT_VAULT')), + lpPoolPublicKey.toBuffer(), + new anchor.BN(spotMarketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + )[0]; +} + +export function getAmmCachePublicKey(programId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from(anchor.utils.bytes.utf8.encode('amm_cache_seed'))], + programId + )[0]; +} + +export function getConstituentCorrelationsPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('constituent_correlations')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export async function getLpPoolTokenTokenAccountPublicKey( + lpPoolTokenMint: PublicKey, + authority: PublicKey +): Promise { + return await getAssociatedTokenAddress(lpPoolTokenMint, authority, true); +} diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 693637f55f..da4d9cc8a5 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -1,4 +1,7 @@ import { + AddressLookupTableAccount, + Keypair, + LAMPORTS_PER_SOL, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, @@ -16,6 +19,11 @@ import { SpotFulfillmentConfigStatus, IfRebalanceConfigParams, TxParams, + AddAmmConstituentMappingDatum, + SwapReduceOnly, + InitializeConstituentParams, + ConstituentStatus, + LPPoolAccount, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -41,9 +49,25 @@ import { getTokenProgramForSpotMarket, getIfRebalanceConfigPublicKey, getInsuranceFundStakeAccountPublicKey, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + getConstituentTargetBasePublicKey, + getConstituentPublicKey, + getConstituentVaultPublicKey, + getAmmCachePublicKey, + getLpPoolTokenVaultPublicKey, + getDriftSignerPublicKey, + getConstituentCorrelationsPublicKey, } from './addresses/pda'; import { squareRootBN } from './math/utils'; -import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { + createInitializeMint2Instruction, + createMintToInstruction, + createTransferCheckedInstruction, + getAssociatedTokenAddressSync, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; import { DriftClient } from './driftClient'; import { PEG_PRECISION, @@ -60,6 +84,8 @@ import { PROGRAM_ID as PHOENIX_PROGRAM_ID } from '@ellipsis-labs/phoenix-sdk'; import { DRIFT_ORACLE_RECEIVER_ID } from './config'; import { getFeedIdUint8Array } from './util/pythOracleUtils'; import { FUEL_RESET_LOG_ACCOUNT } from './constants/txConstants'; +import { JupiterClient, QuoteResponse } from './jupiter/jupiterClient'; +import { SwapMode } from './swap/UnifiedSwapClient'; const OPENBOOK_PROGRAM_ID = new PublicKey( 'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb' @@ -476,11 +502,18 @@ export class AdminClient extends DriftClient { concentrationCoefScale = ONE, curveUpdateIntensity = 0, ammJitIntensity = 0, - name = DEFAULT_MARKET_NAME + name = DEFAULT_MARKET_NAME, + lpPoolId: number = 0 ): Promise { const currentPerpMarketIndex = this.getStateAccount().numberOfMarkets; - const initializeMarketIx = await this.getInitializePerpMarketIx( + const ammCachePublicKey = getAmmCachePublicKey(this.program.programId); + const ammCacheAccount = await this.connection.getAccountInfo( + ammCachePublicKey + ); + const mustInitializeAmmCache = ammCacheAccount?.data == null; + + const initializeMarketIxs = await this.getInitializePerpMarketIx( marketIndex, priceOracle, baseAssetReserve, @@ -506,9 +539,11 @@ export class AdminClient extends DriftClient { concentrationCoefScale, curveUpdateIntensity, ammJitIntensity, - name + name, + mustInitializeAmmCache, + lpPoolId ); - const tx = await this.buildTransaction(initializeMarketIx); + const tx = await this.buildTransaction(initializeMarketIxs); const { txSig } = await this.sendTransaction(tx, [], this.opts); @@ -552,15 +587,22 @@ export class AdminClient extends DriftClient { concentrationCoefScale = ONE, curveUpdateIntensity = 0, ammJitIntensity = 0, - name = DEFAULT_MARKET_NAME - ): Promise { + name = DEFAULT_MARKET_NAME, + includeInitAmmCacheIx = false, + lpPoolId: number = 0 + ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, marketIndex ); + const ixs: TransactionInstruction[] = []; + if (includeInitAmmCacheIx) { + ixs.push(await this.getInitializeAmmCacheIx()); + } + const nameBuffer = encodeName(name); - return await this.program.instruction.initializePerpMarket( + const initPerpIx = await this.program.instruction.initializePerpMarket( marketIndex, baseAssetReserve, quoteAssetReserve, @@ -586,6 +628,7 @@ export class AdminClient extends DriftClient { curveUpdateIntensity, ammJitIntensity, nameBuffer, + lpPoolId, { accounts: { state: await this.getStatePublicKey(), @@ -594,11 +637,159 @@ export class AdminClient extends DriftClient { : this.wallet.publicKey, oracle: priceOracle, perpMarket: perpMarketPublicKey, + ammCache: getAmmCachePublicKey(this.program.programId), rent: SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, } ); + ixs.push(initPerpIx); + return ixs; + } + + public async initializeAmmCache( + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getInitializeAmmCacheIx(); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getInitializeAmmCacheIx(): Promise { + return await this.program.instruction.initializeAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async updateInitialAmmCacheInfo( + perpMarketIndexes: number[], + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getUpdateInitialAmmCacheInfoIx( + perpMarketIndexes + ); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateInitialAmmCacheInfoIx( + perpMarketIndexes: number[] + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readablePerpMarketIndex: perpMarketIndexes, + readableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], + }); + return await this.program.instruction.updateInitialAmmCacheInfo({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + remainingAccounts, + }); + } + + public async overrideAmmCacheInfo( + perpMarketIndex: number, + params: { + quoteOwedFromLpPool?: BN; + lastSettleTs?: BN; + lastFeePoolTokenAmount?: BN; + lastNetPnlPoolTokenAmount?: BN; + ammPositionScalar?: number; + ammInventoryLimit?: BN; + }, + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getOverrideAmmCacheInfoIx( + perpMarketIndex, + params + ); + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getOverrideAmmCacheInfoIx( + perpMarketIndex: number, + params: { + quoteOwedFromLpPool?: BN; + lastSettleSlot?: BN; + lastFeePoolTokenAmount?: BN; + lastNetPnlPoolTokenAmount?: BN; + ammPositionScalar?: number; + ammInventoryLimit?: BN; + } + ): Promise { + return this.program.instruction.overrideAmmCacheInfo( + perpMarketIndex, + Object.assign( + {}, + { + quoteOwedFromLpPool: null, + lastSettleSlot: null, + lastFeePoolTokenAmount: null, + lastNetPnlPoolTokenAmount: null, + ammPositionScalar: null, + ammInventoryLimit: null, + }, + params + ), + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + } + ); + } + + public async resetAmmCache( + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getResetAmmCacheIx(); + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getResetAmmCacheIx(): Promise { + return this.program.instruction.resetAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); } public async initializePredictionMarket( @@ -872,6 +1063,76 @@ export class AdminClient extends DriftClient { ); } + public async updatePerpMarketLpPoolId( + perpMarketIndex: number, + lpPoolId: number + ) { + const updatePerpMarketLpPoolIIx = await this.getUpdatePerpMarketLpPoolIdIx( + perpMarketIndex, + lpPoolId + ); + + const tx = await this.buildTransaction(updatePerpMarketLpPoolIIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdatePerpMarketLpPoolIdIx( + perpMarketIndex: number, + lpPoolId: number + ): Promise { + return await this.program.instruction.updatePerpMarketLpPoolId(lpPoolId, { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + }, + }); + } + + public async updatePerpMarketLpPoolStatus( + perpMarketIndex: number, + lpStatus: number + ) { + const updatePerpMarketLpPoolStatusIx = + await this.getUpdatePerpMarketLpPoolStatusIx(perpMarketIndex, lpStatus); + + const tx = await this.buildTransaction(updatePerpMarketLpPoolStatusIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdatePerpMarketLpPoolStatusIx( + perpMarketIndex: number, + lpStatus: number + ): Promise { + return await this.program.instruction.updatePerpMarketLpPoolStatus( + lpStatus, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + ammCache: getAmmCachePublicKey(this.program.programId), + }, + } + ); + } + public async moveAmmToPrice( perpMarketIndex: number, targetPrice: BN @@ -1062,6 +1323,13 @@ export class AdminClient extends DriftClient { sourceVault: PublicKey ): Promise { const spotMarket = this.getQuoteSpotMarketAccount(); + const remainingAccounts = [ + { + pubkey: spotMarket.mint, + isWritable: false, + isSigner: false, + }, + ]; return await this.program.instruction.depositIntoPerpMarketFeePool(amount, { accounts: { @@ -1079,6 +1347,7 @@ export class AdminClient extends DriftClient { spotMarketVault: spotMarket.vault, tokenProgram: TOKEN_PROGRAM_ID, }, + remainingAccounts, }); } @@ -2349,6 +2618,7 @@ export class AdminClient extends DriftClient { ), oracle: oracle, oldOracle: this.getPerpMarketAccount(perpMarketIndex).amm.oracle, + ammCache: getAmmCachePublicKey(this.program.programId), }, } ); @@ -3159,6 +3429,7 @@ export class AdminClient extends DriftClient { this.program.programId, perpMarketIndex ), + ammCache: getAmmCachePublicKey(this.program.programId), }, } ); @@ -4653,9 +4924,9 @@ export class AdminClient extends DriftClient { ): Promise { return await this.program.instruction.zeroMmOracleFields({ accounts: { - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, state: await this.getStatePublicKey(), perpMarket: await getPerpMarketPublicKey( this.program.programId, @@ -4693,6 +4964,61 @@ export class AdminClient extends DriftClient { ); } + public async updateFeatureBitFlagsBuilderCodes( + enable: boolean + ): Promise { + const updateFeatureBitFlagsBuilderCodesIx = + await this.getUpdateFeatureBitFlagsBuilderCodesIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsBuilderCodesIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsBuilderCodesIx( + enable: boolean + ): Promise { + return this.program.instruction.updateFeatureBitFlagsBuilderCodes(enable, { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + }); + } + + public async updateFeatureBitFlagsBuilderReferral( + enable: boolean + ): Promise { + const updateFeatureBitFlagsBuilderReferralIx = + await this.getUpdateFeatureBitFlagsBuilderReferralIx(enable); + + const tx = await this.buildTransaction( + updateFeatureBitFlagsBuilderReferralIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsBuilderReferralIx( + enable: boolean + ): Promise { + return this.program.instruction.updateFeatureBitFlagsBuilderReferral( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + public async updateFeatureBitFlagsMedianTriggerPrice( enable: boolean ): Promise { @@ -4821,49 +5147,50 @@ export class AdminClient extends DriftClient { ); } - public async updateFeatureBitFlagsBuilderCodes( + public async updateFeatureBitFlagsSettleLpPool( enable: boolean ): Promise { - const updateFeatureBitFlagsBuilderCodesIx = - await this.getUpdateFeatureBitFlagsBuilderCodesIx(enable); + const updateFeatureBitFlagsSettleLpPoolIx = + await this.getUpdateFeatureBitFlagsSettleLpPoolIx(enable); - const tx = await this.buildTransaction(updateFeatureBitFlagsBuilderCodesIx); + const tx = await this.buildTransaction(updateFeatureBitFlagsSettleLpPoolIx); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getUpdateFeatureBitFlagsBuilderCodesIx( + public async getUpdateFeatureBitFlagsSettleLpPoolIx( enable: boolean ): Promise { - return this.program.instruction.updateFeatureBitFlagsBuilderCodes(enable, { - accounts: { - admin: this.useHotWalletAdmin - ? this.wallet.publicKey - : this.getStateAccount().admin, - state: await this.getStatePublicKey(), - }, - }); + return await this.program.instruction.updateFeatureBitFlagsSettleLpPool( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); } - public async updateFeatureBitFlagsBuilderReferral( + public async updateFeatureBitFlagsSwapLpPool( enable: boolean ): Promise { - const updateFeatureBitFlagsBuilderReferralIx = - await this.getUpdateFeatureBitFlagsBuilderReferralIx(enable); + const updateFeatureBitFlagsSettleLpPoolIx = + await this.getUpdateFeatureBitFlagsSwapLpPoolIx(enable); - const tx = await this.buildTransaction( - updateFeatureBitFlagsBuilderReferralIx - ); + const tx = await this.buildTransaction(updateFeatureBitFlagsSettleLpPoolIx); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getUpdateFeatureBitFlagsBuilderReferralIx( + public async getUpdateFeatureBitFlagsSwapLpPoolIx( enable: boolean ): Promise { - return this.program.instruction.updateFeatureBitFlagsBuilderReferral( + return await this.program.instruction.updateFeatureBitFlagsSwapLpPool( enable, { accounts: { @@ -4876,22 +5203,50 @@ export class AdminClient extends DriftClient { ); } - public async adminDisableUpdatePerpBidAskTwap( - authority: PublicKey, - disable: boolean + public async updateFeatureBitFlagsMintRedeemLpPool( + enable: boolean ): Promise { - const disableBidAskTwapUpdateIx = - await this.getAdminDisableUpdatePerpBidAskTwapIx(authority, disable); + const updateFeatureBitFlagsSettleLpPoolIx = + await this.getUpdateFeatureBitFlagsMintRedeemLpPoolIx(enable); - const tx = await this.buildTransaction(disableBidAskTwapUpdateIx); + const tx = await this.buildTransaction(updateFeatureBitFlagsSettleLpPoolIx); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getAdminDisableUpdatePerpBidAskTwapIx( - authority: PublicKey, - disable: boolean + public async getUpdateFeatureBitFlagsMintRedeemLpPoolIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsMintRedeemLpPool( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async adminDisableUpdatePerpBidAskTwap( + authority: PublicKey, + disable: boolean + ): Promise { + const disableBidAskTwapUpdateIx = + await this.getAdminDisableUpdatePerpBidAskTwapIx(authority, disable); + + const tx = await this.buildTransaction(disableBidAskTwapUpdateIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getAdminDisableUpdatePerpBidAskTwapIx( + authority: PublicKey, + disable: boolean ): Promise { return await this.program.instruction.adminDisableUpdatePerpBidAskTwap( disable, @@ -4909,4 +5264,1220 @@ export class AdminClient extends DriftClient { } ); } + + public async initializeLpPool( + lpPoolId: number, + minMintFee: BN, + maxAum: BN, + maxSettleQuoteAmountPerMarket: BN, + mint: Keypair, + whitelistMint?: PublicKey + ): Promise { + const ixs = await this.getInitializeLpPoolIx( + lpPoolId, + minMintFee, + maxAum, + maxSettleQuoteAmountPerMarket, + mint, + whitelistMint + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, [mint]); + return txSig; + } + + public async getInitializeLpPoolIx( + lpPoolId: number, + minMintFee: BN, + maxAum: BN, + maxSettleQuoteAmountPerMarket: BN, + mint: Keypair, + whitelistMint?: PublicKey + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + const lamports = + await this.program.provider.connection.getMinimumBalanceForRentExemption( + MINT_SIZE + ); + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: this.wallet.publicKey, + newAccountPubkey: mint.publicKey, + space: MINT_SIZE, + lamports: Math.min(0.05 * LAMPORTS_PER_SOL, lamports), // should be 0.0014616 ? but bankrun returns 10 SOL + programId: TOKEN_PROGRAM_ID, + }); + const createMintIx = createInitializeMint2Instruction( + mint.publicKey, + 6, + lpPool, + null, + TOKEN_PROGRAM_ID + ); + + return [ + createMintAccountIx, + createMintIx, + this.program.instruction.initializeLpPool( + lpPoolId, + minMintFee, + maxAum, + maxSettleQuoteAmountPerMarket, + whitelistMint ?? PublicKey.default, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool + ), + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + ammConstituentMapping, + constituentTargetBase, + mint: mint.publicKey, + state: await this.getStatePublicKey(), + tokenProgram: TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }, + signers: [mint], + } + ), + ]; + } + + public async initializeConstituent( + lpPoolId: number, + initializeConstituentParams: InitializeConstituentParams + ): Promise { + const ixs = await this.getInitializeConstituentIx( + lpPoolId, + initializeConstituentParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getInitializeConstituentIx( + lpPoolId: number, + initializeConstituentParams: InitializeConstituentParams + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const spotMarketIndex = initializeConstituentParams.spotMarketIndex; + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + const constituent = getConstituentPublicKey( + this.program.programId, + lpPool, + spotMarketIndex + ); + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + + return [ + this.program.instruction.initializeConstituent( + spotMarketIndex, + initializeConstituentParams.decimals, + initializeConstituentParams.maxWeightDeviation, + initializeConstituentParams.swapFeeMin, + initializeConstituentParams.swapFeeMax, + initializeConstituentParams.maxBorrowTokenAmount, + initializeConstituentParams.oracleStalenessThreshold, + initializeConstituentParams.costToTrade, + initializeConstituentParams.constituentDerivativeIndex != null + ? initializeConstituentParams.constituentDerivativeIndex + : null, + initializeConstituentParams.constituentDerivativeDepegThreshold != null + ? initializeConstituentParams.constituentDerivativeDepegThreshold + : ZERO, + initializeConstituentParams.constituentDerivativeIndex != null + ? initializeConstituentParams.derivativeWeight + : ZERO, + initializeConstituentParams.volatility != null + ? initializeConstituentParams.volatility + : 10, + initializeConstituentParams.gammaExecution != null + ? initializeConstituentParams.gammaExecution + : 2, + initializeConstituentParams.gammaInventory != null + ? initializeConstituentParams.gammaInventory + : 2, + initializeConstituentParams.xi != null + ? initializeConstituentParams.xi + : 2, + initializeConstituentParams.constituentCorrelations, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + constituentTargetBase, + constituent, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + spotMarketMint: spotMarketAccount.mint, + constituentVault: getConstituentVaultPublicKey( + this.program.programId, + lpPool, + spotMarketIndex + ), + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + spotMarket: spotMarketAccount.pubkey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + signers: [], + } + ), + ]; + } + + public async updateConstituentStatus( + constituent: PublicKey, + constituentStatus: ConstituentStatus + ): Promise { + const updateConstituentStatusIx = await this.getUpdateConstituentStatusIx( + constituent, + constituentStatus + ); + + const tx = await this.buildTransaction(updateConstituentStatusIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateConstituentStatusIx( + constituent: PublicKey, + constituentStatus: ConstituentStatus + ): Promise { + return await this.program.instruction.updateConstituentStatus( + constituentStatus, + { + accounts: { + constituent, + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateConstituentPausedOperations( + constituent: PublicKey, + pausedOperations: number + ): Promise { + const updateConstituentPausedOperationsIx = + await this.getUpdateConstituentPausedOperationsIx( + constituent, + pausedOperations + ); + + const tx = await this.buildTransaction(updateConstituentPausedOperationsIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateConstituentPausedOperationsIx( + constituent: PublicKey, + pausedOperations: number + ): Promise { + return await this.program.instruction.updateConstituentPausedOperations( + pausedOperations, + { + accounts: { + constituent, + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateConstituentParams( + lpPoolId: number, + constituentPublicKey: PublicKey, + updateConstituentParams: { + maxWeightDeviation?: BN; + swapFeeMin?: BN; + swapFeeMax?: BN; + maxBorrowTokenAmount?: BN; + oracleStalenessThreshold?: BN; + costToTradeBps?: number; + derivativeWeight?: BN; + constituentDerivativeIndex?: number; + volatility?: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; + } + ): Promise { + const ixs = await this.getUpdateConstituentParamsIx( + lpPoolId, + constituentPublicKey, + updateConstituentParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateConstituentParamsIx( + lpPoolId: number, + constituentPublicKey: PublicKey, + updateConstituentParams: { + maxWeightDeviation?: BN; + swapFeeMin?: BN; + swapFeeMax?: BN; + maxBorrowTokenAmount?: BN; + oracleStalenessThreshold?: BN; + derivativeWeight?: BN; + constituentDerivativeIndex?: number; + volatility?: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; + } + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + return [ + this.program.instruction.updateConstituentParams( + Object.assign( + { + maxWeightDeviation: null, + swapFeeMin: null, + swapFeeMax: null, + maxBorrowTokenAmount: null, + oracleStalenessThreshold: null, + costToTradeBps: null, + stablecoinWeight: null, + derivativeWeight: null, + constituentDerivativeIndex: null, + volatility: null, + gammaExecution: null, + gammaInventory: null, + xi: null, + }, + updateConstituentParams + ), + { + accounts: { + admin: this.wallet.publicKey, + constituent: constituentPublicKey, + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase: getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ), + }, + signers: [], + } + ), + ]; + } + + public async updateLpPoolParams( + lpPoolId: number, + updateLpPoolParams: { + maxSettleQuoteAmount?: BN; + volatility?: BN; + gammaExecution?: number; + xi?: number; + whitelistMint?: PublicKey; + maxAum?: BN; + } + ): Promise { + const ixs = await this.getUpdateLpPoolParamsIx( + lpPoolId, + updateLpPoolParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateLpPoolParamsIx( + lpPoolId: number, + updateLpPoolParams: { + maxSettleQuoteAmount?: BN; + volatility?: BN; + gammaExecution?: number; + xi?: number; + whitelistMint?: PublicKey; + maxAum?: BN; + } + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + return [ + this.program.instruction.updateLpPoolParams( + Object.assign( + { + maxSettleQuoteAmount: null, + volatility: null, + gammaExecution: null, + xi: null, + whitelistMint: null, + maxAum: null, + }, + updateLpPoolParams + ), + { + accounts: { + admin: this.wallet.publicKey, + state: await this.getStatePublicKey(), + lpPool, + }, + signers: [], + } + ), + ]; + } + + public async addAmmConstituentMappingData( + lpPoolId: number, + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const ixs = await this.getAddAmmConstituentMappingDataIx( + lpPoolId, + addAmmConstituentMappingData + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getAddAmmConstituentMappingDataIx( + lpPoolId: number, + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + return [ + this.program.instruction.addAmmConstituentMappingData( + addAmmConstituentMappingData, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + constituentTargetBase, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async updateAmmConstituentMappingData( + lpPoolId: number, + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const ixs = await this.getUpdateAmmConstituentMappingDataIx( + lpPoolId, + addAmmConstituentMappingData + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateAmmConstituentMappingDataIx( + lpPoolId: number, + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + return [ + this.program.instruction.updateAmmConstituentMappingData( + addAmmConstituentMappingData, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async removeAmmConstituentMappingData( + lpPoolId: number, + perpMarketIndex: number, + constituentIndex: number + ): Promise { + const ixs = await this.getRemoveAmmConstituentMappingDataIx( + lpPoolId, + perpMarketIndex, + constituentIndex + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getRemoveAmmConstituentMappingDataIx( + lpPoolId: number, + perpMarketIndex: number, + constituentIndex: number + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + + return [ + this.program.instruction.removeAmmConstituentMappingData( + perpMarketIndex, + constituentIndex, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async updateConstituentCorrelationData( + lpPoolId: number, + index1: number, + index2: number, + correlation: BN + ): Promise { + const ixs = await this.getUpdateConstituentCorrelationDataIx( + lpPoolId, + index1, + index2, + correlation + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateConstituentCorrelationDataIx( + lpPoolId: number, + index1: number, + index2: number, + correlation: BN + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + return [ + this.program.instruction.updateConstituentCorrelationData( + index1, + index2, + correlation, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + /** + * Get the drift begin_swap and end_swap instructions + * + * @param outMarketIndex the market index of the token you're buying + * @param inMarketIndex the market index of the token you're selling + * @param amountIn the amount of the token to sell + * @param inTokenAccount the token account to move the tokens being sold (admin signer ata for lp swap) + * @param outTokenAccount the token account to receive the tokens being bought (admin signer ata for lp swap) + * @param limitPrice the limit price of the swap + * @param reduceOnly + * @param userAccountPublicKey optional, specify a custom userAccountPublicKey to use instead of getting the current user account; can be helpful if the account is being created within the current tx + */ + public async getSwapIx( + { + lpPoolId, + outMarketIndex, + inMarketIndex, + amountIn, + inTokenAccount, + outTokenAccount, + limitPrice, + reduceOnly, + userAccountPublicKey, + }: { + lpPoolId: number; + outMarketIndex: number; + inMarketIndex: number; + amountIn: BN; + inTokenAccount: PublicKey; + outTokenAccount: PublicKey; + limitPrice?: BN; + reduceOnly?: SwapReduceOnly; + userAccountPublicKey?: PublicKey; + }, + lpSwap?: boolean + ): Promise<{ + beginSwapIx: TransactionInstruction; + endSwapIx: TransactionInstruction; + }> { + if (!lpSwap) { + return super.getSwapIx({ + outMarketIndex, + inMarketIndex, + amountIn, + inTokenAccount, + outTokenAccount, + limitPrice, + reduceOnly, + userAccountPublicKey, + }); + } + const outSpotMarket = this.getSpotMarketAccount(outMarketIndex); + const inSpotMarket = this.getSpotMarketAccount(inMarketIndex); + + const outTokenProgram = this.getTokenProgramForSpotMarket(outSpotMarket); + const inTokenProgram = this.getTokenProgramForSpotMarket(inSpotMarket); + + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + + const outConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + + const beginSwapIx = this.program.instruction.beginLpSwap( + inMarketIndex, + outMarketIndex, + amountIn, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + signerOutTokenAccount: outTokenAccount, + signerInTokenAccount: inTokenAccount, + constituentOutTokenAccount: outConstituentTokenAccount, + constituentInTokenAccount: inConstituentTokenAccount, + outConstituent, + inConstituent, + lpPool, + instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + tokenProgram: inTokenProgram, + }, + } + ); + + const remainingAccounts = []; + remainingAccounts.push({ + pubkey: outTokenProgram, + isWritable: false, + isSigner: false, + }); + + const endSwapIx = this.program.instruction.endLpSwap( + inMarketIndex, + outMarketIndex, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + signerOutTokenAccount: outTokenAccount, + signerInTokenAccount: inTokenAccount, + constituentOutTokenAccount: outConstituentTokenAccount, + constituentInTokenAccount: inConstituentTokenAccount, + outConstituent, + inConstituent, + lpPool, + tokenProgram: inTokenProgram, + instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }, + remainingAccounts, + } + ); + + return { beginSwapIx, endSwapIx }; + } + + public async getLpJupiterSwapIxV6({ + jupiterClient, + outMarketIndex, + inMarketIndex, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + quote, + lpPoolId, + }: { + jupiterClient: JupiterClient; + outMarketIndex: number; + inMarketIndex: number; + outAssociatedTokenAccount?: PublicKey; + inAssociatedTokenAccount?: PublicKey; + amount: BN; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + quote?: QuoteResponse; + lpPoolId: number; + }): Promise<{ + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + const outMarket = this.getSpotMarketAccount(outMarketIndex); + const inMarket = this.getSpotMarketAccount(inMarketIndex); + + if (!quote) { + const fetchedQuote = await jupiterClient.getQuote({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + }); + + quote = fetchedQuote; + } + + if (!quote) { + throw new Error("Could not fetch Jupiter's quote. Please try again."); + } + + const isExactOut = swapMode === 'ExactOut' || quote.swapMode === 'ExactOut'; + const amountIn = new BN(quote.inAmount); + const exactOutBufferedAmountIn = amountIn.muln(1001).divn(1000); // Add 10bp buffer + + const transaction = await jupiterClient.getSwap({ + quote, + userPublicKey: this.provider.wallet.publicKey, + slippageBps, + }); + + const { transactionMessage, lookupTables } = + await jupiterClient.getTransactionMessageAndLookupTables({ + transaction, + }); + + const jupiterInstructions = jupiterClient.getJupiterInstructions({ + transactionMessage, + inputMint: inMarket.mint, + outputMint: outMarket.mint, + }); + + const preInstructions = []; + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + const outAssociatedTokenAccount = await this.getAssociatedTokenAccount( + outMarket.marketIndex, + false, + tokenProgram + ); + + const outAccountInfo = await this.connection.getAccountInfo( + outAssociatedTokenAccount + ); + if (!outAccountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + outAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + outMarket.mint, + tokenProgram + ) + ); + } + + const inTokenProgram = this.getTokenProgramForSpotMarket(inMarket); + const inAssociatedTokenAccount = await this.getAssociatedTokenAccount( + inMarket.marketIndex, + false, + inTokenProgram + ); + + const inAccountInfo = await this.connection.getAccountInfo( + inAssociatedTokenAccount + ); + if (!inAccountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + inAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + inMarket.mint, + tokenProgram + ) + ); + } + + const { beginSwapIx, endSwapIx } = await this.getSwapIx({ + lpPoolId, + outMarketIndex, + inMarketIndex, + amountIn: isExactOut ? exactOutBufferedAmountIn : amountIn, + inTokenAccount: inAssociatedTokenAccount, + outTokenAccount: outAssociatedTokenAccount, + }); + + const ixs = [ + ...preInstructions, + beginSwapIx, + ...jupiterInstructions, + endSwapIx, + ]; + + return { ixs, lookupTables }; + } + + public async getDevnetLpSwapIxs( + amountIn: BN, + amountOut: BN, + externalUserAuthority: PublicKey, + externalUserInTokenAccount: PublicKey, + externalUserOutTokenAccount: PublicKey, + inSpotMarketIndex: number, + outSpotMarketIndex: number + ): Promise { + const inSpotMarketAccount = this.getSpotMarketAccount(inSpotMarketIndex); + const outSpotMarketAccount = this.getSpotMarketAccount(outSpotMarketIndex); + + const outTokenAccount = await this.getAssociatedTokenAccount( + outSpotMarketAccount.marketIndex, + false, + getTokenProgramForSpotMarket(outSpotMarketAccount) + ); + const inTokenAccount = await this.getAssociatedTokenAccount( + inSpotMarketAccount.marketIndex, + false, + getTokenProgramForSpotMarket(inSpotMarketAccount) + ); + + const externalCreateInTokenAccountIx = + this.createAssociatedTokenAccountIdempotentInstruction( + externalUserInTokenAccount, + this.wallet.publicKey, + externalUserAuthority, + this.getSpotMarketAccount(inSpotMarketIndex)!.mint + ); + + const externalCreateOutTokenAccountIx = + this.createAssociatedTokenAccountIdempotentInstruction( + externalUserOutTokenAccount, + this.wallet.publicKey, + externalUserAuthority, + this.getSpotMarketAccount(outSpotMarketIndex)!.mint + ); + + const outTransferIx = createTransferCheckedInstruction( + externalUserOutTokenAccount, + outSpotMarketAccount.mint, + outTokenAccount, + externalUserAuthority, + amountOut.toNumber(), + outSpotMarketAccount.decimals, + undefined, + getTokenProgramForSpotMarket(outSpotMarketAccount) + ); + + const inTransferIx = createTransferCheckedInstruction( + inTokenAccount, + inSpotMarketAccount.mint, + externalUserInTokenAccount, + this.wallet.publicKey, + amountIn.toNumber(), + inSpotMarketAccount.decimals, + undefined, + getTokenProgramForSpotMarket(inSpotMarketAccount) + ); + + const ixs = [ + externalCreateInTokenAccountIx, + externalCreateOutTokenAccountIx, + outTransferIx, + inTransferIx, + ]; + return ixs; + } + + public async getAllDevnetLpSwapIxs( + lpPoolId: number, + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + externalUserAuthority: PublicKey + ) { + const { beginSwapIx, endSwapIx } = await this.getSwapIx( + { + lpPoolId, + inMarketIndex, + outMarketIndex, + amountIn: inAmount, + inTokenAccount: await this.getAssociatedTokenAccount( + inMarketIndex, + false + ), + outTokenAccount: await this.getAssociatedTokenAccount( + outMarketIndex, + false + ), + }, + true + ); + + const devnetLpSwapIxs = await this.getDevnetLpSwapIxs( + inAmount, + minOutAmount, + externalUserAuthority, + await this.getAssociatedTokenAccount( + inMarketIndex, + false, + getTokenProgramForSpotMarket(this.getSpotMarketAccount(inMarketIndex)), + externalUserAuthority + ), + await this.getAssociatedTokenAccount( + outMarketIndex, + false, + getTokenProgramForSpotMarket(this.getSpotMarketAccount(outMarketIndex)), + externalUserAuthority + ), + inMarketIndex, + outMarketIndex + ); + + return [ + beginSwapIx, + ...devnetLpSwapIxs, + endSwapIx, + ] as TransactionInstruction[]; + } + + public async depositWithdrawToProgramVault( + lpPoolId: number, + depositMarketIndex: number, + borrowMarketIndex: number, + amountToDeposit: BN, + amountToBorrow: BN + ): Promise { + const { depositIx, withdrawIx } = + await this.getDepositWithdrawToProgramVaultIxs( + lpPoolId, + depositMarketIndex, + borrowMarketIndex, + amountToDeposit, + amountToBorrow + ); + + const tx = await this.buildTransaction([depositIx, withdrawIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositWithdrawToProgramVaultIxs( + lpPoolId: number, + depositMarketIndex: number, + borrowMarketIndex: number, + amountToDeposit: BN, + amountToBorrow: BN + ): Promise<{ + depositIx: TransactionInstruction; + withdrawIx: TransactionInstruction; + }> { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const depositSpotMarket = this.getSpotMarketAccount(depositMarketIndex); + const withdrawSpotMarket = this.getSpotMarketAccount(borrowMarketIndex); + + const depositTokenProgram = + this.getTokenProgramForSpotMarket(depositSpotMarket); + const withdrawTokenProgram = + this.getTokenProgramForSpotMarket(withdrawSpotMarket); + + const depositConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + depositMarketIndex + ); + const withdrawConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + borrowMarketIndex + ); + + const depositConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + depositMarketIndex + ); + const withdrawConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + borrowMarketIndex + ); + + const depositIx = this.program.instruction.depositToProgramVault( + amountToDeposit, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + constituent: depositConstituent, + constituentTokenAccount: depositConstituentTokenAccount, + spotMarket: depositSpotMarket.pubkey, + spotMarketVault: depositSpotMarket.vault, + tokenProgram: depositTokenProgram, + mint: depositSpotMarket.mint, + oracle: depositSpotMarket.oracle, + }, + } + ); + + const withdrawIx = this.program.instruction.withdrawFromProgramVault( + amountToBorrow, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + constituent: withdrawConstituent, + constituentTokenAccount: withdrawConstituentTokenAccount, + spotMarket: withdrawSpotMarket.pubkey, + spotMarketVault: withdrawSpotMarket.vault, + tokenProgram: withdrawTokenProgram, + mint: withdrawSpotMarket.mint, + driftSigner: getDriftSignerPublicKey(this.program.programId), + oracle: withdrawSpotMarket.oracle, + }, + } + ); + + return { depositIx, withdrawIx }; + } + + public async depositToProgramVault( + lpPoolId: number, + depositMarketIndex: number, + amountToDeposit: BN + ): Promise { + const depositIx = await this.getDepositToProgramVaultIx( + lpPoolId, + depositMarketIndex, + amountToDeposit + ); + + const tx = await this.buildTransaction([depositIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async withdrawFromProgramVault( + lpPoolId: number, + borrowMarketIndex: number, + amountToWithdraw: BN + ): Promise { + const withdrawIx = await this.getWithdrawFromProgramVaultIx( + lpPoolId, + borrowMarketIndex, + amountToWithdraw + ); + const tx = await this.buildTransaction([withdrawIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositToProgramVaultIx( + lpPoolId: number, + depositMarketIndex: number, + amountToDeposit: BN + ): Promise { + const { depositIx } = await this.getDepositWithdrawToProgramVaultIxs( + lpPoolId, + depositMarketIndex, + depositMarketIndex, + amountToDeposit, + new BN(0) + ); + return depositIx; + } + + public async getWithdrawFromProgramVaultIx( + lpPoolId: number, + borrowMarketIndex: number, + amountToWithdraw: BN + ): Promise { + const { withdrawIx } = await this.getDepositWithdrawToProgramVaultIxs( + lpPoolId, + borrowMarketIndex, + borrowMarketIndex, + new BN(0), + amountToWithdraw + ); + return withdrawIx; + } + + public async updatePerpMarketLpPoolFeeTransferScalar( + marketIndex: number, + lpFeeTransferScalar?: number, + lpExchangeFeeExcluscionScalar?: number + ) { + const ix = await this.getUpdatePerpMarketLpPoolFeeTransferScalarIx( + marketIndex, + lpFeeTransferScalar, + lpExchangeFeeExcluscionScalar + ); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getUpdatePerpMarketLpPoolFeeTransferScalarIx( + marketIndex: number, + lpFeeTransferScalar?: number, + lpExchangeFeeExcluscionScalar?: number + ): Promise { + return this.program.instruction.updatePerpMarketLpPoolFeeTransferScalar( + lpFeeTransferScalar ?? null, + lpExchangeFeeExcluscionScalar ?? null, + { + accounts: { + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + perpMarket: this.getPerpMarketAccount(marketIndex).pubkey, + }, + } + ); + } + + public async updatePerpMarketLpPoolPausedOperations( + marketIndex: number, + pausedOperations: number + ) { + const ix = await this.getUpdatePerpMarketLpPoolPausedOperationsIx( + marketIndex, + pausedOperations + ); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getUpdatePerpMarketLpPoolPausedOperationsIx( + marketIndex: number, + pausedOperations: number + ): Promise { + return this.program.instruction.updatePerpMarketLpPoolPausedOperations( + pausedOperations, + { + accounts: { + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + perpMarket: this.getPerpMarketAccount(marketIndex).pubkey, + }, + } + ); + } + + public async mintLpWhitelistToken( + lpPool: LPPoolAccount, + authority: PublicKey + ): Promise { + const ix = await this.getMintLpWhitelistTokenIx(lpPool, authority); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getMintLpWhitelistTokenIx( + lpPool: LPPoolAccount, + authority: PublicKey + ): Promise { + const mintAmount = 1000; + const associatedTokenAccount = getAssociatedTokenAddressSync( + lpPool.whitelistMint, + authority, + false + ); + + const ixs: TransactionInstruction[] = []; + const createInstruction = + this.createAssociatedTokenAccountIdempotentInstruction( + associatedTokenAccount, + this.wallet.publicKey, + authority, + lpPool.whitelistMint + ); + ixs.push(createInstruction); + const mintToInstruction = createMintToInstruction( + lpPool.whitelistMint, + associatedTokenAccount, + this.wallet.publicKey, + mintAmount, + [], + TOKEN_PROGRAM_ID + ); + ixs.push(mintToInstruction); + return ixs; + } } diff --git a/sdk/src/constituentMap/constituentMap.ts b/sdk/src/constituentMap/constituentMap.ts new file mode 100644 index 0000000000..77f3c9ed87 --- /dev/null +++ b/sdk/src/constituentMap/constituentMap.ts @@ -0,0 +1,285 @@ +import { + Commitment, + Connection, + MemcmpFilter, + PublicKey, + RpcResponseAndContext, +} from '@solana/web3.js'; +import { ConstituentAccountSubscriber, DataAndSlot } from '../accounts/types'; +import { ConstituentAccount } from '../types'; +import { PollingConstituentAccountSubscriber } from './pollingConstituentAccountSubscriber'; +import { WebSocketConstituentAccountSubscriber } from './webSocketConstituentAccountSubscriber'; +import { DriftClient } from '../driftClient'; +import { getConstituentFilter, getConstituentLpPoolFilter } from '../memcmp'; +import { ZSTDDecoder } from 'zstddec'; +import { getLpPoolPublicKey } from '../addresses/pda'; + +const MAX_CONSTITUENT_SIZE_BYTES = 480; // TODO: update this when account is finalized + +export type ConstituentMapConfig = { + driftClient: DriftClient; + connection?: Connection; + subscriptionConfig: + | { + type: 'polling'; + frequency: number; + commitment?: Commitment; + } + | { + type: 'websocket'; + resubTimeoutMs?: number; + logResubMessages?: boolean; + commitment?: Commitment; + }; + lpPoolId?: number; + // potentially use these to filter Constituent accounts + additionalFilters?: MemcmpFilter[]; +}; + +export interface ConstituentMapInterface { + subscribe(): Promise; + unsubscribe(): Promise; + has(key: string): boolean; + get(key: string): ConstituentAccount | undefined; + getFromSpotMarketIndex( + spotMarketIndex: number + ): ConstituentAccount | undefined; + getFromConstituentIndex( + constituentIndex: number + ): ConstituentAccount | undefined; + + getWithSlot(key: string): DataAndSlot | undefined; + mustGet(key: string): Promise; + mustGetWithSlot(key: string): Promise>; +} + +export class ConstituentMap implements ConstituentMapInterface { + private driftClient: DriftClient; + private constituentMap = new Map>(); + private constituentAccountSubscriber: ConstituentAccountSubscriber; + private additionalFilters?: MemcmpFilter[]; + private commitment?: Commitment; + private connection?: Connection; + + private constituentIndexToKeyMap = new Map(); + private spotMarketIndexToKeyMap = new Map(); + + private lpPoolId: number; + + constructor(config: ConstituentMapConfig) { + this.driftClient = config.driftClient; + this.additionalFilters = config.additionalFilters; + this.commitment = config.subscriptionConfig.commitment; + this.connection = config.connection || this.driftClient.connection; + this.lpPoolId = config.lpPoolId ?? 0; + + if (config.subscriptionConfig.type === 'polling') { + this.constituentAccountSubscriber = + new PollingConstituentAccountSubscriber( + this, + this.driftClient.program, + config.subscriptionConfig.frequency, + config.subscriptionConfig.commitment, + this.getFilters() + ); + } else if (config.subscriptionConfig.type === 'websocket') { + this.constituentAccountSubscriber = + new WebSocketConstituentAccountSubscriber( + this, + this.driftClient.program, + config.subscriptionConfig.resubTimeoutMs, + config.subscriptionConfig.commitment, + this.getFilters() + ); + } + + // Listen for account updates from the subscriber + this.constituentAccountSubscriber.eventEmitter.on( + 'onAccountUpdate', + (account: ConstituentAccount, pubkey: PublicKey, slot: number) => { + this.updateConstituentAccount(pubkey.toString(), account, slot); + } + ); + } + + private getFilters(): MemcmpFilter[] { + const filters = [ + getConstituentFilter(), + getConstituentLpPoolFilter( + getLpPoolPublicKey(this.driftClient.program.programId, this.lpPoolId) + ), + ]; + if (this.additionalFilters) { + filters.push(...this.additionalFilters); + } + return filters; + } + + private decode(name: string, buffer: Buffer): ConstituentAccount { + return this.driftClient.program.account.constituent.coder.accounts.decodeUnchecked( + name, + buffer + ); + } + + public async sync(): Promise { + try { + const rpcRequestArgs = [ + this.driftClient.program.programId.toBase58(), + { + commitment: this.commitment, + filters: this.getFilters(), + encoding: 'base64+zstd', + withContext: true, + }, + ]; + + // @ts-ignore + const rpcJSONResponse: any = await this.connection._rpcRequest( + 'getProgramAccounts', + rpcRequestArgs + ); + const rpcResponseAndContext: RpcResponseAndContext< + Array<{ pubkey: PublicKey; account: { data: [string, string] } }> + > = rpcJSONResponse.result; + const slot = rpcResponseAndContext.context.slot; + + const promises = rpcResponseAndContext.value.map( + async (programAccount) => { + const compressedUserData = Buffer.from( + programAccount.account.data[0], + 'base64' + ); + const decoder = new ZSTDDecoder(); + await decoder.init(); + const buffer = Buffer.from( + decoder.decode(compressedUserData, MAX_CONSTITUENT_SIZE_BYTES) + ); + const key = programAccount.pubkey.toString(); + const currAccountWithSlot = this.getWithSlot(key); + + if (currAccountWithSlot) { + if (slot >= currAccountWithSlot.slot) { + const constituentAcc = this.decode('Constituent', buffer); + this.updateConstituentAccount(key, constituentAcc, slot); + } + } else { + const constituentAcc = this.decode('Constituent', buffer); + this.updateConstituentAccount(key, constituentAcc, slot); + } + } + ); + await Promise.all(promises); + } catch (error) { + console.log(`ConstituentMap.sync() error: ${error.message}`); + } + } + + public async subscribe(): Promise { + await this.constituentAccountSubscriber.subscribe(); + } + + public async unsubscribe(): Promise { + await this.constituentAccountSubscriber.unsubscribe(); + this.constituentMap.clear(); + } + + public has(key: string): boolean { + return this.constituentMap.has(key); + } + + public get(key: string): ConstituentAccount | undefined { + return this.constituentMap.get(key)?.data; + } + + public getFromConstituentIndex( + constituentIndex: number + ): ConstituentAccount | undefined { + const key = this.constituentIndexToKeyMap.get(constituentIndex); + return key ? this.get(key) : undefined; + } + + public getFromSpotMarketIndex( + spotMarketIndex: number + ): ConstituentAccount | undefined { + const key = this.spotMarketIndexToKeyMap.get(spotMarketIndex); + return key ? this.get(key) : undefined; + } + + public getWithSlot(key: string): DataAndSlot | undefined { + return this.constituentMap.get(key); + } + + public async mustGet(key: string): Promise { + if (!this.has(key)) { + await this.sync(); + } + const result = this.constituentMap.get(key); + if (!result) { + throw new Error(`ConstituentAccount not found for key: ${key}`); + } + return result.data; + } + + public async mustGetWithSlot( + key: string + ): Promise> { + if (!this.has(key)) { + await this.sync(); + } + const result = this.constituentMap.get(key); + if (!result) { + throw new Error(`ConstituentAccount not found for key: ${key}`); + } + return result; + } + + public size(): number { + return this.constituentMap.size; + } + + public *values(): IterableIterator { + for (const dataAndSlot of this.constituentMap.values()) { + yield dataAndSlot.data; + } + } + + public valuesWithSlot(): IterableIterator> { + return this.constituentMap.values(); + } + + public *entries(): IterableIterator<[string, ConstituentAccount]> { + for (const [key, dataAndSlot] of this.constituentMap.entries()) { + yield [key, dataAndSlot.data]; + } + } + + public entriesWithSlot(): IterableIterator< + [string, DataAndSlot] + > { + return this.constituentMap.entries(); + } + + public updateConstituentAccount( + key: string, + constituentAccount: ConstituentAccount, + slot: number + ): void { + const existingData = this.getWithSlot(key); + if (existingData) { + if (slot >= existingData.slot) { + this.constituentMap.set(key, { + data: constituentAccount, + slot, + }); + } + } else { + this.constituentMap.set(key, { + data: constituentAccount, + slot, + }); + } + this.constituentIndexToKeyMap.set(constituentAccount.constituentIndex, key); + this.spotMarketIndexToKeyMap.set(constituentAccount.spotMarketIndex, key); + } +} diff --git a/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts b/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts new file mode 100644 index 0000000000..e50b34df4b --- /dev/null +++ b/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts @@ -0,0 +1,97 @@ +import { + NotSubscribedError, + ConstituentAccountEvents, + ConstituentAccountSubscriber, +} from '../accounts/types'; +import { Program } from '@coral-xyz/anchor'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Commitment, MemcmpFilter } from '@solana/web3.js'; +import { ConstituentMap } from './constituentMap'; + +export class PollingConstituentAccountSubscriber + implements ConstituentAccountSubscriber +{ + isSubscribed: boolean; + program: Program; + frequency: number; + commitment?: Commitment; + additionalFilters?: MemcmpFilter[]; + eventEmitter: StrictEventEmitter; + + intervalId?: NodeJS.Timeout; + constituentMap: ConstituentMap; + + public constructor( + constituentMap: ConstituentMap, + program: Program, + frequency: number, + commitment?: Commitment, + additionalFilters?: MemcmpFilter[] + ) { + this.constituentMap = constituentMap; + this.isSubscribed = false; + this.program = program; + this.frequency = frequency; + this.commitment = commitment; + this.additionalFilters = additionalFilters; + this.eventEmitter = new EventEmitter(); + } + + async subscribe(): Promise { + if (this.isSubscribed || this.frequency <= 0) { + return true; + } + + const executeSync = async () => { + await this.sync(); + this.intervalId = setTimeout(executeSync, this.frequency); + }; + + // Initial sync + await this.sync(); + + // Start polling + this.intervalId = setTimeout(executeSync, this.frequency); + + this.isSubscribed = true; + return true; + } + + async sync(): Promise { + try { + await this.constituentMap.sync(); + this.eventEmitter.emit('update'); + } catch (error) { + console.log( + `PollingConstituentAccountSubscriber.sync() error: ${error.message}` + ); + this.eventEmitter.emit('error', error); + } + } + + async unsubscribe(): Promise { + if (!this.isSubscribed) { + return; + } + + if (this.intervalId) { + clearTimeout(this.intervalId); + this.intervalId = undefined; + } + + this.isSubscribed = false; + } + + assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } + + didSubscriptionSucceed(): boolean { + return this.isSubscribed; + } +} diff --git a/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts b/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts new file mode 100644 index 0000000000..816acd6211 --- /dev/null +++ b/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts @@ -0,0 +1,112 @@ +import { + NotSubscribedError, + ConstituentAccountEvents, + ConstituentAccountSubscriber, +} from '../accounts/types'; +import { Program } from '@coral-xyz/anchor'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; +import { ConstituentAccount } from '../types'; +import { WebSocketProgramAccountSubscriber } from '../accounts/webSocketProgramAccountSubscriber'; +import { getConstituentFilter } from '../memcmp'; +import { ConstituentMap } from './constituentMap'; + +export class WebSocketConstituentAccountSubscriber + implements ConstituentAccountSubscriber +{ + isSubscribed: boolean; + resubTimeoutMs?: number; + commitment?: Commitment; + program: Program; + eventEmitter: StrictEventEmitter; + + constituentDataAccountSubscriber: WebSocketProgramAccountSubscriber; + constituentMap: ConstituentMap; + private additionalFilters?: MemcmpFilter[]; + + public constructor( + constituentMap: ConstituentMap, + program: Program, + resubTimeoutMs?: number, + commitment?: Commitment, + additionalFilters?: MemcmpFilter[] + ) { + this.constituentMap = constituentMap; + this.isSubscribed = false; + this.program = program; + this.eventEmitter = new EventEmitter(); + this.resubTimeoutMs = resubTimeoutMs; + this.commitment = commitment; + this.additionalFilters = additionalFilters; + } + + async subscribe(): Promise { + if (this.isSubscribed) { + return true; + } + this.constituentDataAccountSubscriber = + new WebSocketProgramAccountSubscriber( + 'LpPoolConstituent', + 'Constituent', + this.program, + this.program.account.constituent.coder.accounts.decode.bind( + this.program.account.constituent.coder.accounts + ), + { + filters: [getConstituentFilter(), ...(this.additionalFilters || [])], + commitment: this.commitment, + } + ); + + await this.constituentDataAccountSubscriber.subscribe( + (accountId: PublicKey, account: ConstituentAccount, context: Context) => { + this.constituentMap.updateConstituentAccount( + accountId.toBase58(), + account, + context.slot + ); + this.eventEmitter.emit( + 'onAccountUpdate', + account, + accountId, + context.slot + ); + } + ); + + this.eventEmitter.emit('update'); + this.isSubscribed = true; + return true; + } + + async sync(): Promise { + try { + await this.constituentMap.sync(); + this.eventEmitter.emit('update'); + } catch (error) { + console.log( + `WebSocketConstituentAccountSubscriber.sync() error: ${error.message}` + ); + this.eventEmitter.emit('error', error); + } + } + + async unsubscribe(): Promise { + if (!this.isSubscribed) { + return; + } + + await Promise.all([this.constituentDataAccountSubscriber.unsubscribe()]); + + this.isSubscribed = false; + } + + assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index fd464aa324..8c47c2ceb1 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -65,6 +65,10 @@ import { SignedMsgOrderParamsDelegateMessage, TokenProgramFlag, PostOnlyParams, + LPPoolAccount, + ConstituentAccount, + ConstituentTargetBaseAccount, + AmmCache, } from './types'; import driftIDL from './idl/drift.json'; @@ -115,6 +119,15 @@ import { getIfRebalanceConfigPublicKey, getRevenueShareAccountPublicKey, getRevenueShareEscrowAccountPublicKey, + getConstituentTargetBasePublicKey, + getAmmConstituentMappingPublicKey, + getLpPoolPublicKey, + getConstituentPublicKey, + getAmmCachePublicKey, + getLpPoolTokenVaultPublicKey, + getConstituentVaultPublicKey, + getConstituentCorrelationsPublicKey, + getLpPoolTokenTokenAccountPublicKey, } from './addresses/pda'; import { DataAndSlot, @@ -140,6 +153,7 @@ import { decodeName, DEFAULT_USER_NAME, encodeName } from './userName'; import { MMOraclePriceData, OraclePriceData } from './oracles/types'; import { DriftClientConfig } from './driftClientConfig'; import { PollingDriftClientAccountSubscriber } from './accounts/pollingDriftClientAccountSubscriber'; +import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; import { RetryTxSender } from './tx/retryTxSender'; import { User } from './user'; import { UserSubscriptionConfig } from './userConfig'; @@ -198,8 +212,7 @@ import { getOracleId } from './oracles/oracleId'; import { SignedMsgOrderParams } from './types'; import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; -import { Commitment } from 'gill'; -import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; +import { ConstituentMap } from './constituentMap/constituentMap'; import { hasBuilder } from './math/orders'; import { RevenueShareEscrowMap } from './userMap/revenueShareEscrowMap'; import { @@ -393,8 +406,6 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, commitment: config.accountSubscription?.commitment, - programUserAccountSubscriber: - config.accountSubscription?.programUserAccountSubscriber, }; this.userStatsAccountSubscriptionConfig = { type: 'websocket', @@ -463,10 +474,7 @@ export class DriftClient { } ); } else { - const accountSubscriberClass = - config.accountSubscription?.driftClientAccountSubscriber ?? - WebSocketDriftClientAccountSubscriber; - this.accountSubscriber = new accountSubscriberClass( + this.accountSubscriber = new WebSocketDriftClientAccountSubscriber( this.program, config.perpMarketIndexes ?? [], config.spotMarketIndexes ?? [], @@ -477,7 +485,9 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, }, - config.accountSubscription?.commitment as Commitment + config.accountSubscription?.commitment, + config.accountSubscription?.perpMarketAccountSubscriber, + config.accountSubscription?.oracleAccountSubscriber ); } this.eventEmitter = this.accountSubscriber.eventEmitter; @@ -638,8 +648,7 @@ export class DriftClient { public getSpotMarketAccount( marketIndex: number ): SpotMarketAccount | undefined { - return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex) - ?.data; + return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex).data; } /** @@ -650,8 +659,7 @@ export class DriftClient { marketIndex: number ): Promise { await this.accountSubscriber.fetch(); - return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex) - ?.data; + return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex).data; } public getSpotMarketAccounts(): SpotMarketAccount[] { @@ -2810,17 +2818,19 @@ export class DriftClient { public async getAssociatedTokenAccount( marketIndex: number, useNative = true, - tokenProgram = TOKEN_PROGRAM_ID + tokenProgram = TOKEN_PROGRAM_ID, + authority = this.wallet.publicKey, + allowOwnerOffCurve = false ): Promise { const spotMarket = this.getSpotMarketAccount(marketIndex); if (useNative && spotMarket.mint.equals(WRAPPED_SOL_MINT)) { - return this.wallet.publicKey; + return authority; } const mint = spotMarket.mint; return await getAssociatedTokenAddress( mint, - this.wallet.publicKey, - undefined, + authority, + allowOwnerOffCurve, tokenProgram ); } @@ -11235,6 +11245,1076 @@ export class DriftClient { }); } + public async getLpPoolAccount(lpPoolId: number): Promise { + return (await this.program.account.lpPool.fetch( + getLpPoolPublicKey(this.program.programId, lpPoolId) + )) as LPPoolAccount; + } + + public async getConstituentTargetBaseAccount( + lpPoolId: number + ): Promise { + return (await this.program.account.constituentTargetBase.fetch( + getConstituentTargetBasePublicKey( + this.program.programId, + getLpPoolPublicKey(this.program.programId, lpPoolId) + ) + )) as ConstituentTargetBaseAccount; + } + + public async getAmmCache(): Promise { + return (await this.program.account.ammCache.fetch( + getAmmCachePublicKey(this.program.programId) + )) as AmmCache; + } + + public async updateLpConstituentTargetBase( + lpPoolId: number, + constituents: PublicKey[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateLpConstituentTargetBaseIx(lpPoolId, constituents), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateLpConstituentTargetBaseIx( + lpPoolId: number, + constituents: PublicKey[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMappingPublicKey = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + const ammCache = getAmmCachePublicKey(this.program.programId); + + const remainingAccounts = constituents.map((constituent) => { + return { + isWritable: false, + isSigner: false, + pubkey: constituent, + }; + }); + + return this.program.instruction.updateLpConstituentTargetBase({ + accounts: { + keeper: this.wallet.publicKey, + lpPool, + ammConstituentMapping: ammConstituentMappingPublicKey, + constituentTargetBase, + state: await this.getStatePublicKey(), + ammCache, + }, + remainingAccounts, + }); + } + + public async updateLpPoolAum( + lpPool: LPPoolAccount, + spotMarketIndexOfConstituents: number[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateLpPoolAumIxs(lpPool, spotMarketIndexOfConstituents), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateLpPoolAumIxs( + lpPool: LPPoolAccount, + spotMarketIndexOfConstituents: number[] + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: spotMarketIndexOfConstituents, + }); + remainingAccounts.push( + ...spotMarketIndexOfConstituents.map((index) => { + return { + pubkey: getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + index + ), + isSigner: false, + isWritable: true, + }; + }) + ); + return this.program.instruction.updateLpPoolAum({ + accounts: { + keeper: this.wallet.publicKey, + lpPool: lpPool.pubkey, + state: await this.getStatePublicKey(), + constituentTargetBase: getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ), + ammCache: getAmmCachePublicKey(this.program.programId), + }, + remainingAccounts, + }); + } + + public async updateAmmCache( + perpMarketIndexes: number[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateAmmCacheIx(perpMarketIndexes), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateAmmCacheIx( + perpMarketIndexes: number[] + ): Promise { + if (perpMarketIndexes.length > 50) { + throw new Error('Cant update more than 50 markets at once'); + } + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readablePerpMarketIndex: perpMarketIndexes, + }); + + return this.program.instruction.updateAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + keeper: this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + quoteMarket: this.getSpotMarketAccount(0).pubkey, + }, + remainingAccounts, + }); + } + + public async updateConstituentOracleInfo( + constituent: ConstituentAccount + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateConstituentOracleInfoIx(constituent), + undefined + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateConstituentOracleInfoIx( + constituent: ConstituentAccount + ): Promise { + const spotMarket = this.getSpotMarketAccount(constituent.spotMarketIndex); + return this.program.instruction.updateConstituentOracleInfo({ + accounts: { + keeper: this.wallet.publicKey, + constituent: constituent.pubkey, + state: await this.getStatePublicKey(), + oracle: spotMarket.oracle, + spotMarket: spotMarket.pubkey, + }, + }); + } + + public async lpPoolSwap( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + lpPool: PublicKey, + userAuthority: PublicKey, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolSwapIx( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + lpPool, + userAuthority + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolSwapIx( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + lpPool: PublicKey, + userAuthority: PublicKey + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex, outMarketIndex], + }); + + const constituentInTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + const constituentOutTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const userInTokenAccount = await getAssociatedTokenAddress( + this.getSpotMarketAccount(inMarketIndex).mint, + userAuthority + ); + const userOutTokenAccount = await getAssociatedTokenAddress( + this.getSpotMarketAccount(outMarketIndex).mint, + userAuthority + ); + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inMarketMint = this.getSpotMarketAccount(inMarketIndex).mint; + const outMarketMint = this.getSpotMarketAccount(outMarketIndex).mint; + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + return this.program.instruction.lpPoolSwap( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + { + remainingAccounts, + accounts: { + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + userInTokenAccount, + userOutTokenAccount, + inConstituent, + outConstituent, + inMarketMint, + outMarketMint, + authority: this.wallet.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + } + + public async viewLpPoolSwapFees( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + inTargetWeight: BN, + outTargetWeight: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getViewLpPoolSwapFeesIx( + inMarketIndex, + outMarketIndex, + inAmount, + inTargetWeight, + outTargetWeight, + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + inConstituent, + outConstituent + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getViewLpPoolSwapFeesIx( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + inTargetWeight: BN, + outTargetWeight: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex, outMarketIndex], + }); + + return this.program.instruction.viewLpPoolSwapFees( + inMarketIndex, + outMarketIndex, + inAmount, + inTargetWeight, + outTargetWeight, + { + remainingAccounts, + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + inConstituent, + outConstituent, + authority: this.wallet.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + } + + public async getCreateLpPoolTokenAccountIx( + lpPool: LPPoolAccount + ): Promise { + const lpMint = lpPool.mint; + const userLpTokenAccount = await getLpPoolTokenTokenAccountPublicKey( + lpMint, + this.wallet.publicKey + ); + + return this.createAssociatedTokenAccountIdempotentInstruction( + userLpTokenAccount, + this.wallet.publicKey, + this.wallet.publicKey, + lpMint + ); + } + + public async createLpPoolTokenAccount( + lpPool: LPPoolAccount, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getCreateLpPoolTokenAccountIx(lpPool), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async lpPoolAddLiquidity({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + txParams, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + }): Promise { + const ixs: TransactionInstruction[] = []; + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [inMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(inMarketIndex); + const inMarketMint = spotMarket.mint; + const isSolMarket = inMarketMint.equals(WRAPPED_SOL_MINT); + + let wSolTokenAccount: PublicKey | undefined; + if (isSolMarket) { + const { ixs: wSolIxs, pubkey } = + await this.getWrappedSolAccountCreationIxs(inAmount, true); + wSolTokenAccount = pubkey; + ixs.push(...wSolIxs); + } + + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const userInTokenAccount = + wSolTokenAccount ?? + (await this.getAssociatedTokenAccount(inMarketIndex, false)); + const constituentInTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const lpMint = lpPool.mint; + const userLpTokenAccount = await getLpPoolTokenTokenAccountPublicKey( + lpMint, + this.wallet.publicKey + ); + if (!(await this.checkIfAccountExists(userLpTokenAccount))) { + ixs.push( + this.createAssociatedTokenAccountIdempotentInstruction( + userLpTokenAccount, + this.wallet.publicKey, + this.wallet.publicKey, + lpMint + ) + ); + } + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + if (!lpPool.whitelistMint.equals(PublicKey.default)) { + const associatedTokenPublicKey = await getAssociatedTokenAddress( + lpPool.whitelistMint, + this.wallet.publicKey + ); + remainingAccounts.push({ + pubkey: associatedTokenPublicKey, + isWritable: false, + isSigner: false, + }); + } + + const lpPoolAddLiquidityIx = this.program.instruction.lpPoolAddLiquidity( + inMarketIndex, + inAmount, + minMintAmount, + { + remainingAccounts, + accounts: { + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + inMarketMint, + inConstituent, + userInTokenAccount, + constituentInTokenAccount, + userLpTokenAccount, + lpMint, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool.pubkey + ), + constituentTargetBase, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + ixs.push(lpPoolAddLiquidityIx); + + if (isSolMarket && wSolTokenAccount) { + ixs.push( + createCloseAccountInstruction( + wSolTokenAccount, + this.wallet.publicKey, + this.wallet.publicKey + ) + ); + } + return [...ixs]; + } + + public async viewLpPoolAddLiquidityFees({ + inMarketIndex, + inAmount, + lpPool, + txParams, + }: { + inMarketIndex: number; + inAmount: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getViewLpPoolAddLiquidityFeesIx({ + inMarketIndex, + inAmount, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getViewLpPoolAddLiquidityFeesIx({ + inMarketIndex, + inAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + lpPool: LPPoolAccount; + }): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(inMarketIndex); + const inMarketMint = spotMarket.mint; + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const lpMint = lpPool.mint; + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + return this.program.instruction.viewLpPoolAddLiquidityFees( + inMarketIndex, + inAmount, + { + accounts: { + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + inMarketMint, + inConstituent, + lpMint, + constituentTargetBase, + }, + remainingAccounts, + } + ); + } + + public async lpPoolRemoveLiquidity({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + txParams, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + }): Promise { + const ixs: TransactionInstruction[] = []; + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [outMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(outMarketIndex); + const outMarketMint = spotMarket.mint; + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + if (outMarketMint.equals(WRAPPED_SOL_MINT)) { + ixs.push( + createAssociatedTokenAccountIdempotentInstruction( + this.wallet.publicKey, + await this.getAssociatedTokenAccount(outMarketIndex, false), + this.wallet.publicKey, + WRAPPED_SOL_MINT + ) + ); + } + const userOutTokenAccount = await this.getAssociatedTokenAccount( + outMarketIndex, + false + ); + const constituentOutTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + const lpMint = lpPool.mint; + const userLpTokenAccount = await getAssociatedTokenAddress( + lpMint, + this.wallet.publicKey, + true + ); + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + ixs.push( + this.program.instruction.lpPoolRemoveLiquidity( + outMarketIndex, + lpToBurn, + minAmountOut, + { + remainingAccounts, + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + outMarketMint, + outConstituent, + userOutTokenAccount, + constituentOutTokenAccount, + userLpTokenAccount, + spotMarketTokenAccount: spotMarket.vault, + lpMint, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool.pubkey + ), + constituentTargetBase, + tokenProgram: TOKEN_PROGRAM_ID, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + } + ) + ); + return ixs; + } + + public async viewLpPoolRemoveLiquidityFees({ + outMarketIndex, + lpToBurn, + lpPool, + txParams, + }: { + outMarketIndex: number; + lpToBurn: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getViewLpPoolRemoveLiquidityFeesIx({ + outMarketIndex, + lpToBurn, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getViewLpPoolRemoveLiquidityFeesIx({ + outMarketIndex, + lpToBurn, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + lpPool: LPPoolAccount; + }): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [outMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(outMarketIndex); + const outMarketMint = spotMarket.mint; + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + const lpMint = lpPool.mint; + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + return this.program.instruction.viewLpPoolRemoveLiquidityFees( + outMarketIndex, + lpToBurn, + { + remainingAccounts, + accounts: { + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + outMarketMint, + outConstituent, + lpMint, + constituentTargetBase, + }, + } + ); + } + + public async getAllLpPoolAddLiquidityIxs( + { + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + }, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true, + view = false + ): Promise { + const ixs: TransactionInstruction[] = []; + + ixs.push( + ...(await this.getAllUpdateLpPoolAumIxs( + lpPool, + constituentMap, + includeUpdateConstituentOracleInfo + )) + ); + + if (view) { + ixs.push( + await this.getViewLpPoolAddLiquidityFeesIx({ + inMarketIndex, + inAmount, + lpPool, + }) + ); + } else { + ixs.push( + ...(await this.getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + })) + ); + } + + return ixs; + } + + public async getAllLpPoolRemoveLiquidityIxs( + { + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + }, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true, + view = false + ): Promise { + const ixs: TransactionInstruction[] = []; + ixs.push( + ...(await this.getAllSettlePerpToLpPoolIxs( + lpPool.lpPoolId, + this.getPerpMarketAccounts() + .filter((marketAccount) => marketAccount.lpStatus > 0) + .map((marketAccount) => marketAccount.marketIndex) + )) + ); + ixs.push( + ...(await this.getAllUpdateLpPoolAumIxs( + lpPool, + constituentMap, + includeUpdateConstituentOracleInfo + )) + ); + if (view) { + ixs.push( + await this.getViewLpPoolRemoveLiquidityFeesIx({ + outMarketIndex, + lpToBurn, + lpPool, + }) + ); + } else { + ixs.push( + ...(await this.getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + })) + ); + } + + return ixs; + } + + public async getAllUpdateLpPoolAumIxs( + lpPool: LPPoolAccount, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true + ): Promise { + const ixs: TransactionInstruction[] = []; + const constituents: ConstituentAccount[] = Array.from( + constituentMap.values() + ); + + if (includeUpdateConstituentOracleInfo) { + for (const constituent of constituents) { + ixs.push(await this.getUpdateConstituentOracleInfoIx(constituent)); + } + } + + const spotMarketIndexes = constituents.map( + (constituent) => constituent.spotMarketIndex + ); + ixs.push(await this.getUpdateLpPoolAumIxs(lpPool, spotMarketIndexes)); + return ixs; + } + + public async getAllUpdateConstituentTargetBaseIxs( + perpMarketIndexes: number[], + lpPool: LPPoolAccount, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true + ): Promise { + const ixs: TransactionInstruction[] = []; + + ixs.push(await this.getUpdateAmmCacheIx(perpMarketIndexes)); + + const constituents: ConstituentAccount[] = Array.from( + constituentMap.values() + ); + + if (includeUpdateConstituentOracleInfo) { + for (const constituent of constituents) { + ixs.push(await this.getUpdateConstituentOracleInfoIx(constituent)); + } + } + + ixs.push( + await this.getUpdateLpConstituentTargetBaseIx( + lpPool.lpPoolId, + Array.from(constituentMap.values()).map( + (constituent) => constituent.pubkey + ) + ) + ); + + ixs.push( + ...(await this.getAllUpdateLpPoolAumIxs(lpPool, constituentMap, false)) + ); + + return ixs; + } + + async getAllLpPoolSwapIxs( + lpPool: LPPoolAccount, + constituentMap: ConstituentMap, + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + userAuthority: PublicKey + ): Promise { + const ixs: TransactionInstruction[] = []; + ixs.push(...(await this.getAllUpdateLpPoolAumIxs(lpPool, constituentMap))); + ixs.push( + await this.getLpPoolSwapIx( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + lpPool.pubkey, + userAuthority + ) + ); + return ixs; + } + + async settlePerpToLpPool( + lpPoolId: number, + perpMarketIndexes: number[] + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getSettlePerpToLpPoolIx(lpPoolId, perpMarketIndexes), + undefined + ), + [], + this.opts + ); + return txSig; + } + + public async getSettlePerpToLpPoolIx( + lpPoolId: number, + perpMarketIndexes: number[] + ): Promise { + const remainingAccounts = []; + remainingAccounts.push( + ...perpMarketIndexes.map((index) => { + return { + pubkey: this.getPerpMarketAccount(index).pubkey, + isSigner: false, + isWritable: true, + }; + }) + ); + const quoteSpotMarketAccount = this.getQuoteSpotMarketAccount(); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + return this.program.instruction.settlePerpToLpPool({ + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + keeper: this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + quoteMarket: quoteSpotMarketAccount.pubkey, + constituent: getConstituentPublicKey(this.program.programId, lpPool, 0), + constituentQuoteTokenAccount: getConstituentVaultPublicKey( + this.program.programId, + lpPool, + 0 + ), + lpPool, + quoteTokenVault: quoteSpotMarketAccount.vault, + tokenProgram: this.getTokenProgramForSpotMarket(quoteSpotMarketAccount), + }, + remainingAccounts, + }); + } + + public async getAllSettlePerpToLpPoolIxs( + lpPoolId: number, + marketIndexes: number[] + ): Promise { + const ixs: TransactionInstruction[] = []; + ixs.push(await this.getUpdateAmmCacheIx(marketIndexes)); + ixs.push(await this.getSettlePerpToLpPoolIx(lpPoolId, marketIndexes)); + return ixs; + } + + /** + * Below here are the transaction sending functions + */ + private handleSignedTransaction(signedTxs: SignedTxData[]) { if (this.enableMetricsEvents && this.metricsEventEmitter) { this.metricsEventEmitter.emit('txSigned', signedTxs); diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index e805344539..0ddb554553 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -5,7 +5,7 @@ import { PublicKey, TransactionVersion, } from '@solana/web3.js'; -import { IWallet, TxParams, UserAccount } from './types'; +import { IWallet, TxParams } from './types'; import { OracleInfo } from './oracles/types'; import { BulkAccountLoader } from './accounts/bulkAccountLoader'; import { DriftEnv } from './config'; @@ -19,9 +19,6 @@ import { import { Coder, Program } from '@coral-xyz/anchor'; import { WebSocketAccountSubscriber } from './accounts/webSocketAccountSubscriber'; import { WebSocketAccountSubscriberV2 } from './accounts/webSocketAccountSubscriberV2'; -import { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; -import { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; -import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; import { grpcDriftClientAccountSubscriberV2 } from './accounts/grpcDriftClientAccountSubscriberV2'; import { grpcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; import { grpcMultiUserAccountSubscriber } from './accounts/grpcMultiUserAccountSubscriber'; @@ -81,7 +78,6 @@ export type DriftClientSubscriptionConfig = resubTimeoutMs?: number; logResubMessages?: boolean; commitment?: Commitment; - programUserAccountSubscriber?: WebSocketProgramAccountSubscriber; perpMarketAccountSubscriber?: new ( accountName: string, program: Program, @@ -90,17 +86,14 @@ export type DriftClientSubscriptionConfig = resubOpts?: ResubOpts, commitment?: Commitment ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; - /** If you use V2 here, whatever you pass for perpMarketAccountSubscriber and oracleAccountSubscriber will be ignored and it will use v2 under the hood regardless */ - driftClientAccountSubscriber?: new ( + oracleAccountSubscriber?: new ( + accountName: string, program: Program, - perpMarketIndexes: number[], - spotMarketIndexes: number[], - oracleInfos: OracleInfo[], - shouldFindAllMarketsAndOracles: boolean, - delistedMarketSetting: DelistedMarketSetting - ) => - | WebSocketDriftClientAccountSubscriber - | WebSocketDriftClientAccountSubscriberV2; + accountPublicKey: PublicKey, + decodeBuffer?: (buffer: Buffer) => any, + resubOpts?: ResubOpts, + commitment?: Commitment + ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; } | { type: 'polling'; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index a358214ea2..1d61c52f13 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4285,6 +4285,11 @@ "isMut": true, "isSigner": false }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, { "name": "oracle", "isMut": false, @@ -4410,9 +4415,65 @@ 32 ] } + }, + { + "name": "lpPoolId", + "type": "u8" } ] }, + { + "name": "initializeAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateInitialAmmCacheInfo", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "initializePredictionMarket", "accounts": [ @@ -4626,6 +4687,97 @@ } ] }, + { + "name": "updatePerpMarketLpPoolPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketLpPoolStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpStatus", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketLpPoolFeeTransferScalar", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "optionalLpFeeTransferScalar", + "type": { + "option": "u8" + } + }, + { + "name": "optionalLpNetPnlTransferScalar", + "type": { + "option": "u8" + } + } + ] + }, { "name": "settleExpiredMarketPoolsToRevenuePool", "accounts": [ @@ -5085,6 +5237,32 @@ } ] }, + { + "name": "updatePerpMarketLpPoolId", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolId", + "type": "u8" + } + ] + }, { "name": "updateInsuranceFundUnstakingPeriod", "accounts": [ @@ -6143,6 +6321,11 @@ "name": "oldOracle", "isMut": false, "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false } ], "args": [ @@ -7576,129 +7759,2162 @@ "type": "bool" } ] - } - ], - "accounts": [ - { - "name": "OpenbookV2FulfillmentConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "openbookV2ProgramId", - "type": "publicKey" - }, - { - "name": "openbookV2Market", - "type": "publicKey" - }, - { - "name": "openbookV2MarketAuthority", - "type": "publicKey" - }, - { - "name": "openbookV2EventHeap", - "type": "publicKey" - }, - { - "name": "openbookV2Bids", - "type": "publicKey" - }, - { - "name": "openbookV2Asks", - "type": "publicKey" - }, - { - "name": "openbookV2BaseVault", - "type": "publicKey" - }, - { - "name": "openbookV2QuoteVault", - "type": "publicKey" - }, - { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } - }, - { - "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 4 - ] - } - } - ] - } }, { - "name": "PhoenixV1FulfillmentConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "phoenixProgramId", - "type": "publicKey" - }, - { - "name": "phoenixLogAuthority", - "type": "publicKey" - }, - { - "name": "phoenixMarket", - "type": "publicKey" + "name": "initializeLpPool", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolId", + "type": "u8" + }, + { + "name": "minMintFee", + "type": "i64" + }, + { + "name": "maxAum", + "type": "u128" + }, + { + "name": "maxSettleQuoteAmountPerMarket", + "type": "u64" + }, + { + "name": "whitelistMint", + "type": "publicKey" + } + ] + }, + { + "name": "updateFeatureBitFlagsSettleLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "updateFeatureBitFlagsSwapLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "updateFeatureBitFlagsMintRedeemLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "initializeConstituent", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentVault", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "maxWeightDeviation", + "type": "i64" + }, + { + "name": "swapFeeMin", + "type": "i64" + }, + { + "name": "swapFeeMax", + "type": "i64" + }, + { + "name": "maxBorrowTokenAmount", + "type": "u64" + }, + { + "name": "oracleStalenessThreshold", + "type": "u64" + }, + { + "name": "costToTrade", + "type": "i32" + }, + { + "name": "constituentDerivativeIndex", + "type": { + "option": "i16" + } + }, + { + "name": "constituentDerivativeDepegThreshold", + "type": "u64" + }, + { + "name": "derivativeWeight", + "type": "u64" + }, + { + "name": "volatility", + "type": "u64" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" + }, + { + "name": "newConstituentCorrelations", + "type": { + "vec": "i64" + } + } + ] + }, + { + "name": "updateConstituentStatus", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "newStatus", + "type": "u8" + } + ] + }, + { + "name": "updateConstituentPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updateConstituentParams", + "accounts": [ + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "constituentParams", + "type": { + "defined": "ConstituentParams" + } + } + ] + }, + { + "name": "updateLpPoolParams", + "accounts": [ + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolParams", + "type": { + "defined": "LpPoolParams" + } + } + ] + }, + { + "name": "addAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "updateAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "removeAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "constituentIndex", + "type": "u16" + } + ] + }, + { + "name": "updateConstituentCorrelationData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "index1", + "type": "u16" + }, + { + "name": "index2", + "type": "u16" + }, + { + "name": "correlation", + "type": "i64" + } + ] + }, + { + "name": "updateLpConstituentTargetBase", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammConstituentMapping", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateLpPoolAum", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateAmmCache", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "overrideAmmCacheInfo", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "overrideParams", + "type": { + "defined": "OverrideAmmCacheParams" + } + } + ] + }, + { + "name": "resetAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "lpPoolSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u64" + } + ] + }, + { + "name": "viewLpPoolSwapFees", + "accounts": [ + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "inTargetWeight", + "type": "i64" + }, + { + "name": "outTargetWeight", + "type": "i64" + } + ] + }, + { + "name": "lpPoolAddLiquidity", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u128" + }, + { + "name": "minMintAmount", + "type": "u64" + } + ] + }, + { + "name": "lpPoolRemoveLiquidity", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u128" + } + ] + }, + { + "name": "viewLpPoolAddLiquidityFees", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": false, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u128" + } + ] + }, + { + "name": "viewLpPoolRemoveLiquidityFees", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": false, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + } + ] + }, + { + "name": "beginLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "amountIn", + "type": "u64" + } + ] + }, + { + "name": "endLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateConstituentOracleInfo", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "depositToProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "withdrawFromProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "settlePerpToLpPool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentQuoteTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "AmmCache", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "cache", + "type": { + "vec": { + "defined": "CacheInfo" + } + } + } + ] + } + }, + { + "name": "OpenbookV2FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "openbookV2ProgramId", + "type": "publicKey" + }, + { + "name": "openbookV2Market", + "type": "publicKey" + }, + { + "name": "openbookV2MarketAuthority", + "type": "publicKey" + }, + { + "name": "openbookV2EventHeap", + "type": "publicKey" + }, + { + "name": "openbookV2Bids", + "type": "publicKey" + }, + { + "name": "openbookV2Asks", + "type": "publicKey" + }, + { + "name": "openbookV2BaseVault", + "type": "publicKey" + }, + { + "name": "openbookV2QuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "PhoenixV1FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "phoenixProgramId", + "type": "publicKey" + }, + { + "name": "phoenixLogAuthority", + "type": "publicKey" + }, + { + "name": "phoenixMarket", + "type": "publicKey" + }, + { + "name": "phoenixBaseVault", + "type": "publicKey" + }, + { + "name": "phoenixQuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "SerumV3FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "serumProgramId", + "type": "publicKey" + }, + { + "name": "serumMarket", + "type": "publicKey" + }, + { + "name": "serumRequestQueue", + "type": "publicKey" + }, + { + "name": "serumEventQueue", + "type": "publicKey" + }, + { + "name": "serumBids", + "type": "publicKey" + }, + { + "name": "serumAsks", + "type": "publicKey" + }, + { + "name": "serumBaseVault", + "type": "publicKey" + }, + { + "name": "serumQuoteVault", + "type": "publicKey" + }, + { + "name": "serumOpenOrders", + "type": "publicKey" + }, + { + "name": "serumSignerNonce", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "HighLeverageModeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxUsers", + "type": "u32" + }, + { + "name": "currentUsers", + "type": "u32" + }, + { + "name": "reduceOnly", + "type": "u8" + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "currentMaintenanceUsers", + "type": "u32" + }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 24 + ] + } + } + ] + } + }, + { + "name": "IfRebalanceConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "totalInAmount", + "docs": [ + "total amount to be sold" + ], + "type": "u64" + }, + { + "name": "currentInAmount", + "docs": [ + "amount already sold" + ], + "type": "u64" + }, + { + "name": "currentOutAmount", + "docs": [ + "amount already bought" + ], + "type": "u64" + }, + { + "name": "currentOutAmountTransferred", + "docs": [ + "amount already transferred to revenue pool" + ], + "type": "u64" + }, + { + "name": "currentInAmountSinceLastTransfer", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" + }, + { + "name": "epochStartTs", + "docs": [ + "start time of epoch" + ], + "type": "i64" + }, + { + "name": "epochInAmount", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" + }, + { + "name": "epochMaxInAmount", + "docs": [ + "max amount to swap in epoch" + ], + "type": "u64" + }, + { + "name": "epochDuration", + "docs": [ + "duration of epoch" + ], + "type": "i64" + }, + { + "name": "outMarketIndex", + "docs": [ + "market index to sell" + ], + "type": "u16" + }, + { + "name": "inMarketIndex", + "docs": [ + "market index to buy" + ], + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" + }, + { + "name": "status", + "type": "u8" + }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "InsuranceFundStake", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "ifShares", + "type": "u128" + }, + { + "name": "lastWithdrawRequestShares", + "type": "u128" + }, + { + "name": "ifBase", + "type": "u128" + }, + { + "name": "lastValidTs", + "type": "i64" + }, + { + "name": "lastWithdrawRequestValue", + "type": "u64" + }, + { + "name": "lastWithdrawRequestTs", + "type": "i64" + }, + { + "name": "costBasis", + "type": "i64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 14 + ] + } + } + ] + } + }, + { + "name": "ProtocolIfSharesTransferConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "whitelistedSigners", + "type": { + "array": [ + "publicKey", + 4 + ] + } + }, + { + "name": "maxTransferPerEpoch", + "type": "u128" + }, + { + "name": "currentEpochTransfer", + "type": "u128" + }, + { + "name": "nextEpochTs", + "type": "i64" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 8 + ] + } + } + ] + } + }, + { + "name": "LPPool", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "docs": [ + "address of the vault." + ], + "type": "publicKey" + }, + { + "name": "mint", + "type": "publicKey" + }, + { + "name": "whitelistMint", + "type": "publicKey" + }, + { + "name": "constituentTargetBase", + "type": "publicKey" + }, + { + "name": "constituentCorrelations", + "type": "publicKey" + }, + { + "name": "maxAum", + "docs": [ + "The current number of VaultConstituents in the vault, each constituent is pda(LPPool.address, constituent_index)", + "which constituent is the quote, receives revenue pool distributions. (maybe this should just be implied idx 0)", + "pub quote_constituent_index: u16,", + "QUOTE_PRECISION: Max AUM, Prohibit minting new DLP beyond this" + ], + "type": "u128" + }, + { + "name": "lastAum", + "docs": [ + "QUOTE_PRECISION: AUM of the vault in USD, updated lazily" + ], + "type": "u128" + }, + { + "name": "cumulativeQuoteSentToPerpMarkets", + "docs": [ + "QUOTE PRECISION: Cumulative quotes from settles" + ], + "type": "u128" + }, + { + "name": "cumulativeQuoteReceivedFromPerpMarkets", + "type": "u128" + }, + { + "name": "totalMintRedeemFeesPaid", + "docs": [ + "QUOTE_PRECISION: Total fees paid for minting and redeeming LP tokens" + ], + "type": "i128" + }, + { + "name": "lastAumSlot", + "docs": [ + "timestamp of last AUM slot" + ], + "type": "u64" + }, + { + "name": "maxSettleQuoteAmount", + "type": "u64" + }, + { + "name": "padding", + "docs": [ + "timestamp of last vAMM revenue rebalance" + ], + "type": "u64" + }, + { + "name": "mintRedeemId", + "docs": [ + "Every mint/redeem has a monotonically increasing id. This is the next id to use" + ], + "type": "u64" + }, + { + "name": "settleId", + "type": "u64" + }, + { + "name": "minMintFee", + "docs": [ + "PERCENTAGE_PRECISION" + ], + "type": "i64" }, { - "name": "phoenixBaseVault", - "type": "publicKey" + "name": "tokenSupply", + "type": "u64" }, { - "name": "phoenixQuoteVault", - "type": "publicKey" + "name": "volatility", + "type": "u64" }, { - "name": "marketIndex", + "name": "constituents", "type": "u16" }, { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } + "name": "quoteConsituentIndex", + "type": "u16" }, { - "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } + "name": "bump", + "type": "u8" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" + }, + { + "name": "targetOracleDelayFeeBpsPer10Slots", + "type": "u8" + }, + { + "name": "targetPositionDelayFeeBpsPer10Slots", + "type": "u8" + }, + { + "name": "lpPoolId", + "type": "u8" }, { "name": "padding", "type": { "array": [ "u8", - 4 + 174 ] } } @@ -7706,76 +9922,327 @@ } }, { - "name": "SerumV3FulfillmentConfig", + "name": "Constituent", "type": { "kind": "struct", "fields": [ { "name": "pubkey", + "docs": [ + "address of the constituent" + ], "type": "publicKey" }, { - "name": "serumProgramId", + "name": "mint", "type": "publicKey" }, { - "name": "serumMarket", + "name": "lpPool", "type": "publicKey" }, { - "name": "serumRequestQueue", + "name": "vault", "type": "publicKey" }, { - "name": "serumEventQueue", - "type": "publicKey" + "name": "totalSwapFees", + "docs": [ + "total fees received by the constituent. Positive = fees received, Negative = fees paid" + ], + "type": "i128" }, { - "name": "serumBids", - "type": "publicKey" + "name": "spotBalance", + "docs": [ + "spot borrow-lend balance for constituent" + ], + "type": { + "defined": "ConstituentSpotBalance" + } }, { - "name": "serumAsks", - "type": "publicKey" + "name": "lastSpotBalanceTokenAmount", + "type": "i64" }, { - "name": "serumBaseVault", + "name": "cumulativeSpotInterestAccruedTokenAmount", + "type": "i64" + }, + { + "name": "maxWeightDeviation", + "docs": [ + "max deviation from target_weight allowed for the constituent", + "precision: PERCENTAGE_PRECISION" + ], + "type": "i64" + }, + { + "name": "swapFeeMin", + "docs": [ + "min fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" + ], + "type": "i64" + }, + { + "name": "swapFeeMax", + "docs": [ + "max fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" + ], + "type": "i64" + }, + { + "name": "maxBorrowTokenAmount", + "docs": [ + "Max Borrow amount:", + "precision: token precision" + ], + "type": "u64" + }, + { + "name": "vaultTokenBalance", + "docs": [ + "ata token balance in token precision" + ], + "type": "u64" + }, + { + "name": "lastOraclePrice", + "type": "i64" + }, + { + "name": "lastOracleSlot", + "type": "u64" + }, + { + "name": "oracleStalenessThreshold", + "docs": [ + "Delay allowed for valid AUM calculation" + ], + "type": "u64" + }, + { + "name": "flashLoanInitialTokenAmount", + "type": "u64" + }, + { + "name": "nextSwapId", + "docs": [ + "Every swap to/from this constituent has a monotonically increasing id. This is the next id to use" + ], + "type": "u64" + }, + { + "name": "derivativeWeight", + "docs": [ + "percentable of derivatve weight to go to this specific derivative PERCENTAGE_PRECISION. Zero if no derivative weight" + ], + "type": "u64" + }, + { + "name": "volatility", + "type": "u64" + }, + { + "name": "constituentDerivativeDepegThreshold", + "type": "u64" + }, + { + "name": "constituentDerivativeIndex", + "docs": [ + "The `constituent_index` of the parent constituent. -1 if it is a parent index", + "Example: if in a pool with SOL (parent) and dSOL (derivative),", + "SOL.constituent_index = 1, SOL.constituent_derivative_index = -1,", + "dSOL.constituent_index = 2, dSOL.constituent_derivative_index = 1" + ], + "type": "i16" + }, + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "constituentIndex", + "type": "u16" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "vaultBump", + "type": "u8" + }, + { + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" + }, + { + "name": "status", + "type": "u8" + }, + { + "name": "pausedOperations", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 162 + ] + } + } + ] + } + }, + { + "name": "AmmConstituentMapping", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lpPool", "type": "publicKey" }, { - "name": "serumQuoteVault", + "name": "bump", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "weights", + "type": { + "vec": { + "defined": "AmmConstituentDatum" + } + } + } + ] + } + }, + { + "name": "ConstituentTargetBase", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lpPool", "type": "publicKey" }, { - "name": "serumOpenOrders", + "name": "bump", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "targets", + "type": { + "vec": { + "defined": "TargetsDatum" + } + } + } + ] + } + }, + { + "name": "ConstituentCorrelations", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lpPool", "type": "publicKey" }, { - "name": "serumSignerNonce", + "name": "bump", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "correlations", + "type": { + "vec": "i64" + } + } + ] + } + }, + { + "name": "PrelaunchOracle", + "type": { + "kind": "struct", + "fields": [ + { + "name": "price", + "type": "i64" + }, + { + "name": "maxPrice", + "type": "i64" + }, + { + "name": "confidence", "type": "u64" }, { - "name": "marketIndex", - "type": "u16" + "name": "lastUpdateSlot", + "type": "u64" }, { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } + "name": "ammLastUpdateSlot", + "type": "u64" }, { - "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } + "name": "perpMarketIndex", + "type": "u16" }, { "name": "padding", "type": { "array": [ "u8", - 4 + 70 ] } } @@ -7783,151 +10250,311 @@ } }, { - "name": "HighLeverageModeConfig", + "name": "PerpMarket", "type": { "kind": "struct", "fields": [ { - "name": "maxUsers", - "type": "u32" + "name": "pubkey", + "docs": [ + "The perp market's address. It is a pda of the market index" + ], + "type": "publicKey" }, { - "name": "currentUsers", - "type": "u32" + "name": "amm", + "docs": [ + "The automated market maker" + ], + "type": { + "defined": "AMM" + } }, { - "name": "reduceOnly", - "type": "u8" + "name": "pnlPool", + "docs": [ + "The market's pnl pool. When users settle negative pnl, the balance increases.", + "When users settle positive pnl, the balance decreases. Can not go negative." + ], + "type": { + "defined": "PoolBalance" + } }, { - "name": "padding1", + "name": "name", + "docs": [ + "Encoded display name for the perp market e.g. SOL-PERP" + ], "type": { "array": [ "u8", - 3 + 32 ] } }, { - "name": "currentMaintenanceUsers", - "type": "u32" - }, - { - "name": "padding2", + "name": "insuranceClaim", + "docs": [ + "The perp market's claim on the insurance fund" + ], "type": { - "array": [ - "u8", - 24 - ] + "defined": "InsuranceClaim" } - } - ] - } - }, - { - "name": "IfRebalanceConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" }, { - "name": "totalInAmount", + "name": "unrealizedPnlMaxImbalance", "docs": [ - "total amount to be sold" + "The max pnl imbalance before positive pnl asset weight is discounted", + "pnl imbalance is the difference between long and short pnl. When it's greater than 0,", + "the amm has negative pnl and the initial asset weight for positive pnl is discounted", + "precision = QUOTE_PRECISION" ], "type": "u64" }, { - "name": "currentInAmount", + "name": "expiryTs", "docs": [ - "amount already sold" + "The ts when the market will be expired. Only set if market is in reduce only mode" ], - "type": "u64" + "type": "i64" }, { - "name": "currentOutAmount", + "name": "expiryPrice", "docs": [ - "amount already bought" + "The price at which positions will be settled. Only set if market is expired", + "precision = PRICE_PRECISION" + ], + "type": "i64" + }, + { + "name": "nextFillRecordId", + "docs": [ + "Every trade has a fill record id. This is the next id to be used" ], "type": "u64" }, { - "name": "currentOutAmountTransferred", + "name": "nextFundingRateRecordId", "docs": [ - "amount already transferred to revenue pool" + "Every funding rate update has a record id. This is the next id to be used" ], "type": "u64" }, { - "name": "currentInAmountSinceLastTransfer", + "name": "nextCurveRecordId", "docs": [ - "amount already bought in epoch" + "Every amm k updated has a record id. This is the next id to be used" ], "type": "u64" }, { - "name": "epochStartTs", + "name": "imfFactor", "docs": [ - "start time of epoch" + "The initial margin fraction factor. Used to increase margin ratio for large positions", + "precision: MARGIN_PRECISION" ], - "type": "i64" + "type": "u32" }, { - "name": "epochInAmount", + "name": "unrealizedPnlImfFactor", "docs": [ - "amount already bought in epoch" + "The imf factor for unrealized pnl. Used to discount asset weight for large positive pnl", + "precision: MARGIN_PRECISION" ], - "type": "u64" + "type": "u32" }, { - "name": "epochMaxInAmount", + "name": "liquidatorFee", "docs": [ - "max amount to swap in epoch" + "The fee the liquidator is paid for taking over perp position", + "precision: LIQUIDATOR_FEE_PRECISION" ], - "type": "u64" + "type": "u32" + }, + { + "name": "ifLiquidationFee", + "docs": [ + "The fee the insurance fund receives from liquidation", + "precision: LIQUIDATOR_FEE_PRECISION" + ], + "type": "u32" + }, + { + "name": "marginRatioInitial", + "docs": [ + "The margin ratio which determines how much collateral is required to open a position", + "e.g. margin ratio of .1 means a user must have $100 of total collateral to open a $1000 position", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "marginRatioMaintenance", + "docs": [ + "The margin ratio which determines when a user will be liquidated", + "e.g. margin ratio of .05 means a user must have $50 of total collateral to maintain a $1000 position", + "else they will be liquidated", + "precision: MARGIN_PRECISION" + ], + "type": "u32" + }, + { + "name": "unrealizedPnlInitialAssetWeight", + "docs": [ + "The initial asset weight for positive pnl. Negative pnl always has an asset weight of 1", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "unrealizedPnlMaintenanceAssetWeight", + "docs": [ + "The maintenance asset weight for positive pnl. Negative pnl always has an asset weight of 1", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" + }, + { + "name": "numberOfUsersWithBase", + "docs": [ + "number of users in a position (base)" + ], + "type": "u32" + }, + { + "name": "numberOfUsers", + "docs": [ + "number of users in a position (pnl) or pnl (quote)" + ], + "type": "u32" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "status", + "docs": [ + "Whether a market is active, reduce only, expired, etc", + "Affects whether users can open/close positions" + ], + "type": { + "defined": "MarketStatus" + } + }, + { + "name": "contractType", + "docs": [ + "Currently only Perpetual markets are supported" + ], + "type": { + "defined": "ContractType" + } + }, + { + "name": "contractTier", + "docs": [ + "The contract tier determines how much insurance a market can receive, with more speculative markets receiving less insurance", + "It also influences the order perp markets can be liquidated, with less speculative markets being liquidated first" + ], + "type": { + "defined": "ContractTier" + } + }, + { + "name": "pausedOperations", + "type": "u8" + }, + { + "name": "quoteSpotMarketIndex", + "docs": [ + "The spot market that pnl is settled in" + ], + "type": "u16" + }, + { + "name": "feeAdjustment", + "docs": [ + "Between -100 and 100, represents what % to increase/decrease the fee by", + "E.g. if this is -50 and the fee is 5bps, the new fee will be 2.5bps", + "if this is 50 and the fee is 5bps, the new fee will be 7.5bps" + ], + "type": "i16" + }, + { + "name": "fuelBoostPosition", + "docs": [ + "fuel multiplier for perp funding", + "precision: 10" + ], + "type": "u8" }, { - "name": "epochDuration", + "name": "fuelBoostTaker", "docs": [ - "duration of epoch" + "fuel multiplier for perp taker", + "precision: 10" ], - "type": "i64" + "type": "u8" }, { - "name": "outMarketIndex", + "name": "fuelBoostMaker", "docs": [ - "market index to sell" + "fuel multiplier for perp maker", + "precision: 10" ], - "type": "u16" + "type": "u8" }, { - "name": "inMarketIndex", - "docs": [ - "market index to buy" - ], + "name": "poolId", + "type": "u8" + }, + { + "name": "highLeverageMarginRatioInitial", "type": "u16" }, { - "name": "maxSlippageBps", + "name": "highLeverageMarginRatioMaintenance", "type": "u16" }, { - "name": "swapMode", + "name": "protectedMakerLimitPriceDivisor", "type": "u8" }, { - "name": "status", + "name": "protectedMakerDynamicDivisor", "type": "u8" }, { - "name": "padding2", + "name": "lpFeeTransferScalar", + "type": "u8" + }, + { + "name": "lpStatus", + "type": "u8" + }, + { + "name": "lpPausedOperations", + "type": "u8" + }, + { + "name": "lpExchangeFeeExcluscionScalar", + "type": "u8" + }, + { + "name": "lastFillPrice", + "type": "u64" + }, + { + "name": "lpPoolId", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ "u8", - 32 + 23 ] } } @@ -7935,90 +10562,97 @@ } }, { - "name": "InsuranceFundStake", + "name": "ProtectedMakerModeConfig", "type": { "kind": "struct", "fields": [ { - "name": "authority", - "type": "publicKey" + "name": "maxUsers", + "type": "u32" }, { - "name": "ifShares", - "type": "u128" + "name": "currentUsers", + "type": "u32" }, { - "name": "lastWithdrawRequestShares", - "type": "u128" + "name": "reduceOnly", + "type": "u8" }, { - "name": "ifBase", - "type": "u128" - }, + "name": "padding", + "type": { + "array": [ + "u8", + 31 + ] + } + } + ] + } + }, + { + "name": "PythLazerOracle", + "type": { + "kind": "struct", + "fields": [ { - "name": "lastValidTs", + "name": "price", "type": "i64" }, { - "name": "lastWithdrawRequestValue", + "name": "publishTime", "type": "u64" }, { - "name": "lastWithdrawRequestTs", - "type": "i64" - }, - { - "name": "costBasis", - "type": "i64" + "name": "postedSlot", + "type": "u64" }, { - "name": "marketIndex", - "type": "u16" + "name": "exponent", + "type": "i32" }, { "name": "padding", "type": { "array": [ "u8", - 14 + 4 ] } + }, + { + "name": "conf", + "type": "u64" } ] } }, { - "name": "ProtocolIfSharesTransferConfig", + "name": "RevenueShare", "type": { "kind": "struct", "fields": [ { - "name": "whitelistedSigners", - "type": { - "array": [ - "publicKey", - 4 - ] - } - }, - { - "name": "maxTransferPerEpoch", - "type": "u128" + "name": "authority", + "docs": [ + "the owner of this account, a builder or referrer" + ], + "type": "publicKey" }, { - "name": "currentEpochTransfer", - "type": "u128" + "name": "totalReferrerRewards", + "type": "u64" }, { - "name": "nextEpochTs", - "type": "i64" + "name": "totalBuilderRewards", + "type": "u64" }, { "name": "padding", "type": { "array": [ - "u128", - 8 + "u8", + 18 ] } } @@ -8026,1052 +10660,1001 @@ } }, { - "name": "PrelaunchOracle", + "name": "RevenueShareEscrow", "type": { "kind": "struct", "fields": [ { - "name": "price", - "type": "i64" + "name": "authority", + "docs": [ + "the owner of this account, a user" + ], + "type": "publicKey" }, { - "name": "maxPrice", - "type": "i64" + "name": "referrer", + "type": "publicKey" }, { - "name": "confidence", - "type": "u64" + "name": "referrerBoostExpireTs", + "type": "u32" }, { - "name": "lastUpdateSlot", - "type": "u64" + "name": "referrerRewardOffset", + "type": "i8" }, { - "name": "ammLastUpdateSlot", - "type": "u64" + "name": "refereeFeeNumeratorOffset", + "type": "i8" }, { - "name": "perpMarketIndex", - "type": "u16" + "name": "referrerBoostNumerator", + "type": "i8" }, { - "name": "padding", + "name": "reservedFixed", "type": { "array": [ "u8", - 70 + 17 ] } + }, + { + "name": "padding0", + "type": "u32" + }, + { + "name": "orders", + "type": { + "vec": { + "defined": "RevenueShareOrder" + } + } + }, + { + "name": "padding1", + "type": "u32" + }, + { + "name": "approvedBuilders", + "type": { + "vec": { + "defined": "BuilderInfo" + } + } } ] } }, { - "name": "PerpMarket", + "name": "SignedMsgUserOrders", + "docs": [ + "* This struct is a duplicate of SignedMsgUserOrdersZeroCopy\n * It is used to give anchor an struct to generate the idl for clients\n * The struct SignedMsgUserOrdersZeroCopy is used to load the data in efficiently" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authorityPubkey", + "type": "publicKey" + }, + { + "name": "padding", + "type": "u32" + }, + { + "name": "signedMsgOrderData", + "type": { + "vec": { + "defined": "SignedMsgOrderId" + } + } + } + ] + } + }, + { + "name": "SignedMsgWsDelegates", + "docs": [ + "* Used to store authenticated delegates for swift-like ws connections" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "delegates", + "type": { + "vec": "publicKey" + } + } + ] + } + }, + { + "name": "SpotMarket", "type": { "kind": "struct", "fields": [ { - "name": "pubkey", + "name": "pubkey", + "docs": [ + "The address of the spot market. It is a pda of the market index" + ], + "type": "publicKey" + }, + { + "name": "oracle", + "docs": [ + "The oracle used to price the markets deposits/borrows" + ], + "type": "publicKey" + }, + { + "name": "mint", + "docs": [ + "The token mint of the market" + ], + "type": "publicKey" + }, + { + "name": "vault", "docs": [ - "The perp market's address. It is a pda of the market index" + "The vault used to store the market's deposits", + "The amount in the vault should be equal to or greater than deposits - borrows" ], "type": "publicKey" }, { - "name": "amm", + "name": "name", "docs": [ - "The automated market maker" + "The encoded display name for the market e.g. SOL" ], "type": { - "defined": "AMM" + "array": [ + "u8", + 32 + ] } }, { - "name": "pnlPool", + "name": "historicalOracleData", + "type": { + "defined": "HistoricalOracleData" + } + }, + { + "name": "historicalIndexData", + "type": { + "defined": "HistoricalIndexData" + } + }, + { + "name": "revenuePool", "docs": [ - "The market's pnl pool. When users settle negative pnl, the balance increases.", - "When users settle positive pnl, the balance decreases. Can not go negative." + "Revenue the protocol has collected in this markets token", + "e.g. for SOL-PERP, funds can be settled in usdc and will flow into the USDC revenue pool" ], "type": { "defined": "PoolBalance" } }, { - "name": "name", + "name": "spotFeePool", "docs": [ - "Encoded display name for the perp market e.g. SOL-PERP" + "The fees collected from swaps between this market and the quote market", + "Is settled to the quote markets revenue pool" ], "type": { - "array": [ - "u8", - 32 - ] + "defined": "PoolBalance" } }, { - "name": "insuranceClaim", + "name": "insuranceFund", "docs": [ - "The perp market's claim on the insurance fund" + "Details on the insurance fund covering bankruptcies in this markets token", + "Covers bankruptcies for borrows with this markets token and perps settling in this markets token" ], "type": { - "defined": "InsuranceClaim" + "defined": "InsuranceFund" } }, { - "name": "unrealizedPnlMaxImbalance", + "name": "totalSpotFee", "docs": [ - "The max pnl imbalance before positive pnl asset weight is discounted", - "pnl imbalance is the difference between long and short pnl. When it's greater than 0,", - "the amm has negative pnl and the initial asset weight for positive pnl is discounted", - "precision = QUOTE_PRECISION" + "The total spot fees collected for this market", + "precision: QUOTE_PRECISION" ], - "type": "u64" + "type": "u128" }, { - "name": "expiryTs", + "name": "depositBalance", "docs": [ - "The ts when the market will be expired. Only set if market is in reduce only mode" + "The sum of the scaled balances for deposits across users and pool balances", + "To convert to the deposit token amount, multiply by the cumulative deposit interest", + "precision: SPOT_BALANCE_PRECISION" ], - "type": "i64" + "type": "u128" }, { - "name": "expiryPrice", + "name": "borrowBalance", "docs": [ - "The price at which positions will be settled. Only set if market is expired", - "precision = PRICE_PRECISION" + "The sum of the scaled balances for borrows across users and pool balances", + "To convert to the borrow token amount, multiply by the cumulative borrow interest", + "precision: SPOT_BALANCE_PRECISION" ], - "type": "i64" + "type": "u128" }, { - "name": "nextFillRecordId", + "name": "cumulativeDepositInterest", "docs": [ - "Every trade has a fill record id. This is the next id to be used" + "The cumulative interest earned by depositors", + "Used to calculate the deposit token amount from the deposit balance", + "precision: SPOT_CUMULATIVE_INTEREST_PRECISION" + ], + "type": "u128" + }, + { + "name": "cumulativeBorrowInterest", + "docs": [ + "The cumulative interest earned by borrowers", + "Used to calculate the borrow token amount from the borrow balance", + "precision: SPOT_CUMULATIVE_INTEREST_PRECISION" + ], + "type": "u128" + }, + { + "name": "totalSocialLoss", + "docs": [ + "The total socialized loss from borrows, in the mint's token", + "precision: token mint precision" + ], + "type": "u128" + }, + { + "name": "totalQuoteSocialLoss", + "docs": [ + "The total socialized loss from borrows, in the quote market's token", + "preicision: QUOTE_PRECISION" + ], + "type": "u128" + }, + { + "name": "withdrawGuardThreshold", + "docs": [ + "no withdraw limits/guards when deposits below this threshold", + "precision: token mint precision" ], "type": "u64" }, { - "name": "nextFundingRateRecordId", + "name": "maxTokenDeposits", "docs": [ - "Every funding rate update has a record id. This is the next id to be used" + "The max amount of token deposits in this market", + "0 if there is no limit", + "precision: token mint precision" ], "type": "u64" }, { - "name": "nextCurveRecordId", + "name": "depositTokenTwap", "docs": [ - "Every amm k updated has a record id. This is the next id to be used" + "24hr average of deposit token amount", + "precision: token mint precision" ], "type": "u64" }, { - "name": "imfFactor", + "name": "borrowTokenTwap", "docs": [ - "The initial margin fraction factor. Used to increase margin ratio for large positions", - "precision: MARGIN_PRECISION" + "24hr average of borrow token amount", + "precision: token mint precision" ], - "type": "u32" + "type": "u64" }, { - "name": "unrealizedPnlImfFactor", + "name": "utilizationTwap", "docs": [ - "The imf factor for unrealized pnl. Used to discount asset weight for large positive pnl", - "precision: MARGIN_PRECISION" + "24hr average of utilization", + "which is borrow amount over token amount", + "precision: SPOT_UTILIZATION_PRECISION" ], - "type": "u32" + "type": "u64" }, { - "name": "liquidatorFee", + "name": "lastInterestTs", "docs": [ - "The fee the liquidator is paid for taking over perp position", - "precision: LIQUIDATOR_FEE_PRECISION" + "Last time the cumulative deposit and borrow interest was updated" ], - "type": "u32" + "type": "u64" }, { - "name": "ifLiquidationFee", + "name": "lastTwapTs", "docs": [ - "The fee the insurance fund receives from liquidation", - "precision: LIQUIDATOR_FEE_PRECISION" + "Last time the deposit/borrow/utilization averages were updated" ], - "type": "u32" + "type": "u64" }, { - "name": "marginRatioInitial", + "name": "expiryTs", "docs": [ - "The margin ratio which determines how much collateral is required to open a position", - "e.g. margin ratio of .1 means a user must have $100 of total collateral to open a $1000 position", - "precision: MARGIN_PRECISION" + "The time the market is set to expire. Only set if market is in reduce only mode" ], - "type": "u32" + "type": "i64" }, { - "name": "marginRatioMaintenance", + "name": "orderStepSize", "docs": [ - "The margin ratio which determines when a user will be liquidated", - "e.g. margin ratio of .05 means a user must have $50 of total collateral to maintain a $1000 position", - "else they will be liquidated", - "precision: MARGIN_PRECISION" + "Spot orders must be a multiple of the step size", + "precision: token mint precision" ], - "type": "u32" + "type": "u64" }, { - "name": "unrealizedPnlInitialAssetWeight", + "name": "orderTickSize", "docs": [ - "The initial asset weight for positive pnl. Negative pnl always has an asset weight of 1", - "precision: SPOT_WEIGHT_PRECISION" + "Spot orders must be a multiple of the tick size", + "precision: PRICE_PRECISION" ], - "type": "u32" + "type": "u64" }, { - "name": "unrealizedPnlMaintenanceAssetWeight", + "name": "minOrderSize", "docs": [ - "The maintenance asset weight for positive pnl. Negative pnl always has an asset weight of 1", - "precision: SPOT_WEIGHT_PRECISION" + "The minimum order size", + "precision: token mint precision" ], - "type": "u32" + "type": "u64" }, { - "name": "numberOfUsersWithBase", + "name": "maxPositionSize", "docs": [ - "number of users in a position (base)" + "The maximum spot position size", + "if the limit is 0, there is no limit", + "precision: token mint precision" ], - "type": "u32" + "type": "u64" }, { - "name": "numberOfUsers", + "name": "nextFillRecordId", "docs": [ - "number of users in a position (pnl) or pnl (quote)" + "Every spot trade has a fill record id. This is the next id to use" ], - "type": "u32" + "type": "u64" }, { - "name": "marketIndex", - "type": "u16" + "name": "nextDepositRecordId", + "docs": [ + "Every deposit has a deposit record id. This is the next id to use" + ], + "type": "u64" }, { - "name": "status", + "name": "initialAssetWeight", "docs": [ - "Whether a market is active, reduce only, expired, etc", - "Affects whether users can open/close positions" + "The initial asset weight used to calculate a deposits contribution to a users initial total collateral", + "e.g. if the asset weight is .8, $100 of deposits contributes $80 to the users initial total collateral", + "precision: SPOT_WEIGHT_PRECISION" ], - "type": { - "defined": "MarketStatus" - } + "type": "u32" }, { - "name": "contractType", + "name": "maintenanceAssetWeight", "docs": [ - "Currently only Perpetual markets are supported" + "The maintenance asset weight used to calculate a deposits contribution to a users maintenance total collateral", + "e.g. if the asset weight is .9, $100 of deposits contributes $90 to the users maintenance total collateral", + "precision: SPOT_WEIGHT_PRECISION" ], - "type": { - "defined": "ContractType" - } + "type": "u32" }, { - "name": "contractTier", + "name": "initialLiabilityWeight", "docs": [ - "The contract tier determines how much insurance a market can receive, with more speculative markets receiving less insurance", - "It also influences the order perp markets can be liquidated, with less speculative markets being liquidated first" + "The initial liability weight used to calculate a borrows contribution to a users initial margin requirement", + "e.g. if the liability weight is .9, $100 of borrows contributes $90 to the users initial margin requirement", + "precision: SPOT_WEIGHT_PRECISION" ], - "type": { - "defined": "ContractTier" - } + "type": "u32" }, { - "name": "pausedOperations", - "type": "u8" + "name": "maintenanceLiabilityWeight", + "docs": [ + "The maintenance liability weight used to calculate a borrows contribution to a users maintenance margin requirement", + "e.g. if the liability weight is .8, $100 of borrows contributes $80 to the users maintenance margin requirement", + "precision: SPOT_WEIGHT_PRECISION" + ], + "type": "u32" }, { - "name": "quoteSpotMarketIndex", + "name": "imfFactor", "docs": [ - "The spot market that pnl is settled in" + "The initial margin fraction factor. Used to increase liability weight/decrease asset weight for large positions", + "precision: MARGIN_PRECISION" ], - "type": "u16" + "type": "u32" }, { - "name": "feeAdjustment", + "name": "liquidatorFee", "docs": [ - "Between -100 and 100, represents what % to increase/decrease the fee by", - "E.g. if this is -50 and the fee is 5bps, the new fee will be 2.5bps", - "if this is 50 and the fee is 5bps, the new fee will be 7.5bps" + "The fee the liquidator is paid for taking over borrow/deposit", + "precision: LIQUIDATOR_FEE_PRECISION" ], - "type": "i16" + "type": "u32" }, { - "name": "fuelBoostPosition", + "name": "ifLiquidationFee", "docs": [ - "fuel multiplier for perp funding", - "precision: 10" + "The fee the insurance fund receives from liquidation", + "precision: LIQUIDATOR_FEE_PRECISION" ], - "type": "u8" + "type": "u32" }, { - "name": "fuelBoostTaker", + "name": "optimalUtilization", "docs": [ - "fuel multiplier for perp taker", - "precision: 10" + "The optimal utilization rate for this market.", + "Used to determine the markets borrow rate", + "precision: SPOT_UTILIZATION_PRECISION" ], - "type": "u8" + "type": "u32" }, { - "name": "fuelBoostMaker", + "name": "optimalBorrowRate", "docs": [ - "fuel multiplier for perp maker", - "precision: 10" + "The borrow rate for this market when the market has optimal utilization", + "precision: SPOT_RATE_PRECISION" ], - "type": "u8" + "type": "u32" }, { - "name": "poolId", - "type": "u8" + "name": "maxBorrowRate", + "docs": [ + "The borrow rate for this market when the market has 1000 utilization", + "precision: SPOT_RATE_PRECISION" + ], + "type": "u32" }, { - "name": "highLeverageMarginRatioInitial", - "type": "u16" + "name": "decimals", + "docs": [ + "The market's token mint's decimals. To from decimals to a precision, 10^decimals" + ], + "type": "u32" }, { - "name": "highLeverageMarginRatioMaintenance", + "name": "marketIndex", "type": "u16" }, { - "name": "protectedMakerLimitPriceDivisor", - "type": "u8" - }, - { - "name": "protectedMakerDynamicDivisor", - "type": "u8" - }, - { - "name": "padding1", - "type": "u32" - }, - { - "name": "lastFillPrice", - "type": "u64" + "name": "ordersEnabled", + "docs": [ + "Whether or not spot trading is enabled" + ], + "type": "bool" }, { - "name": "padding", + "name": "oracleSource", "type": { - "array": [ - "u8", - 24 - ] + "defined": "OracleSource" } - } - ] - } - }, - { - "name": "ProtectedMakerModeConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "maxUsers", - "type": "u32" - }, - { - "name": "currentUsers", - "type": "u32" - }, - { - "name": "reduceOnly", - "type": "u8" }, { - "name": "padding", + "name": "status", "type": { - "array": [ - "u8", - 31 - ] + "defined": "MarketStatus" } - } - ] - } - }, - { - "name": "PythLazerOracle", - "type": { - "kind": "struct", - "fields": [ - { - "name": "price", - "type": "i64" }, { - "name": "publishTime", - "type": "u64" + "name": "assetTier", + "docs": [ + "The asset tier affects how a deposit can be used as collateral and the priority for a borrow being liquidated" + ], + "type": { + "defined": "AssetTier" + } }, { - "name": "postedSlot", - "type": "u64" + "name": "pausedOperations", + "type": "u8" }, { - "name": "exponent", - "type": "i32" + "name": "ifPausedOperations", + "type": "u8" }, { - "name": "padding", - "type": { - "array": [ - "u8", - 4 - ] - } + "name": "feeAdjustment", + "type": "i16" }, { - "name": "conf", - "type": "u64" - } - ] - } - }, - { - "name": "RevenueShare", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", + "name": "maxTokenBorrowsFraction", "docs": [ - "the owner of this account, a builder or referrer" + "What fraction of max_token_deposits", + "disabled when 0, 1 => 1/10000 => .01% of max_token_deposits", + "precision: X/10000" ], - "type": "publicKey" + "type": "u16" }, { - "name": "totalReferrerRewards", + "name": "flashLoanAmount", + "docs": [ + "For swaps, the amount of token loaned out in the begin_swap ix", + "precision: token mint precision" + ], "type": "u64" }, { - "name": "totalBuilderRewards", + "name": "flashLoanInitialTokenAmount", + "docs": [ + "For swaps, the amount in the users token account in the begin_swap ix", + "Used to calculate how much of the token left the system in end_swap ix", + "precision: token mint precision" + ], "type": "u64" }, { - "name": "padding", - "type": { - "array": [ - "u8", - 18 - ] - } - } - ] - } - }, - { - "name": "RevenueShareEscrow", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", + "name": "totalSwapFee", "docs": [ - "the owner of this account, a user" + "The total fees received from swaps", + "precision: token mint precision" ], - "type": "publicKey" - }, - { - "name": "referrer", - "type": "publicKey" + "type": "u64" }, { - "name": "referrerBoostExpireTs", - "type": "u32" + "name": "scaleInitialAssetWeightStart", + "docs": [ + "When to begin scaling down the initial asset weight", + "disabled when 0", + "precision: QUOTE_PRECISION" + ], + "type": "u64" }, { - "name": "referrerRewardOffset", - "type": "i8" + "name": "minBorrowRate", + "docs": [ + "The min borrow rate for this market when the market regardless of utilization", + "1 => 1/200 => .5%", + "precision: X/200" + ], + "type": "u8" }, { - "name": "refereeFeeNumeratorOffset", - "type": "i8" + "name": "fuelBoostDeposits", + "docs": [ + "fuel multiplier for spot deposits", + "precision: 10" + ], + "type": "u8" }, { - "name": "referrerBoostNumerator", - "type": "i8" + "name": "fuelBoostBorrows", + "docs": [ + "fuel multiplier for spot borrows", + "precision: 10" + ], + "type": "u8" }, { - "name": "reservedFixed", - "type": { - "array": [ - "u8", - 17 - ] - } + "name": "fuelBoostTaker", + "docs": [ + "fuel multiplier for spot taker", + "precision: 10" + ], + "type": "u8" }, { - "name": "padding0", - "type": "u32" + "name": "fuelBoostMaker", + "docs": [ + "fuel multiplier for spot maker", + "precision: 10" + ], + "type": "u8" }, { - "name": "orders", - "type": { - "vec": { - "defined": "RevenueShareOrder" - } - } + "name": "fuelBoostInsurance", + "docs": [ + "fuel multiplier for spot insurance stake", + "precision: 10" + ], + "type": "u8" }, { - "name": "padding1", - "type": "u32" + "name": "tokenProgramFlag", + "type": "u8" }, { - "name": "approvedBuilders", - "type": { - "vec": { - "defined": "BuilderInfo" - } - } - } - ] - } - }, - { - "name": "SignedMsgUserOrders", - "docs": [ - "* This struct is a duplicate of SignedMsgUserOrdersZeroCopy\n * It is used to give anchor an struct to generate the idl for clients\n * The struct SignedMsgUserOrdersZeroCopy is used to load the data in efficiently" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "authorityPubkey", - "type": "publicKey" + "name": "poolId", + "type": "u8" }, { "name": "padding", - "type": "u32" - }, - { - "name": "signedMsgOrderData", - "type": { - "vec": { - "defined": "SignedMsgOrderId" - } - } - } - ] - } - }, - { - "name": "SignedMsgWsDelegates", - "docs": [ - "* Used to store authenticated delegates for swift-like ws connections" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "delegates", "type": { - "vec": "publicKey" + "array": [ + "u8", + 40 + ] } } ] } }, { - "name": "SpotMarket", + "name": "State", "type": { "kind": "struct", "fields": [ { - "name": "pubkey", - "docs": [ - "The address of the spot market. It is a pda of the market index" - ], + "name": "admin", "type": "publicKey" }, { - "name": "oracle", - "docs": [ - "The oracle used to price the markets deposits/borrows" - ], + "name": "whitelistMint", "type": "publicKey" }, { - "name": "mint", - "docs": [ - "The token mint of the market" - ], + "name": "discountMint", "type": "publicKey" }, { - "name": "vault", - "docs": [ - "The vault used to store the market's deposits", - "The amount in the vault should be equal to or greater than deposits - borrows" - ], + "name": "signer", "type": "publicKey" }, { - "name": "name", - "docs": [ - "The encoded display name for the market e.g. SOL" - ], - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "historicalOracleData", - "type": { - "defined": "HistoricalOracleData" - } - }, - { - "name": "historicalIndexData", - "type": { - "defined": "HistoricalIndexData" - } + "name": "srmVault", + "type": "publicKey" }, { - "name": "revenuePool", - "docs": [ - "Revenue the protocol has collected in this markets token", - "e.g. for SOL-PERP, funds can be settled in usdc and will flow into the USDC revenue pool" - ], + "name": "perpFeeStructure", "type": { - "defined": "PoolBalance" + "defined": "FeeStructure" } }, { - "name": "spotFeePool", - "docs": [ - "The fees collected from swaps between this market and the quote market", - "Is settled to the quote markets revenue pool" - ], + "name": "spotFeeStructure", "type": { - "defined": "PoolBalance" + "defined": "FeeStructure" } }, { - "name": "insuranceFund", - "docs": [ - "Details on the insurance fund covering bankruptcies in this markets token", - "Covers bankruptcies for borrows with this markets token and perps settling in this markets token" - ], + "name": "oracleGuardRails", "type": { - "defined": "InsuranceFund" + "defined": "OracleGuardRails" } }, { - "name": "totalSpotFee", - "docs": [ - "The total spot fees collected for this market", - "precision: QUOTE_PRECISION" - ], - "type": "u128" + "name": "numberOfAuthorities", + "type": "u64" }, { - "name": "depositBalance", - "docs": [ - "The sum of the scaled balances for deposits across users and pool balances", - "To convert to the deposit token amount, multiply by the cumulative deposit interest", - "precision: SPOT_BALANCE_PRECISION" - ], - "type": "u128" + "name": "numberOfSubAccounts", + "type": "u64" }, { - "name": "borrowBalance", - "docs": [ - "The sum of the scaled balances for borrows across users and pool balances", - "To convert to the borrow token amount, multiply by the cumulative borrow interest", - "precision: SPOT_BALANCE_PRECISION" - ], - "type": "u128" + "name": "lpCooldownTime", + "type": "u64" }, { - "name": "cumulativeDepositInterest", - "docs": [ - "The cumulative interest earned by depositors", - "Used to calculate the deposit token amount from the deposit balance", - "precision: SPOT_CUMULATIVE_INTEREST_PRECISION" - ], - "type": "u128" + "name": "liquidationMarginBufferRatio", + "type": "u32" }, { - "name": "cumulativeBorrowInterest", - "docs": [ - "The cumulative interest earned by borrowers", - "Used to calculate the borrow token amount from the borrow balance", - "precision: SPOT_CUMULATIVE_INTEREST_PRECISION" - ], - "type": "u128" + "name": "settlementDuration", + "type": "u16" }, { - "name": "totalSocialLoss", - "docs": [ - "The total socialized loss from borrows, in the mint's token", - "precision: token mint precision" - ], - "type": "u128" + "name": "numberOfMarkets", + "type": "u16" }, { - "name": "totalQuoteSocialLoss", - "docs": [ - "The total socialized loss from borrows, in the quote market's token", - "preicision: QUOTE_PRECISION" - ], - "type": "u128" + "name": "numberOfSpotMarkets", + "type": "u16" }, { - "name": "withdrawGuardThreshold", - "docs": [ - "no withdraw limits/guards when deposits below this threshold", - "precision: token mint precision" - ], - "type": "u64" + "name": "signerNonce", + "type": "u8" }, { - "name": "maxTokenDeposits", - "docs": [ - "The max amount of token deposits in this market", - "0 if there is no limit", - "precision: token mint precision" - ], - "type": "u64" + "name": "minPerpAuctionDuration", + "type": "u8" }, { - "name": "depositTokenTwap", - "docs": [ - "24hr average of deposit token amount", - "precision: token mint precision" - ], - "type": "u64" + "name": "defaultMarketOrderTimeInForce", + "type": "u8" }, { - "name": "borrowTokenTwap", - "docs": [ - "24hr average of borrow token amount", - "precision: token mint precision" - ], - "type": "u64" + "name": "defaultSpotAuctionDuration", + "type": "u8" }, { - "name": "utilizationTwap", - "docs": [ - "24hr average of utilization", - "which is borrow amount over token amount", - "precision: SPOT_UTILIZATION_PRECISION" - ], - "type": "u64" + "name": "exchangeStatus", + "type": "u8" }, { - "name": "lastInterestTs", - "docs": [ - "Last time the cumulative deposit and borrow interest was updated" - ], - "type": "u64" + "name": "liquidationDuration", + "type": "u8" }, { - "name": "lastTwapTs", - "docs": [ - "Last time the deposit/borrow/utilization averages were updated" - ], - "type": "u64" + "name": "initialPctToLiquidate", + "type": "u16" }, { - "name": "expiryTs", - "docs": [ - "The time the market is set to expire. Only set if market is in reduce only mode" - ], - "type": "i64" + "name": "maxNumberOfSubAccounts", + "type": "u16" }, { - "name": "orderStepSize", - "docs": [ - "Spot orders must be a multiple of the step size", - "precision: token mint precision" - ], - "type": "u64" + "name": "maxInitializeUserFee", + "type": "u16" }, { - "name": "orderTickSize", - "docs": [ - "Spot orders must be a multiple of the tick size", - "precision: PRICE_PRECISION" - ], - "type": "u64" + "name": "featureBitFlags", + "type": "u8" }, { - "name": "minOrderSize", - "docs": [ - "The minimum order size", - "precision: token mint precision" - ], - "type": "u64" + "name": "lpPoolFeatureBitFlags", + "type": "u8" }, { - "name": "maxPositionSize", - "docs": [ - "The maximum spot position size", - "if the limit is 0, there is no limit", - "precision: token mint precision" - ], - "type": "u64" - }, + "name": "padding", + "type": { + "array": [ + "u8", + 8 + ] + } + } + ] + } + }, + { + "name": "User", + "type": { + "kind": "struct", + "fields": [ { - "name": "nextFillRecordId", + "name": "authority", "docs": [ - "Every spot trade has a fill record id. This is the next id to use" + "The owner/authority of the account" ], - "type": "u64" + "type": "publicKey" }, { - "name": "nextDepositRecordId", + "name": "delegate", "docs": [ - "Every deposit has a deposit record id. This is the next id to use" + "An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw" ], - "type": "u64" + "type": "publicKey" }, { - "name": "initialAssetWeight", + "name": "name", "docs": [ - "The initial asset weight used to calculate a deposits contribution to a users initial total collateral", - "e.g. if the asset weight is .8, $100 of deposits contributes $80 to the users initial total collateral", - "precision: SPOT_WEIGHT_PRECISION" + "Encoded display name e.g. \"toly\"" ], - "type": "u32" + "type": { + "array": [ + "u8", + 32 + ] + } }, { - "name": "maintenanceAssetWeight", + "name": "spotPositions", "docs": [ - "The maintenance asset weight used to calculate a deposits contribution to a users maintenance total collateral", - "e.g. if the asset weight is .9, $100 of deposits contributes $90 to the users maintenance total collateral", - "precision: SPOT_WEIGHT_PRECISION" + "The user's spot positions" ], - "type": "u32" + "type": { + "array": [ + { + "defined": "SpotPosition" + }, + 8 + ] + } }, { - "name": "initialLiabilityWeight", + "name": "perpPositions", "docs": [ - "The initial liability weight used to calculate a borrows contribution to a users initial margin requirement", - "e.g. if the liability weight is .9, $100 of borrows contributes $90 to the users initial margin requirement", - "precision: SPOT_WEIGHT_PRECISION" + "The user's perp positions" ], - "type": "u32" + "type": { + "array": [ + { + "defined": "PerpPosition" + }, + 8 + ] + } }, { - "name": "maintenanceLiabilityWeight", + "name": "orders", "docs": [ - "The maintenance liability weight used to calculate a borrows contribution to a users maintenance margin requirement", - "e.g. if the liability weight is .8, $100 of borrows contributes $80 to the users maintenance margin requirement", - "precision: SPOT_WEIGHT_PRECISION" + "The user's orders" ], - "type": "u32" + "type": { + "array": [ + { + "defined": "Order" + }, + 32 + ] + } }, { - "name": "imfFactor", + "name": "lastAddPerpLpSharesTs", "docs": [ - "The initial margin fraction factor. Used to increase liability weight/decrease asset weight for large positions", - "precision: MARGIN_PRECISION" + "The last time the user added perp lp positions" ], - "type": "u32" + "type": "i64" }, { - "name": "liquidatorFee", + "name": "totalDeposits", "docs": [ - "The fee the liquidator is paid for taking over borrow/deposit", - "precision: LIQUIDATOR_FEE_PRECISION" + "The total values of deposits the user has made", + "precision: QUOTE_PRECISION" ], - "type": "u32" + "type": "u64" }, { - "name": "ifLiquidationFee", + "name": "totalWithdraws", "docs": [ - "The fee the insurance fund receives from liquidation", - "precision: LIQUIDATOR_FEE_PRECISION" + "The total values of withdrawals the user has made", + "precision: QUOTE_PRECISION" ], - "type": "u32" + "type": "u64" }, { - "name": "optimalUtilization", + "name": "totalSocialLoss", "docs": [ - "The optimal utilization rate for this market.", - "Used to determine the markets borrow rate", - "precision: SPOT_UTILIZATION_PRECISION" + "The total socialized loss the users has incurred upon the protocol", + "precision: QUOTE_PRECISION" ], - "type": "u32" + "type": "u64" }, { - "name": "optimalBorrowRate", + "name": "settledPerpPnl", "docs": [ - "The borrow rate for this market when the market has optimal utilization", - "precision: SPOT_RATE_PRECISION" + "Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps", + "precision: QUOTE_PRECISION" ], - "type": "u32" + "type": "i64" }, { - "name": "maxBorrowRate", + "name": "cumulativeSpotFees", "docs": [ - "The borrow rate for this market when the market has 1000 utilization", - "precision: SPOT_RATE_PRECISION" + "Fees (taker fees, maker rebate, filler reward) for spot", + "precision: QUOTE_PRECISION" ], - "type": "u32" + "type": "i64" }, { - "name": "decimals", + "name": "cumulativePerpFunding", "docs": [ - "The market's token mint's decimals. To from decimals to a precision, 10^decimals" + "Cumulative funding paid/received for perps", + "precision: QUOTE_PRECISION" ], - "type": "u32" - }, - { - "name": "marketIndex", - "type": "u16" + "type": "i64" }, { - "name": "ordersEnabled", + "name": "liquidationMarginFreed", "docs": [ - "Whether or not spot trading is enabled" + "The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time", + "Defaults to zero when not being liquidated", + "precision: QUOTE_PRECISION" ], - "type": "bool" - }, - { - "name": "oracleSource", - "type": { - "defined": "OracleSource" - } - }, - { - "name": "status", - "type": { - "defined": "MarketStatus" - } + "type": "u64" }, { - "name": "assetTier", + "name": "lastActiveSlot", "docs": [ - "The asset tier affects how a deposit can be used as collateral and the priority for a borrow being liquidated" + "The last slot a user was active. Used to determine if a user is idle" ], - "type": { - "defined": "AssetTier" - } - }, - { - "name": "pausedOperations", - "type": "u8" - }, - { - "name": "ifPausedOperations", - "type": "u8" - }, - { - "name": "feeAdjustment", - "type": "i16" + "type": "u64" }, { - "name": "maxTokenBorrowsFraction", + "name": "nextOrderId", "docs": [ - "What fraction of max_token_deposits", - "disabled when 0, 1 => 1/10000 => .01% of max_token_deposits", - "precision: X/10000" + "Every user order has an order id. This is the next order id to be used" ], - "type": "u16" + "type": "u32" }, { - "name": "flashLoanAmount", + "name": "maxMarginRatio", "docs": [ - "For swaps, the amount of token loaned out in the begin_swap ix", - "precision: token mint precision" + "Custom max initial margin ratio for the user" ], - "type": "u64" + "type": "u32" }, { - "name": "flashLoanInitialTokenAmount", + "name": "nextLiquidationId", "docs": [ - "For swaps, the amount in the users token account in the begin_swap ix", - "Used to calculate how much of the token left the system in end_swap ix", - "precision: token mint precision" + "The next liquidation id to be used for user" ], - "type": "u64" + "type": "u16" }, { - "name": "totalSwapFee", + "name": "subAccountId", "docs": [ - "The total fees received from swaps", - "precision: token mint precision" + "The sub account id for this user" ], - "type": "u64" + "type": "u16" }, { - "name": "scaleInitialAssetWeightStart", + "name": "status", "docs": [ - "When to begin scaling down the initial asset weight", - "disabled when 0", - "precision: QUOTE_PRECISION" + "Whether the user is active, being liquidated or bankrupt" ], - "type": "u64" + "type": "u8" }, { - "name": "minBorrowRate", + "name": "isMarginTradingEnabled", "docs": [ - "The min borrow rate for this market when the market regardless of utilization", - "1 => 1/200 => .5%", - "precision: X/200" + "Whether the user has enabled margin trading" ], - "type": "u8" + "type": "bool" }, { - "name": "fuelBoostDeposits", + "name": "idle", "docs": [ - "fuel multiplier for spot deposits", - "precision: 10" + "User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows", + "Off-chain keeper bots can ignore users that are idle" ], - "type": "u8" + "type": "bool" }, { - "name": "fuelBoostBorrows", + "name": "openOrders", "docs": [ - "fuel multiplier for spot borrows", - "precision: 10" + "number of open orders" ], "type": "u8" }, { - "name": "fuelBoostTaker", + "name": "hasOpenOrder", "docs": [ - "fuel multiplier for spot taker", - "precision: 10" + "Whether or not user has open order" ], - "type": "u8" + "type": "bool" }, { - "name": "fuelBoostMaker", + "name": "openAuctions", "docs": [ - "fuel multiplier for spot maker", - "precision: 10" + "number of open orders with auction" ], "type": "u8" }, { - "name": "fuelBoostInsurance", + "name": "hasOpenAuction", "docs": [ - "fuel multiplier for spot insurance stake", - "precision: 10" + "Whether or not user has open order with auction" ], - "type": "u8" + "type": "bool" }, { - "name": "tokenProgramFlag", - "type": "u8" + "name": "marginMode", + "type": { + "defined": "MarginMode" + } }, { "name": "poolId", "type": "u8" }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "lastFuelBonusUpdateTs", + "type": "u32" + }, { "name": "padding", "type": { "array": [ "u8", - 40 + 12 ] } } @@ -9079,122 +11662,199 @@ } }, { - "name": "State", + "name": "UserStats", "type": { "kind": "struct", "fields": [ { - "name": "admin", + "name": "authority", + "docs": [ + "The authority for all of a users sub accounts" + ], "type": "publicKey" }, { - "name": "whitelistMint", + "name": "referrer", + "docs": [ + "The address that referred this user" + ], "type": "publicKey" }, { - "name": "discountMint", - "type": "publicKey" + "name": "fees", + "docs": [ + "Stats on the fees paid by the user" + ], + "type": { + "defined": "UserFees" + } }, { - "name": "signer", - "type": "publicKey" + "name": "nextEpochTs", + "docs": [ + "The timestamp of the next epoch", + "Epoch is used to limit referrer rewards earned in single epoch" + ], + "type": "i64" }, { - "name": "srmVault", - "type": "publicKey" + "name": "makerVolume30d", + "docs": [ + "Rolling 30day maker volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" }, { - "name": "perpFeeStructure", - "type": { - "defined": "FeeStructure" - } + "name": "takerVolume30d", + "docs": [ + "Rolling 30day taker volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" }, { - "name": "spotFeeStructure", - "type": { - "defined": "FeeStructure" - } + "name": "fillerVolume30d", + "docs": [ + "Rolling 30day filler volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" }, { - "name": "oracleGuardRails", - "type": { - "defined": "OracleGuardRails" - } + "name": "lastMakerVolume30dTs", + "docs": [ + "last time the maker volume was updated" + ], + "type": "i64" }, { - "name": "numberOfAuthorities", - "type": "u64" + "name": "lastTakerVolume30dTs", + "docs": [ + "last time the taker volume was updated" + ], + "type": "i64" }, { - "name": "numberOfSubAccounts", - "type": "u64" + "name": "lastFillerVolume30dTs", + "docs": [ + "last time the filler volume was updated" + ], + "type": "i64" }, { - "name": "lpCooldownTime", + "name": "ifStakedQuoteAssetAmount", + "docs": [ + "The amount of tokens staked in the quote spot markets if" + ], "type": "u64" }, { - "name": "liquidationMarginBufferRatio", - "type": "u32" + "name": "numberOfSubAccounts", + "docs": [ + "The current number of sub accounts" + ], + "type": "u16" }, { - "name": "settlementDuration", + "name": "numberOfSubAccountsCreated", + "docs": [ + "The number of sub accounts created. Can be greater than the number of sub accounts if user", + "has deleted sub accounts" + ], "type": "u16" }, { - "name": "numberOfMarkets", - "type": "u16" + "name": "referrerStatus", + "docs": [ + "Flags for referrer status:", + "First bit (LSB): 1 if user is a referrer, 0 otherwise", + "Second bit: 1 if user was referred, 0 otherwise" + ], + "type": "u8" }, { - "name": "numberOfSpotMarkets", - "type": "u16" + "name": "disableUpdatePerpBidAskTwap", + "type": "bool" }, { - "name": "signerNonce", - "type": "u8" + "name": "padding1", + "type": { + "array": [ + "u8", + 1 + ] + } }, { - "name": "minPerpAuctionDuration", + "name": "fuelOverflowStatus", + "docs": [ + "whether the user has a FuelOverflow account" + ], "type": "u8" }, { - "name": "defaultMarketOrderTimeInForce", - "type": "u8" + "name": "fuelInsurance", + "docs": [ + "accumulated fuel for token amounts of insurance" + ], + "type": "u32" }, { - "name": "defaultSpotAuctionDuration", - "type": "u8" + "name": "fuelDeposits", + "docs": [ + "accumulated fuel for notional of deposits" + ], + "type": "u32" }, { - "name": "exchangeStatus", - "type": "u8" + "name": "fuelBorrows", + "docs": [ + "accumulate fuel bonus for notional of borrows" + ], + "type": "u32" }, { - "name": "liquidationDuration", - "type": "u8" + "name": "fuelPositions", + "docs": [ + "accumulated fuel for perp open interest" + ], + "type": "u32" }, { - "name": "initialPctToLiquidate", - "type": "u16" + "name": "fuelTaker", + "docs": [ + "accumulate fuel bonus for taker volume" + ], + "type": "u32" }, { - "name": "maxNumberOfSubAccounts", - "type": "u16" + "name": "fuelMaker", + "docs": [ + "accumulate fuel bonus for maker volume" + ], + "type": "u32" }, { - "name": "maxInitializeUserFee", - "type": "u16" + "name": "ifStakedGovTokenAmount", + "docs": [ + "The amount of tokens staked in the governance spot markets if" + ], + "type": "u64" }, { - "name": "featureBitFlags", - "type": "u8" + "name": "lastFuelIfBonusUpdateTs", + "docs": [ + "last unix ts user stats data was used to update if fuel (u32 to save space)" + ], + "type": "u32" }, { "name": "padding", "type": { "array": [ "u8", - 9 + 12 ] } } @@ -9202,577 +11862,429 @@ } }, { - "name": "User", + "name": "ReferrerName", "type": { "kind": "struct", "fields": [ { "name": "authority", - "docs": [ - "The owner/authority of the account" - ], "type": "publicKey" }, { - "name": "delegate", - "docs": [ - "An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw" - ], + "name": "user", + "type": "publicKey" + }, + { + "name": "userStats", "type": "publicKey" }, { "name": "name", - "docs": [ - "Encoded display name e.g. \"toly\"" - ], "type": { "array": [ "u8", 32 ] } - }, - { - "name": "spotPositions", - "docs": [ - "The user's spot positions" - ], - "type": { - "array": [ - { - "defined": "SpotPosition" - }, - 8 - ] - } - }, - { - "name": "perpPositions", - "docs": [ - "The user's perp positions" - ], - "type": { - "array": [ - { - "defined": "PerpPosition" - }, - 8 - ] - } - }, + } + ] + } + }, + { + "name": "FuelOverflow", + "type": { + "kind": "struct", + "fields": [ { - "name": "orders", + "name": "authority", "docs": [ - "The user's orders" + "The authority of this overflow account" ], - "type": { - "array": [ - { - "defined": "Order" - }, - 32 - ] - } + "type": "publicKey" }, { - "name": "lastAddPerpLpSharesTs", - "docs": [ - "The last time the user added perp lp positions" - ], - "type": "i64" + "name": "fuelInsurance", + "type": "u128" }, { - "name": "totalDeposits", - "docs": [ - "The total values of deposits the user has made", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "fuelDeposits", + "type": "u128" }, { - "name": "totalWithdraws", - "docs": [ - "The total values of withdrawals the user has made", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "fuelBorrows", + "type": "u128" }, { - "name": "totalSocialLoss", - "docs": [ - "The total socialized loss the users has incurred upon the protocol", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "fuelPositions", + "type": "u128" }, { - "name": "settledPerpPnl", - "docs": [ - "Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps", - "precision: QUOTE_PRECISION" - ], - "type": "i64" + "name": "fuelTaker", + "type": "u128" }, { - "name": "cumulativeSpotFees", - "docs": [ - "Fees (taker fees, maker rebate, filler reward) for spot", - "precision: QUOTE_PRECISION" - ], - "type": "i64" + "name": "fuelMaker", + "type": "u128" }, { - "name": "cumulativePerpFunding", - "docs": [ - "Cumulative funding paid/received for perps", - "precision: QUOTE_PRECISION" - ], - "type": "i64" + "name": "lastFuelSweepTs", + "type": "u32" }, { - "name": "liquidationMarginFreed", - "docs": [ - "The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time", - "Defaults to zero when not being liquidated", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "lastResetTs", + "type": "u32" }, { - "name": "lastActiveSlot", - "docs": [ - "The last slot a user was active. Used to determine if a user is idle" - ], - "type": "u64" - }, + "name": "padding", + "type": { + "array": [ + "u128", + 6 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "UpdatePerpMarketSummaryStatsParams", + "type": { + "kind": "struct", + "fields": [ { - "name": "nextOrderId", - "docs": [ - "Every user order has an order id. This is the next order id to be used" - ], - "type": "u32" + "name": "quoteAssetAmountWithUnsettledLp", + "type": { + "option": "i64" + } }, { - "name": "maxMarginRatio", - "docs": [ - "Custom max initial margin ratio for the user" - ], - "type": "u32" + "name": "netUnsettledFundingPnl", + "type": { + "option": "i64" + } }, { - "name": "nextLiquidationId", - "docs": [ - "The next liquidation id to be used for user" - ], - "type": "u16" + "name": "updateAmmSummaryStats", + "type": { + "option": "bool" + } }, { - "name": "subAccountId", - "docs": [ - "The sub account id for this user" - ], - "type": "u16" - }, + "name": "excludeTotalLiqFee", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "ConstituentParams", + "type": { + "kind": "struct", + "fields": [ { - "name": "status", - "docs": [ - "Whether the user is active, being liquidated or bankrupt" - ], - "type": "u8" + "name": "maxWeightDeviation", + "type": { + "option": "i64" + } }, { - "name": "isMarginTradingEnabled", - "docs": [ - "Whether the user has enabled margin trading" - ], - "type": "bool" + "name": "swapFeeMin", + "type": { + "option": "i64" + } }, { - "name": "idle", - "docs": [ - "User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows", - "Off-chain keeper bots can ignore users that are idle" - ], - "type": "bool" + "name": "swapFeeMax", + "type": { + "option": "i64" + } }, { - "name": "openOrders", - "docs": [ - "number of open orders" - ], - "type": "u8" + "name": "maxBorrowTokenAmount", + "type": { + "option": "u64" + } }, { - "name": "hasOpenOrder", - "docs": [ - "Whether or not user has open order" - ], - "type": "bool" + "name": "oracleStalenessThreshold", + "type": { + "option": "u64" + } }, { - "name": "openAuctions", - "docs": [ - "number of open orders with auction" - ], - "type": "u8" + "name": "costToTradeBps", + "type": { + "option": "i32" + } }, { - "name": "hasOpenAuction", - "docs": [ - "Whether or not user has open order with auction" - ], - "type": "bool" + "name": "constituentDerivativeIndex", + "type": { + "option": "i16" + } }, { - "name": "marginMode", + "name": "derivativeWeight", "type": { - "defined": "MarginMode" + "option": "u64" } }, { - "name": "poolId", - "type": "u8" + "name": "volatility", + "type": { + "option": "u64" + } }, { - "name": "padding1", + "name": "gammaExecution", "type": { - "array": [ - "u8", - 3 - ] + "option": "u8" } }, { - "name": "lastFuelBonusUpdateTs", - "type": "u32" + "name": "gammaInventory", + "type": { + "option": "u8" + } }, { - "name": "padding", + "name": "xi", "type": { - "array": [ - "u8", - 12 - ] + "option": "u8" } } ] } }, { - "name": "UserStats", + "name": "LpPoolParams", "type": { "kind": "struct", "fields": [ { - "name": "authority", - "docs": [ - "The authority for all of a users sub accounts" - ], - "type": "publicKey" + "name": "maxSettleQuoteAmount", + "type": { + "option": "u64" + } }, { - "name": "referrer", - "docs": [ - "The address that referred this user" - ], - "type": "publicKey" + "name": "volatility", + "type": { + "option": "u64" + } }, { - "name": "fees", - "docs": [ - "Stats on the fees paid by the user" - ], + "name": "gammaExecution", "type": { - "defined": "UserFees" + "option": "u8" } }, { - "name": "nextEpochTs", - "docs": [ - "The timestamp of the next epoch", - "Epoch is used to limit referrer rewards earned in single epoch" - ], - "type": "i64" + "name": "xi", + "type": { + "option": "u8" + } }, { - "name": "makerVolume30d", - "docs": [ - "Rolling 30day maker volume for user", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "maxAum", + "type": { + "option": "u128" + } }, { - "name": "takerVolume30d", - "docs": [ - "Rolling 30day taker volume for user", - "precision: QUOTE_PRECISION" - ], - "type": "u64" - }, + "name": "whitelistMint", + "type": { + "option": "publicKey" + } + } + ] + } + }, + { + "name": "OverrideAmmCacheParams", + "type": { + "kind": "struct", + "fields": [ { - "name": "fillerVolume30d", - "docs": [ - "Rolling 30day filler volume for user", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "quoteOwedFromLpPool", + "type": { + "option": "i64" + } }, { - "name": "lastMakerVolume30dTs", - "docs": [ - "last time the maker volume was updated" - ], - "type": "i64" + "name": "lastSettleSlot", + "type": { + "option": "u64" + } }, { - "name": "lastTakerVolume30dTs", - "docs": [ - "last time the taker volume was updated" - ], - "type": "i64" + "name": "lastFeePoolTokenAmount", + "type": { + "option": "u128" + } }, { - "name": "lastFillerVolume30dTs", - "docs": [ - "last time the filler volume was updated" - ], - "type": "i64" + "name": "lastNetPnlPoolTokenAmount", + "type": { + "option": "i128" + } }, { - "name": "ifStakedQuoteAssetAmount", - "docs": [ - "The amount of tokens staked in the quote spot markets if" - ], - "type": "u64" + "name": "ammPositionScalar", + "type": { + "option": "u8" + } }, { - "name": "numberOfSubAccounts", - "docs": [ - "The current number of sub accounts" - ], - "type": "u16" - }, + "name": "ammInventoryLimit", + "type": { + "option": "i64" + } + } + ] + } + }, + { + "name": "AddAmmConstituentMappingDatum", + "type": { + "kind": "struct", + "fields": [ { - "name": "numberOfSubAccountsCreated", - "docs": [ - "The number of sub accounts created. Can be greater than the number of sub accounts if user", - "has deleted sub accounts" - ], + "name": "constituentIndex", "type": "u16" }, { - "name": "referrerStatus", - "docs": [ - "Flags for referrer status:", - "First bit (LSB): 1 if user is a referrer, 0 otherwise", - "Second bit: 1 if user was referred, 0 otherwise" - ], - "type": "u8" - }, - { - "name": "disableUpdatePerpBidAskTwap", - "type": "bool" + "name": "perpMarketIndex", + "type": "u16" }, { - "name": "padding1", - "type": { - "array": [ - "u8", - 1 - ] - } - }, + "name": "weight", + "type": "i64" + } + ] + } + }, + { + "name": "CacheInfo", + "type": { + "kind": "struct", + "fields": [ { - "name": "fuelOverflowStatus", - "docs": [ - "whether the user has a FuelOverflow account" - ], - "type": "u8" + "name": "oracle", + "type": "publicKey" }, { - "name": "fuelInsurance", - "docs": [ - "accumulated fuel for token amounts of insurance" - ], - "type": "u32" + "name": "lastFeePoolTokenAmount", + "type": "u128" }, { - "name": "fuelDeposits", - "docs": [ - "accumulated fuel for notional of deposits" - ], - "type": "u32" + "name": "lastNetPnlPoolTokenAmount", + "type": "i128" }, { - "name": "fuelBorrows", - "docs": [ - "accumulate fuel bonus for notional of borrows" - ], - "type": "u32" + "name": "lastExchangeFees", + "type": "u128" }, { - "name": "fuelPositions", - "docs": [ - "accumulated fuel for perp open interest" - ], - "type": "u32" + "name": "lastSettleAmmExFees", + "type": "u128" }, { - "name": "fuelTaker", - "docs": [ - "accumulate fuel bonus for taker volume" - ], - "type": "u32" + "name": "lastSettleAmmPnl", + "type": "i128" }, { - "name": "fuelMaker", + "name": "position", "docs": [ - "accumulate fuel bonus for maker volume" + "BASE PRECISION" ], - "type": "u32" + "type": "i64" }, { - "name": "ifStakedGovTokenAmount", - "docs": [ - "The amount of tokens staked in the governance spot markets if" - ], + "name": "slot", "type": "u64" }, { - "name": "lastFuelIfBonusUpdateTs", - "docs": [ - "last unix ts user stats data was used to update if fuel (u32 to save space)" - ], - "type": "u32" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 12 - ] - } - } - ] - } - }, - { - "name": "ReferrerName", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "publicKey" - }, - { - "name": "user", - "type": "publicKey" - }, - { - "name": "userStats", - "type": "publicKey" + "name": "lastSettleAmount", + "type": "u64" }, { - "name": "name", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "FuelOverflow", - "type": { - "kind": "struct", - "fields": [ + "name": "lastSettleSlot", + "type": "u64" + }, { - "name": "authority", - "docs": [ - "The authority of this overflow account" - ], - "type": "publicKey" + "name": "lastSettleTs", + "type": "i64" }, { - "name": "fuelInsurance", - "type": "u128" + "name": "quoteOwedFromLpPool", + "type": "i64" }, { - "name": "fuelDeposits", - "type": "u128" + "name": "ammInventoryLimit", + "type": "i64" }, { - "name": "fuelBorrows", - "type": "u128" + "name": "oraclePrice", + "type": "i64" }, { - "name": "fuelPositions", - "type": "u128" + "name": "oracleSlot", + "type": "u64" }, { - "name": "fuelTaker", - "type": "u128" + "name": "oracleSource", + "type": "u8" }, { - "name": "fuelMaker", - "type": "u128" + "name": "oracleValidity", + "type": "u8" }, { - "name": "lastFuelSweepTs", - "type": "u32" + "name": "lpStatusForPerpMarket", + "type": "u8" }, { - "name": "lastResetTs", - "type": "u32" + "name": "ammPositionScalar", + "type": "u8" }, { "name": "padding", "type": { "array": [ - "u128", - 6 + "u8", + 36 ] } } ] } - } - ], - "types": [ + }, { - "name": "UpdatePerpMarketSummaryStatsParams", + "name": "AmmCacheFixed", "type": { "kind": "struct", "fields": [ { - "name": "quoteAssetAmountWithUnsettledLp", - "type": { - "option": "i64" - } - }, - { - "name": "netUnsettledFundingPnl", - "type": { - "option": "i64" - } + "name": "bump", + "type": "u8" }, { - "name": "updateAmmSummaryStats", + "name": "pad", "type": { - "option": "bool" + "array": [ + "u8", + 3 + ] } }, { - "name": "excludeTotalLiqFee", - "type": { - "option": "bool" - } + "name": "len", + "type": "u32" } ] } @@ -9902,14 +12414,196 @@ "type": "i64" }, { - "name": "liabilityTransfer", - "type": "u128" + "name": "liabilityTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "LiquidatePerpPnlForDepositRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "marketOraclePrice", + "type": "i64" + }, + { + "name": "pnlTransfer", + "type": "u128" + }, + { + "name": "assetMarketIndex", + "type": "u16" + }, + { + "name": "assetPrice", + "type": "i64" + }, + { + "name": "assetTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "PerpBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "pnl", + "type": "i128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "clawbackUser", + "type": { + "option": "publicKey" + } + }, + { + "name": "clawbackUserPayment", + "type": { + "option": "u128" + } + }, + { + "name": "cumulativeFundingRateDelta", + "type": "i128" + } + ] + } + }, + { + "name": "SpotBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "borrowAmount", + "type": "u128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "cumulativeDepositInterestDelta", + "type": "u128" + } + ] + } + }, + { + "name": "IfRebalanceConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "totalInAmount", + "type": "u64" + }, + { + "name": "epochMaxInAmount", + "type": "u64" + }, + { + "name": "epochDuration", + "type": "i64" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" + }, + { + "name": "status", + "type": "u8" + } + ] + } + }, + { + "name": "ConstituentSpotBalance", + "type": { + "kind": "struct", + "fields": [ + { + "name": "scaledBalance", + "docs": [ + "The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow", + "interest of corresponding market.", + "precision: token precision" + ], + "type": "u128" + }, + { + "name": "cumulativeDeposits", + "docs": [ + "The cumulative deposits/borrows a user has made into a market", + "precision: token mint precision" + ], + "type": "i64" + }, + { + "name": "marketIndex", + "docs": [ + "The market index of the corresponding spot market" + ], + "type": "u16" + }, + { + "name": "balanceType", + "docs": [ + "Whether the position is deposit or borrow" + ], + "type": { + "defined": "SpotBalanceType" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 5 + ] + } } ] } }, { - "name": "LiquidatePerpPnlForDepositRecord", + "name": "AmmConstituentDatum", "type": { "kind": "struct", "fields": [ @@ -9918,124 +12612,154 @@ "type": "u16" }, { - "name": "marketOraclePrice", - "type": "i64" + "name": "constituentIndex", + "type": "u16" }, { - "name": "pnlTransfer", - "type": "u128" + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "assetMarketIndex", - "type": "u16" + "name": "lastSlot", + "type": "u64" }, { - "name": "assetPrice", + "name": "weight", + "docs": [ + "PERCENTAGE_PRECISION. The weight this constituent has on the perp market" + ], "type": "i64" - }, - { - "name": "assetTransfer", - "type": "u128" } ] } }, { - "name": "PerpBankruptcyRecord", + "name": "AmmConstituentMappingFixed", "type": { "kind": "struct", "fields": [ { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "pnl", - "type": "i128" - }, - { - "name": "ifPayment", - "type": "u128" + "name": "lpPool", + "type": "publicKey" }, { - "name": "clawbackUser", - "type": { - "option": "publicKey" - } + "name": "bump", + "type": "u8" }, { - "name": "clawbackUserPayment", + "name": "pad", "type": { - "option": "u128" + "array": [ + "u8", + 3 + ] } }, { - "name": "cumulativeFundingRateDelta", - "type": "i128" + "name": "len", + "type": "u32" } ] } }, { - "name": "SpotBankruptcyRecord", + "name": "TargetsDatum", "type": { "kind": "struct", "fields": [ { - "name": "marketIndex", - "type": "u16" + "name": "costToTradeBps", + "type": "i32" }, { - "name": "borrowAmount", - "type": "u128" + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "ifPayment", - "type": "u128" + "name": "targetBase", + "type": "i64" }, { - "name": "cumulativeDepositInterestDelta", - "type": "u128" + "name": "lastOracleSlot", + "type": "u64" + }, + { + "name": "lastPositionSlot", + "type": "u64" } ] } }, { - "name": "IfRebalanceConfigParams", + "name": "ConstituentTargetBaseFixed", "type": { "kind": "struct", "fields": [ { - "name": "totalInAmount", - "type": "u64" + "name": "lpPool", + "type": "publicKey" }, { - "name": "epochMaxInAmount", - "type": "u64" + "name": "bump", + "type": "u8" }, { - "name": "epochDuration", - "type": "i64" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "outMarketIndex", - "type": "u16" - }, + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" + } + ] + } + }, + { + "name": "ConstituentCorrelationsFixed", + "type": { + "kind": "struct", + "fields": [ { - "name": "inMarketIndex", - "type": "u16" + "name": "lpPool", + "type": "publicKey" }, { - "name": "maxSlippageBps", - "type": "u16" + "name": "bump", + "type": "u8" }, { - "name": "swapMode", - "type": "u8" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "status", - "type": "u8" + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" } ] } @@ -12252,6 +14976,23 @@ ] } }, + { + "name": "SettlementDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ToLpPool" + }, + { + "name": "FromLpPool" + }, + { + "name": "None" + } + ] + } + }, { "name": "MarginRequirementType", "type": { @@ -12348,6 +15089,15 @@ }, { "name": "UseMMOraclePrice" + }, + { + "name": "UpdateAmmCache" + }, + { + "name": "UpdateLpPoolAum" + }, + { + "name": "LpPoolSwap" } ] } @@ -12681,6 +15431,20 @@ ] } }, + { + "name": "ConstituentStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ReduceOnly" + }, + { + "name": "Decommissioned" + } + ] + } + }, { "name": "MarginCalculationMode", "type": { @@ -12901,6 +15665,37 @@ ] } }, + { + "name": "PerpLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TrackAmmRevenue" + }, + { + "name": "SettleQuoteOwed" + } + ] + } + }, + { + "name": "ConstituentLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Swap" + }, + { + "name": "Deposit" + }, + { + "name": "Withdraw" + } + ] + } + }, { "name": "MarketStatus", "type": { @@ -12936,6 +15731,23 @@ ] } }, + { + "name": "LpStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Uncollateralized" + }, + { + "name": "Active" + }, + { + "name": "Decommissioning" + } + ] + } + }, { "name": "ContractType", "type": { @@ -13130,6 +15942,23 @@ ] } }, + { + "name": "LpPoolFeatureBitFlags", + "type": { + "kind": "enum", + "variants": [ + { + "name": "SettleLpPool" + }, + { + "name": "SwapLpPool" + }, + { + "name": "MintRedeemLpPool" + } + ] + } + }, { "name": "UserStatus", "type": { @@ -16819,6 +19648,101 @@ "code": 6324, "name": "UnableToLoadRevenueShareAccount", "msg": "Unable to load builder account" + }, + { + "code": 6325, + "name": "InvalidConstituent", + "msg": "Invalid Constituent" + }, + { + "code": 6326, + "name": "InvalidAmmConstituentMappingArgument", + "msg": "Invalid Amm Constituent Mapping argument" + }, + { + "code": 6327, + "name": "ConstituentNotFound", + "msg": "Constituent not found" + }, + { + "code": 6328, + "name": "ConstituentCouldNotLoad", + "msg": "Constituent could not load" + }, + { + "code": 6329, + "name": "ConstituentWrongMutability", + "msg": "Constituent wrong mutability" + }, + { + "code": 6330, + "name": "WrongNumberOfConstituents", + "msg": "Wrong number of constituents passed to instruction" + }, + { + "code": 6331, + "name": "InsufficientConstituentTokenBalance", + "msg": "Insufficient constituent token balance" + }, + { + "code": 6332, + "name": "AMMCacheStale", + "msg": "Amm Cache data too stale" + }, + { + "code": 6333, + "name": "LpPoolAumDelayed", + "msg": "LP Pool AUM not updated recently" + }, + { + "code": 6334, + "name": "ConstituentOracleStale", + "msg": "Constituent oracle is stale" + }, + { + "code": 6335, + "name": "LpInvariantFailed", + "msg": "LP Invariant failed" + }, + { + "code": 6336, + "name": "InvalidConstituentDerivativeWeights", + "msg": "Invalid constituent derivative weights" + }, + { + "code": 6337, + "name": "MaxDlpAumBreached", + "msg": "Max DLP AUM Breached" + }, + { + "code": 6338, + "name": "SettleLpPoolDisabled", + "msg": "Settle Lp Pool Disabled" + }, + { + "code": 6339, + "name": "MintRedeemLpPoolDisabled", + "msg": "Mint/Redeem Lp Pool Disabled" + }, + { + "code": 6340, + "name": "LpPoolSettleInvariantBreached", + "msg": "Settlement amount exceeded" + }, + { + "code": 6341, + "name": "InvalidConstituentOperation", + "msg": "Invalid constituent operation" + }, + { + "code": 6342, + "name": "Unauthorized", + "msg": "Unauthorized for operation" + }, + { + "code": 6343, + "name": "InvalidLpPoolId", + "msg": "Invalid Lp Pool Id for Operation" } ] } diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 0919ebd346..b6f69eeb3a 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -12,10 +12,6 @@ export * from './accounts/webSocketDriftClientAccountSubscriber'; export * from './accounts/webSocketInsuranceFundStakeAccountSubscriber'; export * from './accounts/webSocketHighLeverageModeConfigAccountSubscriber'; export { WebSocketAccountSubscriberV2 } from './accounts/webSocketAccountSubscriberV2'; -export { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; -export { WebSocketProgramUserAccountSubscriber } from './accounts/websocketProgramUserAccountSubscriber'; -export { WebSocketProgramAccountsSubscriberV2 } from './accounts/webSocketProgramAccountsSubscriberV2'; -export { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; export * from './accounts/bulkAccountLoader'; export * from './accounts/bulkUserSubscription'; export * from './accounts/bulkUserStatsSubscription'; @@ -141,5 +137,6 @@ export * from './clock/clockSubscriber'; export * from './math/userStatus'; export * from './indicative-quotes/indicativeQuotesSender'; export * from './constants'; +export * from './constituentMap/constituentMap'; export { BN, PublicKey, pyth }; diff --git a/sdk/src/memcmp.ts b/sdk/src/memcmp.ts index 3ecdeb9a9c..c73d21efdb 100644 --- a/sdk/src/memcmp.ts +++ b/sdk/src/memcmp.ts @@ -1,4 +1,4 @@ -import { MemcmpFilter } from '@solana/web3.js'; +import { MemcmpFilter, PublicKey } from '@solana/web3.js'; import bs58 from 'bs58'; import { BorshAccountsCoder } from '@coral-xyz/anchor'; import { encodeName } from './userName'; @@ -140,3 +140,25 @@ export function getRevenueShareEscrowFilter(): MemcmpFilter { }, }; } + +export function getConstituentFilter(): MemcmpFilter { + return { + memcmp: { + offset: 0, + bytes: bs58.encode( + BorshAccountsCoder.accountDiscriminator('Constituent') + ), + }, + }; +} + +export function getConstituentLpPoolFilter( + lpPoolPublicKey: PublicKey +): MemcmpFilter { + return { + memcmp: { + offset: 72, + bytes: lpPoolPublicKey.toBase58(), + }, + }; +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 42ddce507d..e2d7302d23 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -895,6 +895,7 @@ export type PerpMarketAccount = { protectedMakerDynamicDivisor: number; lastFillPrice: BN; + lpPoolId: number; lpFeeTransferScalar: number; lpExchangeFeeExcluscionScalar: number; lpStatus: number; @@ -1734,3 +1735,162 @@ export type RevenueShareSettleRecord = { builderTotalBuilderRewards: BN; builderSubAccountId: number; }; + +export type AddAmmConstituentMappingDatum = { + constituentIndex: number; + perpMarketIndex: number; + weight: BN; +}; + +export type AmmConstituentDatum = AddAmmConstituentMappingDatum & { + lastSlot: BN; +}; + +export type AmmConstituentMapping = { + lpPool: PublicKey; + bump: number; + weights: AmmConstituentDatum[]; +}; + +export type TargetDatum = { + costToTradeBps: number; + lastOracleSlot: BN; + lastPositionSlot: BN; + targetBase: BN; +}; + +export type ConstituentTargetBaseAccount = { + lpPool: PublicKey; + bump: number; + targets: TargetDatum[]; +}; + +export type ConstituentCorrelations = { + lpPool: PublicKey; + bump: number; + correlations: BN[]; +}; + +export type LPPoolAccount = { + lpPoolId: number; + pubkey: PublicKey; + mint: PublicKey; + whitelistMint: PublicKey; + constituentTargetBase: PublicKey; + constituentCorrelations: PublicKey; + maxAum: BN; + lastAum: BN; + cumulativeQuoteSentToPerpMarkets: BN; + cumulativeQuoteReceivedFromPerpMarkets: BN; + totalMintRedeemFeesPaid: BN; + lastAumSlot: BN; + maxSettleQuoteAmount: BN; + mintRedeemId: BN; + settleId: BN; + minMintFee: BN; + tokenSupply: BN; + volatility: BN; + constituents: number; + quoteConstituentIndex: number; + bump: number; + gammaExecution: number; + xi: number; +}; + +export type ConstituentSpotBalance = { + scaledBalance: BN; + cumulativeDeposits: BN; + marketIndex: number; + balanceType: SpotBalanceType; +}; + +export type InitializeConstituentParams = { + spotMarketIndex: number; + decimals: number; + maxWeightDeviation: BN; + swapFeeMin: BN; + swapFeeMax: BN; + maxBorrowTokenAmount: BN; + oracleStalenessThreshold: BN; + costToTrade: number; + derivativeWeight: BN; + constituentDerivativeIndex?: number; + constituentDerivativeDepegThreshold?: BN; + constituentCorrelations: BN[]; + volatility: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; +}; + +export enum ConstituentStatus { + ACTIVE = 0, + REDUCE_ONLY = 1, + DECOMMISSIONED = 2, +} +export enum ConstituentLpOperation { + Swap = 0b00000001, + Deposit = 0b00000010, + Withdraw = 0b00000100, +} + +export type ConstituentAccount = { + pubkey: PublicKey; + mint: PublicKey; + lpPool: PublicKey; + vault: PublicKey; + totalSwapFees: BN; + spotBalance: ConstituentSpotBalance; + lastSpotBalanceTokenAmount: BN; + cumulativeSpotInterestAccruedTokenAmount: BN; + maxWeightDeviation: BN; + swapFeeMin: BN; + swapFeeMax: BN; + maxBorrowTokenAmount: BN; + vaultTokenBalance: BN; + lastOraclePrice: BN; + lastOracleSlot: BN; + oracleStalenessThreshold: BN; + flashLoanInitialTokenAmount: BN; + nextSwapId: BN; + derivativeWeight: BN; + volatility: BN; + constituentDerivativeDepegThreshold: BN; + constituentDerivativeIndex: number; + spotMarketIndex: number; + constituentIndex: number; + decimals: number; + bump: number; + vaultBump: number; + gammaInventory: number; + gammaExecution: number; + xi: number; + status: number; + pausedOperations: number; +}; + +export type CacheInfo = { + oracle: PublicKey; + lastFeePoolTokenAmount: BN; + lastNetPnlPoolTokenAmount: BN; + lastExchangeFees: BN; + lastSettleAmmExFees: BN; + lastSettleAmmPnl: BN; + position: BN; + slot: BN; + lastSettleAmount: BN; + lastSettleSlot: BN; + lastSettleTs: BN; + quoteOwedFromLpPool: BN; + ammInventoryLimit: BN; + oraclePrice: BN; + oracleSlot: BN; + oracleSource: number; + oracleValidity: number; + lpStatusForPerpMarket: number; + ammPositionScalar: number; +}; + +export type AmmCache = { + cache: CacheInfo[]; +}; diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 78b954d28f..b7c74b140f 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -43,6 +43,8 @@ test_files=( liquidatePerpPnlForDeposit.ts liquidateSpot.ts liquidateSpotSocialLoss.ts + lpPool.ts + lpPoolSwap.ts marketOrder.ts marketOrderBaseAssetAmount.ts maxDeposit.ts @@ -94,4 +96,4 @@ test_files=( for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} || exit 1 -done \ No newline at end of file +done diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index 8b0fc7f5bd..f3fb157085 100755 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -7,8 +7,8 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json test_files=( - builderCodes.ts - # placeAndMakeSignedMsgBankrun.ts + lpPool.ts + lpPoolSwap.ts ) for test_file in ${test_files[@]}; do diff --git a/tests/fixtures/token_2022.so b/tests/fixtures/token_2022.so new file mode 100755 index 0000000000000000000000000000000000000000..23c12ecb2fa59e69d92e9a2100b7e0395603d708 GIT binary patch literal 1382016 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

-bkyKzHdDJ`C;wv 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

90}~Kxm;0+eM&0GD$58R;fZMm*1(u6~yt(hm42 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 I_j!xJF<*9hy@!eKTqVFr^ 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 literal 0 HcmV?d00001 diff --git a/tests/lpPool.ts b/tests/lpPool.ts new file mode 100644 index 0000000000..91470291ed --- /dev/null +++ b/tests/lpPool.ts @@ -0,0 +1,1734 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, +} from '@solana/web3.js'; +import { + createAssociatedTokenAccountInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddress, + getAssociatedTokenAddressSync, + getMint, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; + +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBaseAccount, + AmmConstituentMapping, + LPPoolAccount, + getConstituentVaultPublicKey, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_RATE_PRECISION, + getAmmCachePublicKey, + AmmCache, + ZERO, + getConstituentPublicKey, + ConstituentAccount, + PositionDirection, + getPythLazerOraclePublicKey, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + BASE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + getTokenAmount, + TWO, + ConstituentLpOperation, +} from '../sdk/src'; + +import { + createWSolTokenAccountForUser, + initializeQuoteSpotMarket, + initializeSolSpotMarket, + mockAtaTokenAccountForMint, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccountWithAuthority, + overWriteMintAccount, + overWritePerpMarket, + overWriteSpotMarket, + setFeedPriceNoProgram, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_LAZER_HEX_STRING_SOL, PYTH_STORAGE_DATA } from './pythLazerData'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let userLpTokenAccount: PublicKey; + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle: PublicKey; + let spotMarketOracle2: PublicKey; + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + let solUsd: PublicKey; + let solUsdLazer: PublicKey; + + const lpPoolId = 0; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey(program.programId, lpPoolId); + + let whitelistMint: PublicKey; + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200); + spotMarketOracle2 = await mockOracleNoProgram(bankrunContextWrapper, 200); + + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 9); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 200); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1, 2], + spotMarketIndexes: [0, 1], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(1_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + + const periodicity = new BN(0); + + solUsdLazer = getPythLazerOraclePublicKey(program.programId, 6); + await adminClient.initializePythLazerOracle(6); + + await adminClient.initializePerpMarket( + 0, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(0, 1); + + await adminClient.initializePerpMarket( + 1, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(1, 1); + + await adminClient.initializePerpMarket( + 2, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(2, 1); + + await adminClient.updatePerpAuctionDuration(new BN(0)); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const imfFactor = 0; + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + await initializeSolSpotMarket(adminClient, spotMarketOracle2); + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle2, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + await adminClient.initializeLpPool( + lpPoolId, + ZERO, + new BN(1_000_000_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() + ); + + await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + + // Give the vamm some inventory + await adminClient.openPosition(PositionDirection.LONG, BASE_PRECISION, 0); + await adminClient.openPosition(PositionDirection.SHORT, BASE_PRECISION, 1); + assert( + adminClient + .getUser() + .getActivePerpPositions() + .filter((x) => !x.baseAssetAmount.eq(ZERO)).length == 2 + ); + + console.log('create whitelist mint'); + const whitelistKeypair = Keypair.generate(); + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: bankrunContextWrapper.provider.wallet.publicKey, + newAccountPubkey: whitelistKeypair.publicKey, + space: MINT_SIZE, + lamports: 10_000_000_000, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + whitelistKeypair.publicKey, + 0, + bankrunContextWrapper.provider.wallet.publicKey, + bankrunContextWrapper.provider.wallet.publicKey, + TOKEN_PROGRAM_ID + ) + ); + + await bankrunContextWrapper.sendTransaction(transaction, [ + whitelistKeypair, + ]); + + const whitelistMintInfo = + await bankrunContextWrapper.connection.getAccountInfo( + whitelistKeypair.publicKey + ); + console.log('whitelistMintInfo', whitelistMintInfo); + + whitelistMint = whitelistKeypair.publicKey; + }); + + after(async () => { + await adminClient.unsubscribe(); + }); + + it('can create a new LP Pool', async () => { + // check LpPool created + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminClient.wallet.publicKey + ); + + // Check amm constituent map exists + const ammConstituentMapPublicKey = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammConstituentMap = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapPublicKey + )) as AmmConstituentMapping; + expect(ammConstituentMap).to.not.be.null; + assert(ammConstituentMap.weights.length == 0); + + // check constituent target weights exists + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 0); + + // check mint created correctly + const mintInfo = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(mintInfo.decimals).to.equal(tokenDecimals); + expect(Number(mintInfo.supply)).to.equal(0); + expect(mintInfo.mintAuthority?.toBase58()).to.equal(lpPoolKey.toBase58()); + }); + + it('can add constituents to LP Pool', async () => { + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [], + }); + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(lpPool.constituents == 1); + + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 1); + + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const constituentTokenVault = + await bankrunContextWrapper.connection.getAccountInfo( + constituentVaultPublicKey + ); + expect(constituentTokenVault).to.not.be.null; + + // Add second constituent representing SOL + await adminClient.initializeConstituent(lpPool.lpPoolId, { + spotMarketIndex: 1, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + derivativeWeight: ZERO, + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ZERO], + }); + }); + + it('can add amm mapping datum', async () => { + // Firt constituent is USDC, so add no mapping. We will add a second mapping though + // for the second constituent which is SOL + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 1, + constituentIndex: 1, + weight: PERCENTAGE_PRECISION, + }, + ]); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammMapping = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.length == 1); + }); + + it('can update and remove amm constituent mapping entries', async () => { + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 2, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION, + }, + ]); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + let ammMapping = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.length == 2); + + // Update + await adminClient.updateAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 2, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION.muln(2), + }, + ]); + ammMapping = (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert( + ammMapping.weights + .find((x) => x.perpMarketIndex == 2) + .weight.eq(PERCENTAGE_PRECISION.muln(2)) + ); + + // Remove + await adminClient.removeAmmConstituentMappingData(lpPoolId, 2, 0); + ammMapping = (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.find((x) => x.perpMarketIndex == 2) == undefined); + assert(ammMapping.weights.length === 1); + }); + + it('can crank amm info into the cache', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + expect(ammCache).to.not.be.null; + assert(ammCache.cache.length == 3); + assert(ammCache.cache[0].oracle.equals(solUsd)); + assert(ammCache.cache[0].oraclePrice.eq(new BN(200000000))); + }); + + it('can update constituent properties and correlations', async () => { + const constituentPublicKey = getConstituentPublicKey( + program.programId, + lpPoolKey, + 0 + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + constituentPublicKey + )) as ConstituentAccount; + + await adminClient.updateConstituentParams(lpPoolId, constituentPublicKey, { + costToTradeBps: 10, + }); + const constituentTargetBase = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const targets = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBase + )) as ConstituentTargetBaseAccount; + expect(targets).to.not.be.null; + assert(targets.targets[constituent.constituentIndex].costToTradeBps == 10); + + await adminClient.updateConstituentCorrelationData( + lpPoolId, + 0, + 1, + PERCENTAGE_PRECISION.muln(87).divn(100) + ); + + await adminClient.updateConstituentCorrelationData( + lpPoolId, + 0, + 1, + PERCENTAGE_PRECISION + ); + }); + + it('fails adding datum with bad params', async () => { + // Bad perp market index + try { + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 3, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION, + }, + ]); + expect.fail('should have failed'); + } catch (e) { + console.log(e.message); + expect(e.message).to.contain('0x18b6'); // InvalidAmmConstituentMappingArgument + } + + // Bad constituent index + try { + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 0, + constituentIndex: 5, + weight: PERCENTAGE_PRECISION, + }, + ]); + expect.fail('should have failed'); + } catch (e) { + expect(e.message).to.contain('0x18b6'); // InvalidAmmConstituentMappingArgument + } + }); + + it('fails to add liquidity if aum not updated atomically', async () => { + try { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + await adminClient.lpPoolAddLiquidity({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + }); + expect.fail('should have failed'); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18bd')); // LpPoolAumDelayed + } + }); + + it('fails to add liquidity if a paused operation', async () => { + await adminClient.updateConstituentPausedOperations( + getConstituentPublicKey(program.programId, lpPoolKey, 0), + ConstituentLpOperation.Deposit + ); + try { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + await adminClient.sendTransaction(tx); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18c5')); // InvalidConstituentOperation + } + await adminClient.updateConstituentPausedOperations( + getConstituentPublicKey(program.programId, lpPoolKey, 0), + 0 + ); + }); + + it('can update pool aum', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + assert(lpPool.constituents == 2); + + const createAtaIx = + adminClient.createAssociatedTokenAccountIdempotentInstruction( + await getAssociatedTokenAddress( + lpPool.mint, + adminClient.wallet.publicKey, + true + ), + adminClient.wallet.publicKey, + adminClient.wallet.publicKey, + lpPool.mint + ); + + await adminClient.sendTransaction(new Transaction().add(createAtaIx), []); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + await adminClient.sendTransaction(tx); + + await adminClient.updateLpPoolAum(lpPool, [0, 1]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + console.log(lpPool.lastAum.toString()); + assert(lpPool.lastAum.eq(new BN(1000).mul(QUOTE_PRECISION))); + + // Should fail if we dont pass in the second constituent + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 1) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + try { + await adminClient.updateLpPoolAum(lpPool, [0]); + expect.fail('should have failed'); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18ba')); // WrongNumberOfConstituents + } + }); + + it('can update constituent target weights', async () => { + await adminClient.postPythLazerOracleUpdate([6], PYTH_LAZER_HEX_STRING_SOL); + await adminClient.updatePerpMarketOracle( + 0, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updatePerpMarketOracle( + 1, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updatePerpMarketOracle( + 2, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updateAmmCache([0, 1, 2]); + + await adminClient.overrideAmmCacheInfo(0, { + ammPositionScalar: 100, + ammInventoryLimit: BASE_PRECISION.muln(5000), + }); + await adminClient.overrideAmmCacheInfo(1, { + ammPositionScalar: 100, + ammInventoryLimit: BASE_PRECISION.muln(5000), + }); + await adminClient.overrideAmmCacheInfo(2, { + ammPositionScalar: 100, + ammInventoryLimit: BASE_PRECISION.muln(5000), + }); + + let tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + tx.add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + ]) + ); + await adminClient.sendTransaction(tx); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 2); + assert( + constituentTargetBase.targets.filter((x) => x.targetBase.eq(ZERO)) + .length !== constituentTargetBase.targets.length + ); + + // Make sure the target base respects the cache scalar + const cacheValueBefore = constituentTargetBase.targets[1].targetBase; + await adminClient.overrideAmmCacheInfo(1, { + ammPositionScalar: 50, + }); + tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + tx.add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + ]) + ); + await adminClient.sendTransaction(tx); + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + console.log(cacheValueBefore.toString()); + expect( + constituentTargetBase.targets[1].targetBase.toNumber() + ).to.approximately(cacheValueBefore.muln(50).divn(100).toNumber(), 1); + + await adminClient.overrideAmmCacheInfo(1, { + ammPositionScalar: 100, + }); + }); + + it('can add constituent to LP Pool thats a derivative and behave correctly', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + await adminClient.initializeConstituent(lpPool.lpPoolId, { + spotMarketIndex: 2, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + oracleStalenessThreshold: new BN(400), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(2), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ZERO, PERCENTAGE_PRECISION.muln(87).divn(100)], + constituentDerivativeIndex: 1, + }); + + await adminClient.updateAmmCache([0, 1, 2]); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + assert(!constituent.lastOraclePrice.eq(ZERO)); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ]) + ); + await adminClient.sendTransaction(tx); + + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + expect(constituentTargetBase).to.not.be.null; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect( + constituentTargetBase.targets[1].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[2].targetBase.toNumber(), + 10 + ); + + // Move the oracle price to be double, so it should have half of the target base + const derivativeBalanceBefore = constituentTargetBase.targets[2].targetBase; + const derivative = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + await setFeedPriceNoProgram(bankrunContextWrapper, 400, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + const tx2 = new Transaction(); + tx2 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ]) + ); + await adminClient.sendTransaction(tx2); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + const derivativeBalanceAfter = constituentTargetBase.targets[2].targetBase; + + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + + expect(derivativeBalanceAfter.toNumber()).to.be.approximately( + derivativeBalanceBefore.toNumber() / 2, + 20 + ); + + // Move the oracle price to be half, so its target base should go to zero + const parentBalanceBefore = constituentTargetBase.targets[1].targetBase; + await setFeedPriceNoProgram(bankrunContextWrapper, 100, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + const tx3 = new Transaction(); + tx3 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ]) + ); + await adminClient.sendTransaction(tx3); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + const parentBalanceAfter = constituentTargetBase.targets[1].targetBase; + + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect(parentBalanceAfter.toNumber()).to.be.approximately( + parentBalanceBefore.toNumber() * 2, + 10 + ); + await setFeedPriceNoProgram(bankrunContextWrapper, 200, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + }); + + it('can settle pnl from perp markets into the usdc account', async () => { + await adminClient.updateFeatureBitFlagsSettleLpPool(true); + + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + // Exclude 25% of exchange fees, put 100 dollars there to make sure that the + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(0, 100, 25); + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(1, 100, 0); + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(2, 100, 0); + + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.totalExchangeFee = perpMarket.amm.totalExchangeFee.add( + QUOTE_PRECISION.muln(100) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + await adminClient.depositIntoPerpMarketFeePool( + 0, + new BN(100).mul(QUOTE_PRECISION), + await adminClient.getAssociatedTokenAccount(0) + ); + + await adminClient.depositIntoPerpMarketFeePool( + 1, + new BN(100).mul(QUOTE_PRECISION), + await adminClient.getAssociatedTokenAccount(0) + ); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterDeposit = lpPool.lastAum; + + // Make sure the amount recorded goes into the cache and that the quote amount owed is adjusted + // for new influx in fees + const ammCacheBeforeAdjust = ammCache; + // Test pausing tracking for market 0 + await adminClient.updatePerpMarketLpPoolPausedOperations(0, 1); + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + assert(ammCache.cache[0].lastFeePoolTokenAmount.eq(ZERO)); + assert( + ammCache.cache[0].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool + ) + ); + assert(ammCache.cache[1].lastFeePoolTokenAmount.eq(new BN(100000000))); + assert( + ammCache.cache[1].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[1].quoteOwedFromLpPool.sub( + new BN(100).mul(QUOTE_PRECISION) + ) + ) + ); + + // Market 0 on the amm cache will update now that tracking is permissioned again + await adminClient.updatePerpMarketLpPoolPausedOperations(0, 0); + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + assert(ammCache.cache[0].lastFeePoolTokenAmount.eq(new BN(100000000))); + assert( + ammCache.cache[0].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool.sub( + new BN(75).mul(QUOTE_PRECISION) + ) + ) + ); + + const usdcBefore = constituent.vaultTokenBalance; + // Update Amm Cache to update the aum + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterUpdateCacheBeforeSettle = lpPool.lastAum; + assert( + lpAumAfterUpdateCacheBeforeSettle.eq( + lpAumAfterDeposit.add(new BN(175).mul(QUOTE_PRECISION)) + ) + ); + + // Calculate the expected transfer amount which is the increase in fee pool - amount owed, + // but we have to consider the fee pool limitations + const pnlPoolBalance0 = getTokenAmount( + adminClient.getPerpMarketAccount(0).pnlPool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const feePoolBalance0 = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + + const pnlPoolBalance1 = getTokenAmount( + adminClient.getPerpMarketAccount(1).pnlPool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const feePoolBalance1 = getTokenAmount( + adminClient.getPerpMarketAccount(1).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + + // Expected transfers per pool are capital constrained by the actual balances + const expectedTransfer0 = BN.min( + ammCache.cache[0].quoteOwedFromLpPool.muln(-1), + pnlPoolBalance0.add(feePoolBalance0).sub(QUOTE_PRECISION.muln(25)) + ); + const expectedTransfer1 = BN.min( + ammCache.cache[1].quoteOwedFromLpPool.muln(-1), + pnlPoolBalance1.add(feePoolBalance1) + ); + const expectedTransferAmount = expectedTransfer0.add(expectedTransfer1); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) + ); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + await adminClient.sendTransaction(settleTx); + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterSettle = lpPool.lastAum; + assert(lpAumAfterSettle.eq(lpAumAfterUpdateCacheBeforeSettle)); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const usdcAfter = constituent.vaultTokenBalance; + const feePoolBalanceAfter = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + console.log('usdcBefore', usdcBefore.toString()); + console.log('usdcAfter', usdcAfter.toString()); + + // Verify the expected usdc transfer amount + assert(usdcAfter.sub(usdcBefore).eq(expectedTransferAmount)); + console.log('feePoolBalanceBefore', feePoolBalance0.toString()); + console.log('feePoolBalanceAfter', feePoolBalanceAfter.toString()); + // Fee pool can cover it all in first perp market + expect( + feePoolBalance0.sub(feePoolBalanceAfter).toNumber() + ).to.be.approximately(expectedTransfer0.toNumber(), 1); + + // Constituent sync worked successfully + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const constituentVault = + await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + assert( + new BN(constituentVault.amount.toString()).eq( + constituent.vaultTokenBalance + ) + ); + }); + + it('will settle gracefully when trying to settle pnl from constituents to perp markets if not enough usdc in the constituent vault', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + + /// First remove some liquidity so DLP doesnt have enought to transfer + const lpTokenBalance = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + const tx = new Transaction(); + tx.add( + ...(await adminClient.getAllSettlePerpToLpPoolIxs( + lpPool.lpPoolId, + [0, 1, 2] + )) + ); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + tx.add( + ...(await adminClient.getLpPoolRemoveLiquidityIx({ + outMarketIndex: 0, + lpToBurn: new BN(lpTokenBalance.amount.toString()), + minAmountOut: new BN(1000).mul(QUOTE_PRECISION), + lpPool: lpPool, + })) + ); + await adminClient.sendTransaction(tx); + + let constituentVault = + await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + + const expectedTransferAmount = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const constituentUSDCBalanceBefore = constituentVault.amount; + + // Temporarily overwrite perp market to have taken a loss on the fee pool + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const spotMarket = adminClient.getSpotMarketAccount(0); + const perpMarket = adminClient.getPerpMarketAccount(0); + spotMarket.depositBalance = spotMarket.depositBalance.sub( + perpMarket.amm.feePool.scaledBalance.add( + spotMarket.cumulativeDepositInterest.muln(10 ** 3) + ) + ); + await overWriteSpotMarket( + adminClient, + bankrunContextWrapper, + spotMarket.pubkey, + spotMarket + ); + perpMarket.amm.feePool.scaledBalance = ZERO; + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + /// Now finally try and settle Perp to LP Pool + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) + ); + await adminClient.sendTransaction(settleTx); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + constituentVault = await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + + // Should have written fee pool amount owed to the amm cache and new constituent usdc balane should just be the quote precision to leave aum > 0 + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + // No more usdc left in the constituent vault + console.log('constituentVault.amount', constituentVault.amount.toString()); + assert(constituent.vaultTokenBalance.eq(QUOTE_PRECISION)); + assert(new BN(constituentVault.amount.toString()).eq(QUOTE_PRECISION)); + + // Should have recorded the amount left over to the amm cache and increased the amount in the fee pool + assert( + ammCache.cache[0].lastFeePoolTokenAmount.eq( + new BN(constituentUSDCBalanceBefore.toString()).sub(QUOTE_PRECISION) + ) + ); + expect( + ammCache.cache[0].quoteOwedFromLpPool.toNumber() + ).to.be.approximately( + expectedTransferAmount + .sub(new BN(constituentUSDCBalanceBefore.toString())) + .add(QUOTE_PRECISION) + .toNumber(), + 1 + ); + assert( + adminClient + .getPerpMarketAccount(0) + .amm.feePool.scaledBalance.eq( + new BN(constituentUSDCBalanceBefore.toString()) + .sub(QUOTE_PRECISION) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ) + ); + + // Update the LP pool AUM + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + assert(lpPool.lastAum.eq(QUOTE_PRECISION)); + }); + + it('perp market will not transfer with the constituent vault if it is owed from dlp', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const owedAmount = ammCache.cache[0].quoteOwedFromLpPool; + + // Give the perp market half of its owed amount + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.feePool.scaledBalance = + perpMarket.amm.feePool.scaledBalance.add( + owedAmount + .div(TWO) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) + ); + await adminClient.sendTransaction(settleTx); + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + expect( + ammCache.cache[0].quoteOwedFromLpPool.toNumber() + ).to.be.approximately(owedAmount.divn(2).toNumber(), 1); + assert(constituent.vaultTokenBalance.eq(QUOTE_PRECISION)); + assert(lpPool.lastAum.eq(QUOTE_PRECISION)); + + // Deposit here to DLP to make sure aum calc work with perp market debt + await overWriteMintAccount( + bankrunContextWrapper, + lpPool.mint, + BigInt(lpPool.lastAum.toNumber()) + ); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + await adminClient.sendTransaction(tx); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + let aum = new BN(0); + for (let i = 0; i <= 2; i++) { + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + aum = aum.add( + constituent.vaultTokenBalance + .mul(constituent.lastOraclePrice) + .div(QUOTE_PRECISION) + ); + } + + // Overwrite the amm cache with amount owed + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + for (let i = 0; i <= ammCache.cache.length - 1; i++) { + aum = aum.sub(ammCache.cache[i].quoteOwedFromLpPool); + } + assert(lpPool.lastAum.eq(aum)); + }); + + it('perp market will transfer with the constituent vault if it should send more than its owed', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const aumBefore = lpPool.lastAum; + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + const balanceBefore = constituent.vaultTokenBalance; + const owedAmount = ammCache.cache[0].quoteOwedFromLpPool; + + // Give the perp market double of its owed amount + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.feePool.scaledBalance = + perpMarket.amm.feePool.scaledBalance.add( + owedAmount + .mul(TWO) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) + ); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + await adminClient.sendTransaction(settleTx); + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(ammCache.cache[0].quoteOwedFromLpPool.eq(ZERO)); + assert(constituent.vaultTokenBalance.eq(balanceBefore.add(owedAmount))); + assert(lpPool.lastAum.eq(aumBefore.add(owedAmount.muln(2)))); + }); + + it('can work with multiple derivatives on the same parent', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + await adminClient.initializeConstituent(lpPool.lpPoolId, { + spotMarketIndex: 3, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(4), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ + ZERO, + PERCENTAGE_PRECISION.muln(87).divn(100), + PERCENTAGE_PRECISION, + ], + constituentDerivativeIndex: 1, + }); + + await adminClient.updateConstituentParams( + lpPool.lpPoolId, + getConstituentPublicKey(program.programId, lpPoolKey, 2), + { + derivativeWeight: PERCENTAGE_PRECISION.divn(4), + } + ); + + await adminClient.updateAmmCache([0, 1, 2]); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 3) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 3) + )) as ConstituentAccount; + assert(!constituent.lastOraclePrice.eq(ZERO)); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ]) + ); + await adminClient.sendTransaction(tx); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + expect(constituentTargetBase).to.not.be.null; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect( + constituentTargetBase.targets[2].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[3].targetBase.toNumber(), + 10 + ); + expect( + constituentTargetBase.targets[3].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[1].targetBase.toNumber() / 2, + 10 + ); + + // Set the derivative weights to 0 + await adminClient.updateConstituentParams( + lpPool.lpPoolId, + getConstituentPublicKey(program.programId, lpPoolKey, 2), + { + derivativeWeight: ZERO, + } + ); + + await adminClient.updateConstituentParams( + lpPool.lpPoolId, + getConstituentPublicKey(program.programId, lpPoolKey, 3), + { + derivativeWeight: ZERO, + } + ); + + const parentTargetBaseBefore = constituentTargetBase.targets[1].targetBase; + const tx2 = new Transaction(); + tx2 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ]) + ); + await adminClient.sendTransaction(tx2); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + + const parentTargetBaseAfter = constituentTargetBase.targets[1].targetBase; + + expect(parentTargetBaseAfter.toNumber()).to.be.approximately( + parentTargetBaseBefore.toNumber() * 2, + 10 + ); + }); + + it('cant withdraw more than constituent limit', async () => { + await adminClient.updateConstituentParams( + lpPoolId, + getConstituentPublicKey(program.programId, lpPoolKey, 0), + { + maxBorrowTokenAmount: new BN(10).muln(10 ** 6), + } + ); + + try { + await adminClient.withdrawFromProgramVault( + lpPoolId, + 0, + new BN(100).mul(QUOTE_PRECISION) + ); + } catch (e) { + console.log(e); + assert(e.toString().includes('0x18bf')); // invariant failed + } + }); + + it('cant disable lp pool settling', async () => { + await adminClient.updateFeatureBitFlagsSettleLpPool(false); + + try { + await adminClient.settlePerpToLpPool(lpPoolId, [0, 1, 2]); + assert(false, 'Should have thrown'); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18c2')); // SettleLpPoolDisabled + } + + await adminClient.updateFeatureBitFlagsSettleLpPool(true); + }); + + it('can do spot vault withdraws when there are borrows', async () => { + // First deposit into wsol account from subaccount 1 + await adminClient.initializeUserAccount(1); + const pubkey = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + new BN(7_000).mul(new BN(10 ** 9)) + ); + await adminClient.deposit(new BN(1000).mul(new BN(10 ** 9)), 2, pubkey, 1); + const lpPool = await adminClient.getLpPoolAccount(lpPoolId); + + // Deposit into LP pool some balance + const ixs = []; + ixs.push(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + ixs.push( + ...(await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 2, + minMintAmount: new BN(1), + lpPool, + inAmount: new BN(100).mul(new BN(10 ** 9)), + })) + ); + await adminClient.sendTransaction(new Transaction().add(...ixs)); + await adminClient.depositToProgramVault( + lpPool.lpPoolId, + 2, + new BN(100).mul(new BN(10 ** 9)) + ); + + const spotMarket = adminClient.getSpotMarketAccount(2); + spotMarket.depositBalance = new BN(1_186_650_830_132); + spotMarket.borrowBalance = new BN(320_916_317_572); + spotMarket.cumulativeBorrowInterest = new BN(697_794_836_247_770); + spotMarket.cumulativeDepositInterest = new BN(188_718_954_233_794); + await overWriteSpotMarket( + adminClient, + bankrunContextWrapper, + spotMarket.pubkey, + spotMarket + ); + + // const curClock = + // await bankrunContextWrapper.provider.context.banksClient.getClock(); + // bankrunContextWrapper.provider.context.setClock( + // new Clock( + // curClock.slot, + // curClock.epochStartTimestamp, + // curClock.epoch, + // curClock.leaderScheduleEpoch, + // curClock.unixTimestamp + BigInt(60 * 60 * 24 * 365 * 10) + // ) + // ); + + await adminClient.withdrawFromProgramVault( + lpPoolId, + 2, + new BN(500).mul(new BN(10 ** 9)) + ); + }); + + it('whitelist mint', async () => { + await adminClient.updateLpPoolParams(lpPoolId, { + whitelistMint: whitelistMint, + }); + + const lpPool = await adminClient.getLpPoolAccount(lpPoolId); + assert(lpPool.whitelistMint.equals(whitelistMint)); + + console.log('lpPool.whitelistMint', lpPool.whitelistMint.toString()); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + try { + await adminClient.sendTransaction(tx); + assert(false, 'Should have thrown'); + } catch (e) { + assert(e.toString().includes('0x1789')); // invalid whitelist token + } + + const whitelistMintAta = getAssociatedTokenAddressSync( + whitelistMint, + adminClient.wallet.publicKey + ); + const ix = createAssociatedTokenAccountInstruction( + bankrunContextWrapper.context.payer.publicKey, + whitelistMintAta, + adminClient.wallet.publicKey, + whitelistMint + ); + const mintToIx = createMintToInstruction( + whitelistMint, + whitelistMintAta, + bankrunContextWrapper.provider.wallet.publicKey, + 1 + ); + await bankrunContextWrapper.sendTransaction( + new Transaction().add(ix, mintToIx) + ); + + const txAfter = new Transaction(); + txAfter.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + txAfter.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + + // successfully call add liquidity + await adminClient.sendTransaction(txAfter); + }); + + it('can initialize multiple lp pools', async () => { + const newLpPoolId = 1; + + await adminClient.initializeLpPool( + newLpPoolId, + ZERO, + new BN(1_000_000_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() + ); + await adminClient.initializeConstituent(newLpPoolId, { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(2), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [], + constituentDerivativeIndex: -1, + }); + + const oldLpPool = await adminClient.getLpPoolAccount(lpPoolId); + // cant settle a perp market to the new lp pool with a different id + + try { + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(newLpPoolId, [0, 1, 2]) + ); + settleTx.add( + await adminClient.getUpdateLpPoolAumIxs(oldLpPool, [0, 1, 2]) + ); + await adminClient.sendTransaction(settleTx); + assert.fail('Should have thrown'); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18')); + } + }); +}); diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts new file mode 100644 index 0000000000..d256c56ed3 --- /dev/null +++ b/tests/lpPoolCUs.ts @@ -0,0 +1,658 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + AddressLookupTableProgram, + ComputeBudgetProgram, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, + TransactionMessage, + VersionedTransaction, +} from '@solana/web3.js'; +import { + createInitializeMint2Instruction, + getMint, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; + +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBaseAccount, + AmmConstituentMapping, + LPPoolAccount, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_RATE_PRECISION, + getAmmCachePublicKey, + AmmCache, + ZERO, + getConstituentPublicKey, + ConstituentAccount, + PositionDirection, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + BASE_PRECISION, +} from '../sdk/src'; + +import { + initializeQuoteSpotMarket, + mockAtaTokenAccountForMint, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccountWithAuthority, + overwriteConstituentAccount, + sleep, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_STORAGE_DATA } from './pythLazerData'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + +const NUMBER_OF_CONSTITUENTS = 10; +const NUMBER_OF_PERP_MARKETS = 60; +const NUMBER_OF_USERS = Math.ceil(NUMBER_OF_PERP_MARKETS / 8); + +const PERP_MARKET_INDEXES = Array.from( + { length: NUMBER_OF_PERP_MARKETS }, + (_, i) => i +); +const SPOT_MARKET_INDEXES = Array.from( + { length: NUMBER_OF_CONSTITUENTS + 2 }, + (_, i) => i +); +const CONSTITUENT_INDEXES = Array.from( + { length: NUMBER_OF_CONSTITUENTS }, + (_, i) => i +); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let _userLpTokenAccount: PublicKey; + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle2: PublicKey; + + let adminKeypair: Keypair; + + let lutAddress: PublicKey; + + const userClients: TestClient[] = []; + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + let solUsd: PublicKey; + + const lpPoolId = 0; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey(program.programId, lpPoolId); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const imfFactor = 0; + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle2 = await mockOracleNoProgram(bankrunContextWrapper, 200); + + const keypair = new Keypair(); + adminKeypair = keypair; + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 12); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 200); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1, 2], + spotMarketIndexes: [0, 1], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(1_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + + await adminClient.initializePythLazerOracle(6); + + await adminClient.updatePerpAuctionDuration(new BN(0)); + + console.log('create whitelist mint'); + const whitelistKeypair = Keypair.generate(); + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: bankrunContextWrapper.provider.wallet.publicKey, + newAccountPubkey: whitelistKeypair.publicKey, + space: MINT_SIZE, + lamports: 10_000_000_000, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + whitelistKeypair.publicKey, + 0, + bankrunContextWrapper.provider.wallet.publicKey, + bankrunContextWrapper.provider.wallet.publicKey, + TOKEN_PROGRAM_ID + ) + ); + + await bankrunContextWrapper.sendTransaction(transaction, [ + whitelistKeypair, + ]); + + const whitelistMintInfo = + await bankrunContextWrapper.connection.getAccountInfo( + whitelistKeypair.publicKey + ); + console.log('whitelistMintInfo', whitelistMintInfo); + }); + + after(async () => { + await adminClient.unsubscribe(); + for (const userClient of userClients) { + await userClient.unsubscribe(); + } + }); + + it('can create a new LP Pool', async () => { + await adminClient.initializeLpPool( + lpPoolId, + ZERO, + new BN(1_000_000_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + new Keypair() + ); + await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + + // check LpPool created + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + _userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminClient.wallet.publicKey + ); + + // Check amm constituent map exists + const ammConstituentMapPublicKey = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammConstituentMap = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapPublicKey + )) as AmmConstituentMapping; + expect(ammConstituentMap).to.not.be.null; + assert(ammConstituentMap.weights.length == 0); + + // check constituent target weights exists + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 0); + + // check mint created correctly + const mintInfo = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(mintInfo.decimals).to.equal(tokenDecimals); + expect(Number(mintInfo.supply)).to.equal(0); + expect(mintInfo.mintAuthority?.toBase58()).to.equal(lpPoolKey.toBase58()); + }); + + it('can add constituents to LP Pool', async () => { + // USDC Constituent + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [], + }); + + for (let i = 0; i < NUMBER_OF_CONSTITUENTS; i++) { + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle2, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + await sleep(50); + } + await adminClient.unsubscribe(); + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(adminKeypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1], + spotMarketIndexes: SPOT_MARKET_INDEXES, + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.subscribe(); + await sleep(50); + + const correlations = [ZERO]; + for (let i = 1; i < NUMBER_OF_CONSTITUENTS; i++) { + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: i, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + derivativeWeight: ZERO, + volatility: PERCENTAGE_PRECISION.muln( + Math.floor(Math.random() * 10) + ).divn(100), + constituentCorrelations: correlations, + }); + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(lpPool.constituents == i + 1); + + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == i + 1); + + correlations.push(new BN(Math.floor(Math.random() * 100)).divn(100)); + } + }); + + it('can initialize many perp markets and given some inventory', async () => { + for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { + await adminClient.initializePerpMarket( + i, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + new BN(0), + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(i, 1); + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(i, 100); + await sleep(50); + } + + await adminClient.unsubscribe(); + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(adminKeypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: PERP_MARKET_INDEXES, + spotMarketIndexes: SPOT_MARKET_INDEXES, + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.subscribe(); + }); + + it('can initialize all the different extra users', async () => { + for (let i = 0; i < NUMBER_OF_USERS; i++) { + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 9); + await sleep(100); + const userClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: PERP_MARKET_INDEXES, + spotMarketIndexes: SPOT_MARKET_INDEXES, + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await userClient.subscribe(); + await sleep(100); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + await sleep(100); + + await userClient.initializeUserAccountAndDepositCollateral( + new BN(10_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + await sleep(100); + userClients.push(userClient); + } + + let userIndex = 0; + for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { + // Give the vamm some inventory + const userClient = userClients[userIndex]; + await userClient.openPosition(PositionDirection.LONG, BASE_PRECISION, i); + await sleep(50); + if ( + userClient + .getUser() + .getActivePerpPositions() + .filter((x) => !x.baseAssetAmount.eq(ZERO)).length == 8 + ) { + userIndex++; + } + } + }); + + it('can add lots of mapping data', async () => { + // Assume that constituent 0 is USDC + for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { + for (let j = 1; j <= 3; j++) { + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: i, + constituentIndex: j, + weight: PERCENTAGE_PRECISION.divn(3), + }, + ]); + await sleep(50); + } + } + }); + + it('can add all addresses to lookup tables', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + const [lookupTableInst, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority: adminClient.wallet.publicKey, + payer: adminClient.wallet.publicKey, + recentSlot: slot.toNumber() - 10, + }); + + const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + payer: adminClient.wallet.publicKey, + authority: adminClient.wallet.publicKey, + lookupTable: lookupTableAddress, + addresses: CONSTITUENT_INDEXES.map((i) => + getConstituentPublicKey(program.programId, lpPoolKey, i) + ), + }); + + const tx = new Transaction().add(lookupTableInst).add(extendInstruction); + await adminClient.sendTransaction(tx); + lutAddress = lookupTableAddress; + + const chunkies = chunks( + adminClient.getPerpMarketAccounts().map((account) => account.pubkey), + 20 + ); + for (const chunk of chunkies) { + const extendTx = new Transaction(); + const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + payer: adminClient.wallet.publicKey, + authority: adminClient.wallet.publicKey, + lookupTable: lookupTableAddress, + addresses: chunk, + }); + extendTx.add(extendInstruction); + await adminClient.sendTransaction(extendTx); + } + }); + + it('can crank amm info into the cache', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + for (const chunk of chunks(PERP_MARKET_INDEXES, 20)) { + const txSig = await adminClient.updateAmmCache(chunk); + const cus = + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); + console.log(cus); + assert(cus < 200_000); + } + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + expect(ammCache).to.not.be.null; + assert(ammCache.cache.length == NUMBER_OF_PERP_MARKETS); + }); + + it('can update target balances', async () => { + for (let i = 0; i < NUMBER_OF_CONSTITUENTS; i++) { + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + } + + const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_400_000, + }); + const ammCacheIxs = await Promise.all( + chunks(PERP_MARKET_INDEXES, 50).map( + async (chunk) => await adminClient.getUpdateAmmCacheIx(chunk) + ) + ); + const updateBaseIx = await adminClient.getUpdateLpConstituentTargetBaseIx( + lpPoolId, + [getConstituentPublicKey(program.programId, lpPoolKey, 1)] + ); + + const txMessage = new TransactionMessage({ + payerKey: adminClient.wallet.publicKey, + recentBlockhash: (await adminClient.connection.getLatestBlockhash()) + .blockhash, + instructions: [cuIx, ...ammCacheIxs, updateBaseIx], + }); + + const lookupTableAccount = ( + await bankrunContextWrapper.connection.getAddressLookupTable(lutAddress) + ).value; + const message = txMessage.compileToV0Message([lookupTableAccount]); + + const txSig = await adminClient.connection.sendTransaction( + new VersionedTransaction(message) + ); + + const cus = Number( + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig) + ); + console.log(cus); + + // assert(+cus.toString() < 100_000); + }); + + it('can update AUM with high balances', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + for (let i = 0; i < NUMBER_OF_CONSTITUENTS; i++) { + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + getConstituentPublicKey(program.programId, lpPoolKey, i), + [['vaultTokenBalance', QUOTE_PRECISION.muln(1000)]] + ); + } + + const tx = new Transaction(); + tx.add( + await adminClient.getUpdateLpPoolAumIxs(lpPool, CONSTITUENT_INDEXES) + ); + const txSig = await adminClient.sendTransaction(tx); + const cus = Number( + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig.txSig) + ); + console.log(cus); + }); +}); + +const chunks = (array: readonly T[], size: number): T[][] => { + return new Array(Math.ceil(array.length / size)) + .fill(null) + .map((_, index) => index * size) + .map((begin) => array.slice(begin, begin + size)); +}; diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts new file mode 100644 index 0000000000..4361c9a948 --- /dev/null +++ b/tests/lpPoolSwap.ts @@ -0,0 +1,998 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; +import { Program } from '@coral-xyz/anchor'; +import { + Account, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBaseAccount, + OracleSource, + SPOT_MARKET_RATE_PRECISION, + SPOT_MARKET_WEIGHT_PRECISION, + LPPoolAccount, + convertToNumber, + getConstituentVaultPublicKey, + getConstituentPublicKey, + ConstituentAccount, + ZERO, + getSerumSignerPublicKey, + BN_MAX, + isVariant, + ConstituentStatus, + getSignedTokenAmount, + getTokenAmount, +} from '../sdk/src'; +import { + initializeQuoteSpotMarket, + mockUSDCMint, + mockUserUSDCAccount, + mockOracleNoProgram, + setFeedPriceNoProgram, + overWriteTokenAccountBalance, + overwriteConstituentAccount, + mockAtaTokenAccountForMint, + overWriteMintAccount, + createWSolTokenAccountForUser, + initializeSolSpotMarket, + createUserWithUSDCAndWSOLAccount, + sleep, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { DexInstructions, Market, OpenOrders } from '@project-serum/serum'; +import { listMarket, SERUM, makePlaceOrderTransaction } from './serumHelper'; +import { NATIVE_MINT } from '@solana/spl-token'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + // Align account (de)serialization with on-chain zero-copy layouts + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle: PublicKey; + + let serumMarketPublicKey: PublicKey; + + let serumDriftClient: TestClient; + let serumWSOL: PublicKey; + let serumUSDC: PublicKey; + let serumKeypair: Keypair; + + let adminSolAta: PublicKey; + + let openOrdersAccount: PublicKey; + + const usdcAmount = new BN(500 * 10 ** 6); + const solAmount = new BN(2 * 10 ** 9); + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(10 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(10 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const lpPoolId = 0; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey(program.programId, lpPoolId); + + let userUSDCAccount: Keypair; + let serumMarket: Market; + + before(async () => { + const context = await startAnchor( + '', + [ + { + name: 'serum_dex', + programId: new PublicKey( + 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX' + ), + }, + ], + [] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200.1); + + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 50 * LAMPORTS_PER_SOL); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1], + spotMarketIndexes: [0, 1, 2], + oracleInfos: [ + { + publicKey: spotMarketOracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + // Ensure the client uses the same custom coder + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + new BN(10).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair.publicKey + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(10).mul(QUOTE_PRECISION), + userUSDCAccount.publicKey + ); + + const periodicity = new BN(0); + + await adminClient.initializePerpMarket( + 0, + spotMarketOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(0, 1); + + await adminClient.initializePerpMarket( + 1, + spotMarketOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(1, 1); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const imfFactor = 0; + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + adminSolAta = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + new BN(20 * 10 ** 9) // 10 SOL + ); + + await adminClient.initializeLpPool( + lpPoolId, + new BN(100), // 1 bps + new BN(100_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() // dlp mint + ); + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION, + volatility: ZERO, + constituentCorrelations: [], + }); + await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 1, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: PERCENTAGE_PRECISION.muln(4).divn(100), + constituentCorrelations: [ZERO], + }); + + await initializeSolSpotMarket(adminClient, spotMarketOracle); + await adminClient.updateSpotMarketStepSizeAndTickSize( + 2, + new BN(100000000), + new BN(100) + ); + await adminClient.updateSpotAuctionDuration(0); + + await adminClient.deposit( + new BN(5 * 10 ** 9), // 10 SOL + 2, // market index + adminSolAta // user token account + ); + + await adminClient.depositIntoSpotMarketVault( + 2, + new BN(4 * 10 ** 9), // 4 SOL + adminSolAta + ); + + [serumDriftClient, serumWSOL, serumUSDC, serumKeypair] = + await createUserWithUSDCAndWSOLAccount( + bankrunContextWrapper, + usdcMint, + program, + solAmount, + usdcAmount, + [], + [0, 1], + [ + { + publicKey: spotMarketOracle, + source: OracleSource.PYTH, + }, + ], + bulkAccountLoader + ); + + await bankrunContextWrapper.fundKeypair( + serumKeypair, + 50 * LAMPORTS_PER_SOL + ); + await serumDriftClient.deposit(usdcAmount, 0, serumUSDC); + }); + + after(async () => { + await adminClient.unsubscribe(); + await serumDriftClient.unsubscribe(); + }); + + it('LP Pool init properly', async () => { + let lpPool: LPPoolAccount; + try { + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + expect(lpPool).to.not.be.null; + } catch (e) { + expect.fail('LP Pool should have been created'); + } + + try { + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 2); + } catch (e) { + expect.fail('Amm constituent map should have been created'); + } + }); + + it('lp pool swap', async () => { + let spotOracle = adminClient.getOracleDataForSpotMarket(1); + const price1 = convertToNumber(spotOracle.price); + + await setFeedPriceNoProgram(bankrunContextWrapper, 224.3, spotMarketOracle); + + await adminClient.fetchAccounts(); + + spotOracle = adminClient.getOracleDataForSpotMarket(1); + const price2 = convertToNumber(spotOracle.price); + assert(price2 > price1); + + const const0TokenAccount = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const const1TokenAccount = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 1 + ); + + const const0Key = getConstituentPublicKey(program.programId, lpPoolKey, 0); + const const1Key = getConstituentPublicKey(program.programId, lpPoolKey, 1); + + const c0TokenBalance = new BN(224_300_000_000); + const c1TokenBalance = new BN(1_000_000_000); + + await overWriteTokenAccountBalance( + bankrunContextWrapper, + const0TokenAccount, + BigInt(c0TokenBalance.toString()) + ); + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + const0Key, + [['vaultTokenBalance', c0TokenBalance]] + ); + + await overWriteTokenAccountBalance( + bankrunContextWrapper, + const1TokenAccount, + BigInt(c1TokenBalance.toString()) + ); + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + const1Key, + [['vaultTokenBalance', c1TokenBalance]] + ); + + // check fields overwritten correctly + const c0 = (await adminClient.program.account.constituent.fetch( + const0Key + )) as ConstituentAccount; + expect(c0.vaultTokenBalance.toString()).to.equal(c0TokenBalance.toString()); + + const c1 = (await adminClient.program.account.constituent.fetch( + const1Key + )) as ConstituentAccount; + expect(c1.vaultTokenBalance.toString()).to.equal(c1TokenBalance.toString()); + + await adminClient.updateConstituentOracleInfo(c1); + await adminClient.updateConstituentOracleInfo(c0); + + const prec = new BN(10).pow(new BN(tokenDecimals)); + console.log( + `const0 balance: ${convertToNumber(c0.vaultTokenBalance, prec)}` + ); + console.log( + `const1 balance: ${convertToNumber(c1.vaultTokenBalance, prec)}` + ); + + const lpPool1 = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + expect(lpPool1.lastAumSlot.toNumber()).to.be.equal(0); + + await adminClient.updateLpPoolAum(lpPool1, [1, 0]); + + const lpPool2 = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + expect(lpPool2.lastAumSlot.toNumber()).to.be.greaterThan(0); + expect(lpPool2.lastAum.gt(lpPool1.lastAum)).to.be.true; + console.log(`AUM: ${convertToNumber(lpPool2.lastAum, QUOTE_PRECISION)}`); + + // swap c0 for c1 + + const adminAuth = adminClient.wallet.publicKey; + + // mint some tokens for user + const c0UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + usdcMint.publicKey, + new BN(224_300_000_000), + adminAuth + ); + const c1UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + spotTokenMint.publicKey, + new BN(1_000_000_000), + adminAuth + ); + + const inTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const outTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c1UserTokenAccount + ); + + // in = 0, out = 1 + const swapTx = new Transaction(); + swapTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool2, [0, 1])); + swapTx.add( + await adminClient.getLpPoolSwapIx( + 0, + 1, + new BN(224_300_000), + new BN(0), + lpPoolKey, + adminAuth + ) + ); + + // Should throw since we havnet enabled swaps yet + try { + await adminClient.sendTransaction(swapTx); + assert(false, 'Should have thrown'); + } catch (error) { + assert(error.message.includes('0x17f1')); + } + + // Enable swaps + await adminClient.updateFeatureBitFlagsSwapLpPool(true); + + // Send swap + await adminClient.sendTransaction(swapTx); + + const inTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const outTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c1UserTokenAccount + ); + const diffInToken = + inTokenBalanceAfter.amount - inTokenBalanceBefore.amount; + const diffOutToken = + outTokenBalanceAfter.amount - outTokenBalanceBefore.amount; + + expect(Number(diffInToken)).to.be.equal(-224_300_000); + expect(Number(diffOutToken)).to.be.approximately(1001298, 1); + + console.log( + `in Token: ${inTokenBalanceBefore.amount} -> ${ + inTokenBalanceAfter.amount + } (${Number(diffInToken) / 1e6})` + ); + console.log( + `out Token: ${outTokenBalanceBefore.amount} -> ${ + outTokenBalanceAfter.amount + } (${Number(diffOutToken) / 1e6})` + ); + }); + + it('lp pool add and remove liquidity: usdc', async () => { + // add c0 liquidity + const adminAuth = adminClient.wallet.publicKey; + const c0UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + usdcMint.publicKey, + new BN(1_000_000_000_000), + adminAuth + ); + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + await adminClient.updateLpPoolAum(lpPool, [0, 1]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpPoolAumBefore = lpPool.lastAum; + + const userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminAuth + ); + + // check fields overwritten correctly + const c0 = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + const c1 = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 1) + )) as ConstituentAccount; + await adminClient.updateConstituentOracleInfo(c1); + await adminClient.updateConstituentOracleInfo(c0); + + const userC0TokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + await overWriteMintAccount( + bankrunContextWrapper, + lpPool.mint, + BigInt(lpPool.lastAum.toNumber()) + ); + + const tokensAdded = new BN(1_000_000_000_000); + let tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 0, + inAmount: tokensAdded, + minMintAmount: new BN(1), + lpPool: lpPool, + })) + ); + await adminClient.sendTransaction(tx); + + // Should fail to add more liquidity if it's in redulce only mode; + await adminClient.updateConstituentStatus( + c0.pubkey, + ConstituentStatus.REDUCE_ONLY + ); + tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 0, + inAmount: tokensAdded, + minMintAmount: new BN(1), + lpPool: lpPool, + })) + ); + try { + await adminClient.sendTransaction(tx); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18c5')); // InvalidConstituentOperation + } + await adminClient.updateConstituentStatus( + c0.pubkey, + ConstituentStatus.ACTIVE + ); + + await sleep(500); + + const userC0TokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpPoolAumAfter = lpPool.lastAum; + const lpPoolAumDiff = lpPoolAumAfter.sub(lpPoolAumBefore); + expect(lpPoolAumDiff.toString()).to.be.equal(tokensAdded.toString()); + + const userC0TokenBalanceDiff = + Number(userC0TokenBalanceAfter.amount) - + Number(userC0TokenBalanceBefore.amount); + expect(Number(userC0TokenBalanceDiff)).to.be.equal( + -1 * tokensAdded.toNumber() + ); + + const userLpTokenBalanceDiff = + Number(userLpTokenBalanceAfter.amount) - + Number(userLpTokenBalanceBefore.amount); + expect(userLpTokenBalanceDiff).to.be.equal( + (((tokensAdded.toNumber() * 9997) / 10000) * 9999) / 10000 + ); // max weight deviation: expect min swap% fee on constituent, + 0.01% lp mint fee + + const constituentBalanceBefore = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + console.log(`constituentBalanceBefore: ${constituentBalanceBefore}`); + + // remove liquidity + const removeTx = new Transaction(); + removeTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + removeTx.add( + await adminClient.getDepositToProgramVaultIx( + lpPoolId, + 0, + new BN(constituentBalanceBefore) + ) + ); + removeTx.add( + ...(await adminClient.getLpPoolRemoveLiquidityIx({ + outMarketIndex: 0, + lpToBurn: new BN(userLpTokenBalanceAfter.amount.toString()), + minAmountOut: new BN(1), + lpPool: lpPool, + })) + ); + await adminClient.sendTransaction(removeTx); + + const constituentAfterRemoveLiquidity = + (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const blTokenAmountAfterRemoveLiquidity = getSignedTokenAmount( + getTokenAmount( + constituentAfterRemoveLiquidity.spotBalance.scaledBalance, + adminClient.getSpotMarketAccount(0), + constituentAfterRemoveLiquidity.spotBalance.balanceType + ), + constituentAfterRemoveLiquidity.spotBalance.balanceType + ); + + const withdrawFromProgramVaultTx = new Transaction(); + withdrawFromProgramVaultTx.add( + await adminClient.getWithdrawFromProgramVaultIx( + lpPoolId, + 0, + blTokenAmountAfterRemoveLiquidity.abs() + ) + ); + await adminClient.sendTransaction(withdrawFromProgramVaultTx); + + const userC0TokenBalanceAfterBurn = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceAfterBurn = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + const userC0TokenBalanceAfterBurnDiff = + Number(userC0TokenBalanceAfterBurn.amount) - + Number(userC0TokenBalanceAfter.amount); + + expect(userC0TokenBalanceAfterBurnDiff).to.be.greaterThan(0); + expect(Number(userLpTokenBalanceAfterBurn.amount)).to.be.equal(0); + + const totalC0TokensLost = new BN( + userC0TokenBalanceAfterBurn.amount.toString() + ).sub(tokensAdded); + const totalC0TokensLostPercent = + Number(totalC0TokensLost) / Number(tokensAdded); + expect(totalC0TokensLostPercent).to.be.approximately(-0.0006, 0.0001); // lost about 7bps swapping in an out + }); + + it('Add Serum Market', async () => { + serumMarketPublicKey = await listMarket({ + context: bankrunContextWrapper, + wallet: bankrunContextWrapper.provider.wallet, + baseMint: NATIVE_MINT, + quoteMint: usdcMint.publicKey, + baseLotSize: 100000000, + quoteLotSize: 100, + dexProgramId: SERUM, + feeRateBps: 0, + }); + + serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'confirmed' }, + SERUM + ); + + await adminClient.initializeSerumFulfillmentConfig( + 2, + serumMarketPublicKey, + SERUM + ); + + serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'recent' }, + SERUM + ); + + const serumOpenOrdersAccount = new Account(); + const createOpenOrdersIx = await OpenOrders.makeCreateAccountTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket.address, + serumDriftClient.wallet.publicKey, + serumOpenOrdersAccount.publicKey, + serumMarket.programId + ); + await serumDriftClient.sendTransaction( + new Transaction().add(createOpenOrdersIx), + [serumOpenOrdersAccount] + ); + + const adminOpenOrdersAccount = new Account(); + const adminCreateOpenOrdersIx = + await OpenOrders.makeCreateAccountTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket.address, + adminClient.wallet.publicKey, + adminOpenOrdersAccount.publicKey, + serumMarket.programId + ); + await adminClient.sendTransaction( + new Transaction().add(adminCreateOpenOrdersIx), + [adminOpenOrdersAccount] + ); + + openOrdersAccount = adminOpenOrdersAccount.publicKey; + }); + + it('swap sol for usdc', async () => { + // Initialize new constituent for market 2 + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 2, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [ZERO, PERCENTAGE_PRECISION], + }); + + const beforeSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + console.log(`beforeSOLBalance: ${beforeSOLBalance}`); + const beforeUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + console.log(`beforeUSDCBalance: ${beforeUSDCBalance}`); + + const serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'recent' }, + SERUM + ); + + const adminSolAccount = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + ZERO + ); + + // place ask to sell 1 sol for 100 usdc + const { transaction, signers } = await makePlaceOrderTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket, + { + owner: serumDriftClient.wallet, + payer: serumWSOL, + side: 'sell', + price: 100, + size: 1, + orderType: 'postOnly', + clientId: undefined, // todo? + openOrdersAddressKey: undefined, + openOrdersAccount: undefined, + feeDiscountPubkey: null, + selfTradeBehavior: 'abortTransaction', + maxTs: BN_MAX, + } + ); + + const signerKeypairs = signers.map((signer) => { + return Keypair.fromSecretKey(signer.secretKey); + }); + + await serumDriftClient.sendTransaction(transaction, signerKeypairs); + + const amountIn = new BN(200).muln( + 10 ** adminClient.getSpotMarketAccount(0).decimals + ); + + const { beginSwapIx, endSwapIx } = await adminClient.getSwapIx( + { + lpPoolId: lpPoolId, + amountIn: amountIn, + inMarketIndex: 0, + outMarketIndex: 2, + inTokenAccount: userUSDCAccount.publicKey, + outTokenAccount: adminSolAccount, + }, + true + ); + + const serumBidIx = serumMarket.makePlaceOrderInstruction( + bankrunContextWrapper.connection.toConnection(), + { + owner: adminClient.wallet.publicKey, + payer: userUSDCAccount.publicKey, + side: 'buy', + price: 100, + size: 2, // larger than maker orders so that entire maker order is taken + orderType: 'ioc', + clientId: new BN(1), // todo? + openOrdersAddressKey: openOrdersAccount, + feeDiscountPubkey: null, + selfTradeBehavior: 'abortTransaction', + } + ); + + const serumConfig = await adminClient.getSerumV3FulfillmentConfig( + serumMarket.publicKey + ); + const settleFundsIx = DexInstructions.settleFunds({ + market: serumMarket.publicKey, + openOrders: openOrdersAccount, + owner: adminClient.wallet.publicKey, + // @ts-ignore + baseVault: serumConfig.serumBaseVault, + // @ts-ignore + quoteVault: serumConfig.serumQuoteVault, + baseWallet: adminSolAccount, + quoteWallet: userUSDCAccount.publicKey, + vaultSigner: getSerumSignerPublicKey( + serumMarket.programId, + serumMarket.publicKey, + serumConfig.serumSignerNonce + ), + programId: serumMarket.programId, + }); + + const tx = new Transaction() + .add(beginSwapIx) + .add(serumBidIx) + .add(settleFundsIx) + .add(endSwapIx); + + // Should fail if usdc is in reduce only + const c0pubkey = getConstituentPublicKey(program.programId, lpPoolKey, 0); + await adminClient.updateConstituentStatus( + c0pubkey, + ConstituentStatus.REDUCE_ONLY + ); + try { + await adminClient.sendTransaction(tx); + } catch (e) { + assert(e.message.includes('0x18c0')); + } + await adminClient.updateConstituentStatus( + c0pubkey, + ConstituentStatus.ACTIVE + ); + + const { txSig } = await adminClient.sendTransaction(tx); + + bankrunContextWrapper.printTxLogs(txSig); + + // Balances should be accuarate after swap + const afterSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const afterUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + const solDiff = afterSOLBalance - beforeSOLBalance; + const usdcDiff = afterUSDCBalance - beforeUSDCBalance; + + console.log( + `in Token: ${beforeUSDCBalance} -> ${afterUSDCBalance} (${usdcDiff})` + ); + console.log( + `out Token: ${beforeSOLBalance} -> ${afterSOLBalance} (${solDiff})` + ); + + expect(usdcDiff).to.be.equal(-100040000); + expect(solDiff).to.be.equal(1000000000); + }); + + it('deposit and withdraw atomically before swapping', async () => { + const beforeSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const beforeUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + await adminClient.depositWithdrawToProgramVault( + lpPoolId, + 0, + 2, + new BN(400).mul(QUOTE_PRECISION), // 100 USDC + new BN(2 * 10 ** 9) // 100 USDC + ); + + const afterSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const afterUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + const solDiff = afterSOLBalance - beforeSOLBalance; + const usdcDiff = afterUSDCBalance - beforeUSDCBalance; + + console.log( + `in Token: ${beforeUSDCBalance} -> ${afterUSDCBalance} (${usdcDiff})` + ); + console.log( + `out Token: ${beforeSOLBalance} -> ${afterSOLBalance} (${solDiff})` + ); + + expect(usdcDiff).to.be.equal(-400000000); + expect(solDiff).to.be.equal(2000000000); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + + assert(constituent.spotBalance.scaledBalance.eq(new BN(2000000001))); + assert(isVariant(constituent.spotBalance.balanceType, 'borrow')); + }); +}); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index 8df74e2a33..b4a1ca83a6 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -44,7 +44,9 @@ import { DriftClient, OrderType, ReferrerInfo, -} from '../sdk'; + ConstituentAccount, + SpotMarketAccount, +} from '../sdk/src'; import { TestClient, SPOT_MARKET_RATE_PRECISION, @@ -1208,6 +1210,23 @@ export async function overWritePerpMarket( }); } +export async function overWriteSpotMarket( + driftClient: TestClient, + bankrunContextWrapper: BankrunContextWrapper, + spotMarketKey: PublicKey, + spotMarket: SpotMarketAccount +) { + bankrunContextWrapper.context.setAccount(spotMarketKey, { + executable: false, + owner: driftClient.program.programId, + lamports: LAMPORTS_PER_SOL, + data: await driftClient.program.account.spotMarket.coder.accounts.encode( + 'SpotMarket', + spotMarket + ), + }); +} + export async function getPerpMarketDecoded( driftClient: TestClient, bankrunContextWrapper: BankrunContextWrapper, @@ -1362,3 +1381,29 @@ export async function placeAndFillVammTrade({ console.error(e); } } + +export async function overwriteConstituentAccount( + bankrunContextWrapper: BankrunContextWrapper, + program: Program, + constituentPublicKey: PublicKey, + overwriteFields: Array<[key: keyof ConstituentAccount, value: any]> +) { + const acc = await program.account.constituent.fetch(constituentPublicKey); + if (!acc) { + throw new Error( + `Constituent account ${constituentPublicKey.toBase58()} not found` + ); + } + for (const [key, value] of overwriteFields) { + acc[key] = value; + } + bankrunContextWrapper.context.setAccount(constituentPublicKey, { + executable: false, + owner: program.programId, + lamports: LAMPORTS_PER_SOL, + data: await program.account.constituent.coder.accounts.encode( + 'Constituent', + acc + ), + }); +} From 56900cdf0e5c3a938aa4550222d6114ac057d612 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:10:28 +0000 Subject: [PATCH 211/247] sdk: release v2.145.0-beta.3 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 782892d821..cca9f72e35 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.145.0-beta.2 \ No newline at end of file +2.145.0-beta.3 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 3b79f17ffe..5ff71620ee 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.145.0-beta.2", + "version": "2.145.0-beta.3", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From c768aaa2aa124b0e2167dacab1560e42ce8d2878 Mon Sep 17 00:00:00 2001 From: wphan Date: Tue, 28 Oct 2025 22:24:46 -0700 Subject: [PATCH 212/247] v2.145.0 --- CHANGELOG.md | 10 ++++++++++ Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d540031c7..68fb1376ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking +## [2.145.0] - 2025-10-28 + +### Features + +- dlp ([#1885](https://github.com/drift-labs/protocol-v2/pull/1885)) + +### Fixes + +### Breaking + ## [2.144.0] - 2025-10-27 ### Features diff --git a/Cargo.lock b/Cargo.lock index c48d6a323b..910debab7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.144.0" +version = "2.145.0" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index e1f35eed93..762cfdd509 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.144.0" +version = "2.145.0" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index 5ff71620ee..9234fe1fa7 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.145.0-beta.3", + "version": "2.145.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 1d61c52f13..bfdb4ad5d2 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.144.0", + "version": "2.145.0", "name": "drift", "instructions": [ { From 691ce8f6ffbc6dedc0be1c473ce66eedb99c3f72 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 05:32:41 +0000 Subject: [PATCH 213/247] sdk: release v2.146.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index cca9f72e35..c3792566e1 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.145.0-beta.3 \ No newline at end of file +2.146.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 9234fe1fa7..8b90a0bea0 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.145.0", + "version": "2.146.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From d40470ed112cd9a8445595d763959c61a714d458 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 29 Oct 2025 08:58:18 -0600 Subject: [PATCH 214/247] feat: decoding user isolated position fields (#1996) * feat: decoding user isolated position fields * fix: formatting --- sdk/src/decode/user.ts | 7 +++++++ sdk/src/types.ts | 2 ++ sdk/src/user.ts | 2 ++ 3 files changed, 11 insertions(+) diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index e0d852f6e8..d6a53358a5 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -84,6 +84,11 @@ export function decodeUser(buffer: Buffer): UserAccount { const quoteAssetAmount = readSignedBigInt64LE(buffer, offset + 16); const lpShares = readUnsignedBigInt64LE(buffer, offset + 64); const openOrders = buffer.readUInt8(offset + 94); + const positionFlag = buffer.readUInt8(offset + 95); + const isolatedPositionScaledBalance = readUnsignedBigInt64LE( + buffer, + offset + 96 + ); if ( baseAssetAmount.eq(ZERO) && @@ -135,6 +140,8 @@ export function decodeUser(buffer: Buffer): UserAccount { openOrders, perLpBase, maxMarginRatio, + isolatedPositionScaledBalance, + positionFlag, }); } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index e2d7302d23..bb15805693 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1136,6 +1136,8 @@ export type PerpPosition = { lastBaseAssetAmountPerLp: BN; lastQuoteAssetAmountPerLp: BN; perLpBase: number; + isolatedPositionScaledBalance: BN; + positionFlag: number; }; export type UserStatsAccount = { diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 1c3e0730f1..7d5d78e256 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -338,6 +338,8 @@ export class User { lastQuoteAssetAmountPerLp: ZERO, perLpBase: 0, maxMarginRatio: 0, + isolatedPositionScaledBalance: ZERO, + positionFlag: 0, }; } From c1e5e5c1e1b7798471f628c54df7bc9118560fb4 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:03:43 +0000 Subject: [PATCH 215/247] sdk: release v2.146.0-beta.1 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index c3792566e1..a92d0991f4 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.0 \ No newline at end of file +2.146.0-beta.1 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 8b90a0bea0..c35a3940c5 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.0", + "version": "2.146.0-beta.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From abca5288737ddb7e4363636181e7398ab546b191 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 29 Oct 2025 10:44:17 -0600 Subject: [PATCH 216/247] fix: re-add accidentally yeeted ws v2 susbcribers (#1997) * fix: re-add accidentally yeeted ws v2 susbcribers * fix: formatting --- .../webSocketProgramAccountSubscriberV2.ts | 733 ++++++++++++++---- sdk/src/driftClient.ts | 11 +- sdk/src/driftClientConfig.ts | 23 +- sdk/src/index.ts | 4 + 4 files changed, 592 insertions(+), 179 deletions(-) diff --git a/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts index 1176b86589..31a68ce02e 100644 --- a/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts +++ b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts @@ -7,17 +7,96 @@ import { AccountInfoWithBase64EncodedData, createSolanaClient, isAddress, - type Address, - type Commitment as GillCommitment, + Lamports, + Slot, + Address, + Commitment as GillCommitment, } from 'gill'; import bs58 from 'bs58'; -export class WebSocketProgramAccountSubscriberV2 +type ProgramAccountSubscriptionAsyncIterable = AsyncIterable< + Readonly<{ + context: Readonly<{ + slot: Slot; + }>; + value: Readonly<{ + account: Readonly<{ + executable: boolean; + lamports: Lamports; + owner: Address; + rentEpoch: bigint; + space: bigint; + }> & + Readonly; + pubkey: Address; + }>; + }> +>; +/** + * WebSocketProgramAccountsSubscriberV2 + * + * High-level overview + * - WebSocket-first subscriber for Solana program accounts that also layers in + * targeted polling to detect missed updates reliably. + * - Emits decoded account updates via the provided `onChange` callback. + * - Designed to focus extra work on the specific accounts the consumer cares + * about ("monitored accounts") while keeping baseline WS behavior for the + * full program subscription. + * + * Why polling if this is a WebSocket subscriber? + * - WS infra can stall, drop, or reorder notifications under network stress or + * provider hiccups. When that happens, critical account changes can be missed. + * - To mitigate this, the class accepts a set of accounts (provided via constructor) to monitor + * and uses light polling to verify whether a WS change was missed. + * - If polling detects a newer slot with different data than the last seen + * buffer, a centralized resubscription is triggered to restore a clean stream. + * + * Initial fetch (on subscribe) + * - On `subscribe()`, we first perform a single batched fetch of all monitored + * accounts ("initial monitor fetch"). + * - Purpose: seed the internal `bufferAndSlotMap` and emit the latest state so + * consumers have up-to-date data immediately, even before WS events arrive. + * - This step does not decide resubscription; it only establishes ground truth. + * + * Continuous polling (only for monitored accounts) + * - After seeding, each monitored account is put into a monitoring cycle: + * 1) If no WS notification for an account is observed for `pollingIntervalMs`, + * we enqueue it for a batched fetch (buffered for a short window). + * 2) Once an account enters the "currently polling" set, a shared batch poll + * runs every `pollingIntervalMs` across all such accounts. + * 3) If WS notifications resume for an account, that account is removed from + * the polling set and returns to passive monitoring. + * - Polling compares the newly fetched buffer with the last stored buffer at a + * later slot. A difference indicates a missed update; we schedule a single + * resubscription (coalesced across accounts) to re-sync. + * + * Accounts the consumer cares about + * - Provide accounts up-front via the constructor `accountsToMonitor`, or add + * them dynamically with `addAccountToMonitor()` and remove with + * `removeAccountFromMonitor()`. + * - Only these accounts incur additional polling safeguards; other accounts are + * still processed from the WS stream normally. + * + * Resubscription strategy + * - Missed updates from any monitored account are coalesced and trigger a single + * resubscription after a short delay. This avoids rapid churn. + * - If `resubOpts.resubTimeoutMs` is set, an inactivity timer also performs a + * batch check of monitored accounts. If a missed update is found, the same + * centralized resubscription flow is used. + * + * Tuning knobs + * - `setPollingInterval(ms)`: adjust how often monitoring/polling runs + * (default 30s). Shorter = faster detection, higher RPC load. + * - Debounced immediate poll (~100ms): batches accounts added to polling right after inactivity. + * - Batch size for `getMultipleAccounts` is limited to 100, requests are chunked + * and processed concurrently. + */ + +export class WebSocketProgramAccountsSubscriberV2 implements ProgramAccountSubscriber { subscriptionName: string; accountDiscriminator: string; - bufferAndSlot?: BufferAndSlot; bufferAndSlotMap: Map = new Map(); program: Program; decodeBuffer: (accountName: string, ix: Buffer) => T; @@ -28,7 +107,7 @@ export class WebSocketProgramAccountSubscriberV2 buffer: Buffer ) => void; listenerId?: number; - resubOpts?: ResubOpts; + resubOpts: ResubOpts; isUnsubscribing = false; timeoutId?: ReturnType; options: { filters: MemcmpFilter[]; commitment?: Commitment }; @@ -51,6 +130,15 @@ export class WebSocketProgramAccountSubscriberV2 private accountsCurrentlyPolling: Set = new Set(); // Track which accounts are being polled private batchPollingTimeout?: ReturnType; // Single timeout for batch polling + // Debounced immediate poll to batch multiple additions within a short window + private debouncedImmediatePollTimeout?: ReturnType; + private debouncedImmediatePollMs: number = 100; // configurable short window + + // Centralized resubscription handling + private missedChangeDetected = false; // Flag to track if any missed change was detected + private resubscriptionTimeout?: ReturnType; // Timeout for delayed resubscription + private accountsWithMissedUpdates: Set = new Set(); // Track which accounts had missed updates + public constructor( subscriptionName: string, accountDiscriminator: string, @@ -66,7 +154,11 @@ export class WebSocketProgramAccountSubscriberV2 this.accountDiscriminator = accountDiscriminator; this.program = program; this.decodeBuffer = decodeBufferFn; - this.resubOpts = resubOpts; + this.resubOpts = resubOpts ?? { + resubTimeoutMs: 30000, + usePollingInsteadOfResub: true, + logResubMessages: false, + }; if (this.resubOpts?.resubTimeoutMs < 1000) { console.log( 'resubTimeoutMs should be at least 1000ms to avoid spamming resub' @@ -92,6 +184,44 @@ export class WebSocketProgramAccountSubscriberV2 this.rpcSubscriptions = rpcSubscriptions; } + private async handleNotificationLoop( + notificationPromise: Promise + ) { + try { + const subscriptionIterable = await notificationPromise; + for await (const notification of subscriptionIterable) { + try { + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + this.handleRpcResponse( + notification.context, + notification.value.pubkey, + notification.value.account.data + ); + this.setTimeout(); + } else { + this.handleRpcResponse( + notification.context, + notification.value.pubkey, + notification.value.account.data + ); + } + } catch (error) { + console.error( + `Error handling RPC response for pubkey ${notification.value.pubkey}:`, + error + ); + } + } + } catch (error) { + console.error( + `[${this.subscriptionName}] Error in notification loop:`, + error + ); + } + } + async subscribe( onChange: ( accountId: PublicKey, @@ -100,62 +230,91 @@ export class WebSocketProgramAccountSubscriberV2 buffer: Buffer ) => void ): Promise { + /** + * Start the WebSocket subscription and initialize polling safeguards. + * + * Flow + * - Seeds all monitored accounts with a single batched RPC fetch and emits + * their current state. + * - Subscribes to program notifications via WS using gill. + * - If `resubOpts.resubTimeoutMs` is set, starts an inactivity timer that + * batch-checks monitored accounts when WS goes quiet. + * - Begins monitoring for accounts that may need polling when WS + * notifications are not observed within `pollingIntervalMs`. + * + * @param onChange Callback invoked with decoded account data when an update + * is detected (via WS or batch RPC fetch). + */ + const startTime = performance.now(); if (this.listenerId != null || this.isUnsubscribing) { return; } + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] initializing subscription. This many monitored accounts: ${this.accountsToMonitor.size}` + ); + } + this.onChange = onChange; + // initial fetch of monitored data - only fetch and populate, don't check for missed changes + await this.fetchAndPopulateAllMonitoredAccounts(); + // Create abort controller for proper cleanup const abortController = new AbortController(); this.abortController = abortController; + this.listenerId = Math.random(); // Unique ID for logging purposes + + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + this.setTimeout(); + } + // Subscribe to program account changes using gill's rpcSubscriptions const programId = this.program.programId.toBase58(); if (isAddress(programId)) { - const subscription = await this.rpcSubscriptions + const subscriptionPromise = this.rpcSubscriptions .programNotifications(programId, { commitment: this.options.commitment as GillCommitment, encoding: 'base64', - filters: this.options.filters.map((filter) => ({ - memcmp: { - offset: BigInt(filter.memcmp.offset), - bytes: filter.memcmp.bytes as any, - encoding: 'base64' as const, - }, - })), + filters: this.options.filters.map((filter) => { + // Convert filter bytes from base58 to base64 if needed + let bytes = filter.memcmp.bytes; + if ( + typeof bytes === 'string' && + /^[1-9A-HJ-NP-Za-km-z]+$/.test(bytes) + ) { + // Looks like base58 - convert to base64 + const decoded = bs58.decode(bytes); + bytes = Buffer.from(decoded).toString('base64'); + } + + return { + memcmp: { + offset: BigInt(filter.memcmp.offset), + bytes: bytes as any, + encoding: 'base64' as const, + }, + }; + }), }) .subscribe({ abortSignal: abortController.signal, }); - for await (const notification of subscription) { - if (this.resubOpts?.resubTimeoutMs) { - this.receivingData = true; - clearTimeout(this.timeoutId); - this.handleRpcResponse( - notification.context, - notification.value.account - ); - this.setTimeout(); - } else { - this.handleRpcResponse( - notification.context, - notification.value.account - ); - } - } - } - - this.listenerId = Math.random(); // Unique ID for logging purposes - - if (this.resubOpts?.resubTimeoutMs) { - this.receivingData = true; - this.setTimeout(); + // Start notification loop without awaiting + this.handleNotificationLoop(subscriptionPromise); + // Start monitoring for accounts that may need polling if no WS event is received + this.startMonitoringForAccounts(); } - - // Start monitoring for accounts that may need polling if no WS event is received - this.startMonitoringForAccounts(); + const endTime = performance.now(); + console.log( + `[PROFILING] ${this.subscriptionName}.subscribe() completed in ${ + endTime - startTime + }ms` + ); } protected setTimeout(): void { @@ -172,12 +331,21 @@ export class WebSocketProgramAccountSubscriberV2 if (this.receivingData) { if (this.resubOpts?.logResubMessages) { console.log( - `No ws data from ${this.subscriptionName} in ${this.resubOpts?.resubTimeoutMs}ms, resubscribing` + `No ws data from ${this.subscriptionName} in ${this.resubOpts?.resubTimeoutMs}ms, checking for missed changes` ); } - await this.unsubscribe(true); - this.receivingData = false; - await this.subscribe(this.onChange); + + // Check for missed changes in monitored accounts + const missedChangeDetected = await this.fetchAllMonitoredAccounts(); + + if (missedChangeDetected) { + // Signal missed change with a generic identifier since we don't have specific account IDs from this context + this.signalMissedChange('timeout-check'); + } else { + // No missed changes, continue monitoring + this.receivingData = false; + this.setTimeout(); + } } }, this.resubOpts?.resubTimeoutMs @@ -186,43 +354,42 @@ export class WebSocketProgramAccountSubscriberV2 handleRpcResponse( context: { slot: bigint }, + accountId: Address, accountInfo?: AccountInfoBase & - (AccountInfoWithBase58EncodedData | AccountInfoWithBase64EncodedData) + ( + | AccountInfoWithBase58EncodedData + | AccountInfoWithBase64EncodedData + )['data'] ): void { const newSlot = Number(context.slot); let newBuffer: Buffer | undefined = undefined; if (accountInfo) { - // Extract data from gill response - if (accountInfo.data) { - // Handle different data formats from gill - if (Array.isArray(accountInfo.data)) { - // If it's a tuple [data, encoding] - const [data, encoding] = accountInfo.data; - - if (encoding === ('base58' as any)) { - // Convert base58 to buffer using bs58 - newBuffer = Buffer.from(bs58.decode(data)); - } else { - newBuffer = Buffer.from(data, 'base64'); - } + // Handle different data formats from gill + if (Array.isArray(accountInfo)) { + // If it's a tuple [data, encoding] + const [data, encoding] = accountInfo; + + if (encoding === ('base58' as any)) { + // Convert base58 to buffer using bs58 + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); } } } - // Convert gill's account key to PublicKey - // Note: accountInfo doesn't have a key property, we need to get it from the notification - // For now, we'll use a placeholder - this needs to be fixed based on the actual gill API - const accountId = new PublicKey('11111111111111111111111111111111'); // Placeholder - const accountIdString = accountId.toBase58(); - + const accountIdString = accountId.toString(); const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); // Track WebSocket notification time for this account this.lastWsNotificationTime.set(accountIdString, Date.now()); - // If this account was being polled, stop polling it - if (this.accountsCurrentlyPolling.has(accountIdString)) { + // If this account was being polled, stop polling it if the buffer has changed + if ( + this.accountsCurrentlyPolling.has(accountIdString) && + !existingBufferAndSlot?.buffer.equals(newBuffer) + ) { this.accountsCurrentlyPolling.delete(accountIdString); // If no more accounts are being polled, stop batch polling @@ -237,12 +404,7 @@ export class WebSocketProgramAccountSubscriberV2 if (!existingBufferAndSlot) { if (newBuffer) { - this.bufferAndSlotMap.set(accountIdString, { - buffer: newBuffer, - slot: newSlot, - }); - const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); - this.onChange(accountId, account, { slot: newSlot }, newBuffer); + this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString); } return; } @@ -253,12 +415,7 @@ export class WebSocketProgramAccountSubscriberV2 const oldBuffer = existingBufferAndSlot.buffer; if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) { - this.bufferAndSlotMap.set(accountIdString, { - buffer: newBuffer, - slot: newSlot, - }); - const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); - this.onChange(accountId, account, { slot: newSlot }, newBuffer); + this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString); } } @@ -283,17 +440,21 @@ export class WebSocketProgramAccountSubscriberV2 const timeoutId = setTimeout(async () => { // Check if we've received a WS notification for this account recently const lastNotificationTime = - this.lastWsNotificationTime.get(accountIdString); + this.lastWsNotificationTime.get(accountIdString) || 0; const currentTime = Date.now(); if ( !lastNotificationTime || currentTime - lastNotificationTime >= this.pollingIntervalMs ) { - // No recent WS notification, start polling - await this.pollAccount(accountIdString); - // Schedule next poll - this.startPollingForAccount(accountIdString); + if (this.resubOpts?.logResubMessages) { + console.debug( + `[${this.subscriptionName}] No recent WS notification for ${accountIdString}, adding to polling set` + ); + } + // No recent WS notification: add to polling and schedule debounced poll + this.accountsCurrentlyPolling.add(accountIdString); + this.scheduleDebouncedImmediatePoll(); } else { // We received a WS notification recently, continue monitoring this.startMonitoringForAccount(accountIdString); @@ -303,17 +464,35 @@ export class WebSocketProgramAccountSubscriberV2 this.pollingTimeouts.set(accountIdString, timeoutId); } - private startPollingForAccount(accountIdString: string): void { - // Add account to polling set - this.accountsCurrentlyPolling.add(accountIdString); - - // If this is the first account being polled, start batch polling - if (this.accountsCurrentlyPolling.size === 1) { - this.startBatchPolling(); + private scheduleDebouncedImmediatePoll(): void { + if (this.debouncedImmediatePollTimeout) { + clearTimeout(this.debouncedImmediatePollTimeout); } + this.debouncedImmediatePollTimeout = setTimeout(async () => { + try { + await this.pollAllAccounts(); + // After the immediate poll, ensure continuous batch polling is active + if ( + !this.batchPollingTimeout && + this.accountsCurrentlyPolling.size > 0 + ) { + this.startBatchPolling(); + } + } catch (e) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error during debounced immediate poll:`, + e + ); + } + } + }, this.debouncedImmediatePollMs); } private startBatchPolling(): void { + if (this.resubOpts?.logResubMessages) { + console.debug(`[${this.subscriptionName}] Scheduling batch polling`); + } // Clear existing batch polling timeout if (this.batchPollingTimeout) { clearTimeout(this.batchPollingTimeout); @@ -335,8 +514,40 @@ export class WebSocketProgramAccountSubscriberV2 return; } + if (this.resubOpts?.logResubMessages) { + console.debug( + `[${this.subscriptionName}] Polling all accounts`, + accountsToPoll.length, + 'accounts' + ); + } + + // Use the shared batch fetch method + await this.fetchAccountsBatch(accountsToPoll); + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error batch polling accounts:`, + error + ); + } + } + } + + /** + * Fetches and populates all monitored accounts data without checking for missed changes + * This is used during initial subscription to populate data + */ + private async fetchAndPopulateAllMonitoredAccounts(): Promise { + try { + // Get all accounts currently being polled + const accountsToMonitor = Array.from(this.accountsToMonitor); + if (accountsToMonitor.length === 0) { + return; + } + // Fetch all accounts in a single batch request - const accountAddresses = accountsToPoll.map( + const accountAddresses = accountsToMonitor.map( (accountId) => accountId as Address ); const rpcResponse = await this.rpc @@ -349,8 +560,8 @@ export class WebSocketProgramAccountSubscriberV2 const currentSlot = Number(rpcResponse.context.slot); // Process each account response - for (let i = 0; i < accountsToPoll.length; i++) { - const accountIdString = accountsToPoll[i]; + for (let i = 0; i < accountsToMonitor.length; i++) { + const accountIdString = accountsToMonitor[i]; const accountInfo = rpcResponse.value[i]; if (!accountInfo) { @@ -363,7 +574,7 @@ export class WebSocketProgramAccountSubscriberV2 if (!existingBufferAndSlot) { // Account not in our map yet, add it let newBuffer: Buffer | undefined = undefined; - if (accountInfo.data) { + if (accountInfo) { if (Array.isArray(accountInfo.data)) { const [data, encoding] = accountInfo.data; newBuffer = Buffer.from(data, encoding); @@ -371,21 +582,16 @@ export class WebSocketProgramAccountSubscriberV2 } if (newBuffer) { - this.bufferAndSlotMap.set(accountIdString, { - buffer: newBuffer, - slot: currentSlot, - }); - const account = this.decodeBuffer( - this.accountDiscriminator, - newBuffer + this.updateBufferAndHandleChange( + newBuffer, + currentSlot, + accountIdString ); - const accountId = new PublicKey(accountIdString); - this.onChange(accountId, account, { slot: currentSlot }, newBuffer); } continue; } - // Check if we missed an update + // For initial population, just update the slot if we have newer data if (currentSlot > existingBufferAndSlot.slot) { let newBuffer: Buffer | undefined = undefined; if (accountInfo.data) { @@ -399,83 +605,89 @@ export class WebSocketProgramAccountSubscriberV2 } } - // Check if buffer has changed - if ( - newBuffer && - (!existingBufferAndSlot.buffer || - !newBuffer.equals(existingBufferAndSlot.buffer)) - ) { - if (this.resubOpts?.logResubMessages) { - console.log( - `[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing` - ); - } - // We missed an update, resubscribe - await this.unsubscribe(true); - this.receivingData = false; - await this.subscribe(this.onChange); - return; + // Update with newer data if available + if (newBuffer) { + this.updateBufferAndHandleChange( + newBuffer, + currentSlot, + accountIdString + ); } } } } catch (error) { if (this.resubOpts?.logResubMessages) { console.log( - `[${this.subscriptionName}] Error batch polling accounts:`, + `[${this.subscriptionName}] Error fetching and populating monitored accounts:`, error ); } } } - private async pollAccount(accountIdString: string): Promise { + /** + * Fetches all monitored accounts and checks for missed changes + * Returns true if a missed change was detected and resubscription is needed + */ + private async fetchAllMonitoredAccounts(): Promise { try { - // Fetch current account data using gill's rpc - const accountAddress = accountIdString as Address; + // Get all accounts currently being polled + const accountsToMonitor = Array.from(this.accountsToMonitor); + if (accountsToMonitor.length === 0) { + return false; + } + + // Fetch all accounts in a single batch request + const accountAddresses = accountsToMonitor.map( + (accountId) => accountId as Address + ); const rpcResponse = await this.rpc - .getAccountInfo(accountAddress, { + .getMultipleAccounts(accountAddresses, { commitment: this.options.commitment as GillCommitment, encoding: 'base64', }) .send(); const currentSlot = Number(rpcResponse.context.slot); - const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); - if (!existingBufferAndSlot) { - // Account not in our map yet, add it - if (rpcResponse.value) { + // Process each account response + for (let i = 0; i < accountsToMonitor.length; i++) { + const accountIdString = accountsToMonitor[i]; + const accountInfo = rpcResponse.value[i]; + + if (!accountInfo) { + continue; + } + + const existingBufferAndSlot = + this.bufferAndSlotMap.get(accountIdString); + + if (!existingBufferAndSlot) { + // Account not in our map yet, add it let newBuffer: Buffer | undefined = undefined; - if (rpcResponse.value.data) { - if (Array.isArray(rpcResponse.value.data)) { - const [data, encoding] = rpcResponse.value.data; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; newBuffer = Buffer.from(data, encoding); } } if (newBuffer) { - this.bufferAndSlotMap.set(accountIdString, { - buffer: newBuffer, - slot: currentSlot, - }); - const account = this.decodeBuffer( - this.accountDiscriminator, - newBuffer + this.updateBufferAndHandleChange( + newBuffer, + currentSlot, + accountIdString ); - const accountId = new PublicKey(accountIdString); - this.onChange(accountId, account, { slot: currentSlot }, newBuffer); } + continue; } - return; - } - // Check if we missed an update - if (currentSlot > existingBufferAndSlot.slot) { - let newBuffer: Buffer | undefined = undefined; - if (rpcResponse.value) { - if (rpcResponse.value.data) { - if (Array.isArray(rpcResponse.value.data)) { - const [data, encoding] = rpcResponse.value.data; + // Check if we missed an update + if (currentSlot > existingBufferAndSlot.slot) { + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; if (encoding === ('base58' as any)) { newBuffer = Buffer.from(bs58.decode(data)); } else { @@ -483,30 +695,130 @@ export class WebSocketProgramAccountSubscriberV2 } } } - } - // Check if buffer has changed - if ( - newBuffer && - (!existingBufferAndSlot.buffer || - !newBuffer.equals(existingBufferAndSlot.buffer)) - ) { - if (this.resubOpts?.logResubMessages) { - console.log( - `[${this.subscriptionName}] Polling detected missed update for account ${accountIdString}, resubscribing` - ); + // Check if buffer has changed + if ( + newBuffer && + (!existingBufferAndSlot.buffer || + !newBuffer.equals(existingBufferAndSlot.buffer)) + ) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing` + ); + } + // We missed an update, return true to indicate resubscription is needed + return true; } - // We missed an update, resubscribe - await this.unsubscribe(true); - this.receivingData = false; - await this.subscribe(this.onChange); - return; } } + + // No missed changes detected + return false; + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error batch polling accounts:`, + error + ); + } + return false; + } + } + + private async fetchAccountsBatch(accountIds: string[]): Promise { + try { + // Chunk account IDs into groups of 100 (getMultipleAccounts limit) + const chunkSize = 100; + const chunks: string[][] = []; + for (let i = 0; i < accountIds.length; i += chunkSize) { + chunks.push(accountIds.slice(i, i + chunkSize)); + } + + // Process all chunks concurrently + await Promise.all( + chunks.map(async (chunk) => { + const accountAddresses = chunk.map( + (accountId) => accountId as Address + ); + const rpcResponse = await this.rpc + .getMultipleAccounts(accountAddresses, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + }) + .send(); + + const currentSlot = Number(rpcResponse.context.slot); + + // Process each account response in this chunk + for (let i = 0; i < chunk.length; i++) { + const accountIdString = chunk[i]; + const accountInfo = rpcResponse.value[i]; + + if (!accountInfo) { + continue; + } + + const existingBufferAndSlot = + this.bufferAndSlotMap.get(accountIdString); + + if (!existingBufferAndSlot) { + // Account not in our map yet, add it + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + newBuffer = Buffer.from(data, encoding); + } + } + + if (newBuffer) { + this.updateBufferAndHandleChange( + newBuffer, + currentSlot, + accountIdString + ); + } + continue; + } + + // Check if we missed an update + if (currentSlot > existingBufferAndSlot.slot) { + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + if (encoding === ('base58' as any)) { + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + + // Check if buffer has changed + if ( + newBuffer && + (!existingBufferAndSlot.buffer || + !newBuffer.equals(existingBufferAndSlot.buffer)) + ) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, signaling resubscription` + ); + } + // Signal missed change instead of immediately resubscribing + this.signalMissedChange(accountIdString); + return; + } + } + } + }) + ); } catch (error) { if (this.resubOpts?.logResubMessages) { console.log( - `[${this.subscriptionName}] Error polling account ${accountIdString}:`, + `[${this.subscriptionName}] Error fetching accounts batch:`, error ); } @@ -525,8 +837,72 @@ export class WebSocketProgramAccountSubscriberV2 this.batchPollingTimeout = undefined; } + // Clear initial fetch timeout + // if (this.initialFetchTimeout) { + // clearTimeout(this.initialFetchTimeout); + // this.initialFetchTimeout = undefined; + // } + + // Clear resubscription timeout + if (this.resubscriptionTimeout) { + clearTimeout(this.resubscriptionTimeout); + this.resubscriptionTimeout = undefined; + } + // Clear accounts currently polling this.accountsCurrentlyPolling.clear(); + + // Clear accounts pending initial monitor fetch + // this.accountsPendingInitialMonitorFetch.clear(); + + // Reset missed change flag and clear accounts with missed updates + this.missedChangeDetected = false; + this.accountsWithMissedUpdates.clear(); + } + + /** + * Centralized resubscription handler that only resubscribes once after checking all accounts + */ + private async handleResubscription(): Promise { + if (this.missedChangeDetected) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Missed change detected for ${ + this.accountsWithMissedUpdates.size + } accounts: ${Array.from(this.accountsWithMissedUpdates).join( + ', ' + )}, resubscribing` + ); + } + await this.unsubscribe(true); + this.receivingData = false; + await this.subscribe(this.onChange); + this.missedChangeDetected = false; + this.accountsWithMissedUpdates.clear(); + } + } + + /** + * Signal that a missed change was detected and schedule resubscription + */ + private signalMissedChange(accountIdString: string): void { + if (!this.missedChangeDetected) { + this.missedChangeDetected = true; + this.accountsWithMissedUpdates.add(accountIdString); + + // Clear any existing resubscription timeout + if (this.resubscriptionTimeout) { + clearTimeout(this.resubscriptionTimeout); + } + + // Schedule resubscription after a short delay to allow for batch processing + this.resubscriptionTimeout = setTimeout(async () => { + await this.handleResubscription(); + }, 100); // 100ms delay to allow for batch processing + } else { + // If already detected, just add the account to the set + this.accountsWithMissedUpdates.add(accountIdString); + } } unsubscribe(onResub = false): Promise { @@ -553,6 +929,11 @@ export class WebSocketProgramAccountSubscriberV2 } // Method to add accounts to the polling list + /** + * Add an account to the monitored set. + * - Monitored accounts are subject to initial fetch and periodic batch polls + * if WS notifications are not observed within `pollingIntervalMs`. + */ addAccountToMonitor(accountId: PublicKey): void { const accountIdString = accountId.toBase58(); this.accountsToMonitor.add(accountIdString); @@ -586,6 +967,10 @@ export class WebSocketProgramAccountSubscriberV2 } // Method to set polling interval + /** + * Set the monitoring/polling interval for monitored accounts. + * Shorter intervals detect missed updates sooner but increase RPC load. + */ setPollingInterval(intervalMs: number): void { this.pollingIntervalMs = intervalMs; // Restart monitoring with new interval if already subscribed @@ -593,4 +978,18 @@ export class WebSocketProgramAccountSubscriberV2 this.startMonitoringForAccounts(); } } + + private updateBufferAndHandleChange( + newBuffer: Buffer, + newSlot: number, + accountIdString: string + ) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: newSlot, + }); + const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); + const accountIdPubkey = new PublicKey(accountIdString); + this.onChange(accountIdPubkey, account, { slot: newSlot }, newBuffer); + } } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 8c47c2ceb1..c5f4c9707a 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -406,6 +406,8 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, commitment: config.accountSubscription?.commitment, + programUserAccountSubscriber: + config.accountSubscription?.programUserAccountSubscriber, }; this.userStatsAccountSubscriptionConfig = { type: 'websocket', @@ -474,7 +476,10 @@ export class DriftClient { } ); } else { - this.accountSubscriber = new WebSocketDriftClientAccountSubscriber( + const accountSubscriberClass = + config.accountSubscription?.driftClientAccountSubscriber ?? + WebSocketDriftClientAccountSubscriber; + this.accountSubscriber = new accountSubscriberClass( this.program, config.perpMarketIndexes ?? [], config.spotMarketIndexes ?? [], @@ -485,9 +490,7 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, }, - config.accountSubscription?.commitment, - config.accountSubscription?.perpMarketAccountSubscriber, - config.accountSubscription?.oracleAccountSubscriber + config.accountSubscription?.commitment ); } this.eventEmitter = this.accountSubscriber.eventEmitter; diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index 0ddb554553..4ca3a8a9eb 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -5,7 +5,7 @@ import { PublicKey, TransactionVersion, } from '@solana/web3.js'; -import { IWallet, TxParams } from './types'; +import { IWallet, TxParams, UserAccount } from './types'; import { OracleInfo } from './oracles/types'; import { BulkAccountLoader } from './accounts/bulkAccountLoader'; import { DriftEnv } from './config'; @@ -22,6 +22,9 @@ import { WebSocketAccountSubscriberV2 } from './accounts/webSocketAccountSubscri import { grpcDriftClientAccountSubscriberV2 } from './accounts/grpcDriftClientAccountSubscriberV2'; import { grpcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; import { grpcMultiUserAccountSubscriber } from './accounts/grpcMultiUserAccountSubscriber'; +import { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; +import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; +import { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; export type DriftClientConfig = { connection: Connection; @@ -78,6 +81,7 @@ export type DriftClientSubscriptionConfig = resubTimeoutMs?: number; logResubMessages?: boolean; commitment?: Commitment; + programUserAccountSubscriber?: WebSocketProgramAccountSubscriber; perpMarketAccountSubscriber?: new ( accountName: string, program: Program, @@ -86,14 +90,17 @@ export type DriftClientSubscriptionConfig = resubOpts?: ResubOpts, commitment?: Commitment ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; - oracleAccountSubscriber?: new ( - accountName: string, + /** If you use V2 here, whatever you pass for perpMarketAccountSubscriber will be ignored and it will use v2 under the hood regardless */ + driftClientAccountSubscriber?: new ( program: Program, - accountPublicKey: PublicKey, - decodeBuffer?: (buffer: Buffer) => any, - resubOpts?: ResubOpts, - commitment?: Commitment - ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; + perpMarketIndexes: number[], + spotMarketIndexes: number[], + oracleInfos: OracleInfo[], + shouldFindAllMarketsAndOracles: boolean, + delistedMarketSetting: DelistedMarketSetting + ) => + | WebSocketDriftClientAccountSubscriber + | WebSocketDriftClientAccountSubscriberV2; } | { type: 'polling'; diff --git a/sdk/src/index.ts b/sdk/src/index.ts index b6f69eeb3a..6c73a0de63 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -12,6 +12,10 @@ export * from './accounts/webSocketDriftClientAccountSubscriber'; export * from './accounts/webSocketInsuranceFundStakeAccountSubscriber'; export * from './accounts/webSocketHighLeverageModeConfigAccountSubscriber'; export { WebSocketAccountSubscriberV2 } from './accounts/webSocketAccountSubscriberV2'; +export { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; +export { WebSocketProgramUserAccountSubscriber } from './accounts/websocketProgramUserAccountSubscriber'; +export { WebSocketProgramAccountsSubscriberV2 } from './accounts/webSocketProgramAccountsSubscriberV2'; +export { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; export * from './accounts/bulkAccountLoader'; export * from './accounts/bulkUserSubscription'; export * from './accounts/bulkUserStatsSubscription'; From fb3f9a0ce3dc7ba73b05196f0f44111766725841 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:49:44 +0000 Subject: [PATCH 217/247] sdk: release v2.146.0-beta.2 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index a92d0991f4..b77e5ca327 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.1 \ No newline at end of file +2.146.0-beta.2 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c35a3940c5..954270e98c 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.1", + "version": "2.146.0-beta.2", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 80de21fdd0bf286a7759188763b540e9f24893f8 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 29 Oct 2025 11:07:56 -0600 Subject: [PATCH 218/247] fix: post merge dupe field on swift --- sdk/src/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/src/types.ts b/sdk/src/types.ts index dff4f7d404..fd0d9368e5 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1352,7 +1352,6 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; - isolatedPositionDeposit?: BN | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; isolatedPositionDeposit?: BN | null; @@ -1366,7 +1365,6 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; - isolatedPositionDeposit?: BN | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; isolatedPositionDeposit?: BN | null; From d7bc3ee0fc3968348f9e377620407770ced91d4c Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:39:08 -0700 Subject: [PATCH 219/247] amm cache init refactor (#1998) --- programs/drift/src/instructions/admin.rs | 53 +++++++++++++++++++++--- programs/drift/src/state/amm_cache.rs | 8 +++- sdk/src/adminClient.ts | 43 ++++++++++++------- tests/lpPool.ts | 2 + tests/lpPoolCUs.ts | 2 + tests/lpPoolSwap.ts | 2 + 6 files changed, 89 insertions(+), 21 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 07b0913f48..747a8cb17e 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1135,15 +1135,37 @@ pub fn handle_initialize_perp_market( pub fn handle_initialize_amm_cache(ctx: Context) -> Result<()> { let amm_cache = &mut ctx.accounts.amm_cache; - let state = &ctx.accounts.state; - amm_cache - .cache - .resize_with(state.number_of_markets as usize, CacheInfo::default); amm_cache.bump = ctx.bumps.amm_cache; Ok(()) } +pub fn handle_resize_amm_cache(ctx: Context) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + let state = &ctx.accounts.state; + let current_size = amm_cache.cache.len(); + let new_size = (state.number_of_markets as usize).min(current_size + 20_usize); + + msg!( + "resizing amm cache from {} entries to {}", + current_size, + new_size + ); + + let growth = new_size.saturating_sub(current_size); + validate!( + growth <= 20, + ErrorCode::DefaultError, + "cannot grow amm_cache by more than 20 entries in a single resize (requested +{})", + growth + )?; + + amm_cache.cache.resize_with(new_size, CacheInfo::default); + amm_cache.validate(state)?; + + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -5510,7 +5532,7 @@ pub struct InitializeAmmCache<'info> { #[account( init, seeds = [AMM_POSITIONS_CACHE.as_ref()], - space = AmmCache::space(state.number_of_markets as usize), + space = AmmCache::init_space(), bump, payer = admin )] @@ -5519,6 +5541,27 @@ pub struct InitializeAmmCache<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct ResizeAmmCache<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump, + realloc = AmmCache::space(amm_cache.cache.len() + (state.number_of_markets as usize - amm_cache.cache.len()).min(20_usize)), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_cache: Box>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + #[derive(Accounts)] pub struct DeleteInitializedPerpMarket<'info> { #[account(mut)] diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs index 090213235c..59982ce15c 100644 --- a/programs/drift/src/state/amm_cache.rs +++ b/programs/drift/src/state/amm_cache.rs @@ -175,15 +175,19 @@ impl HasLen for AmmCacheFixed { } impl AmmCache { + pub fn init_space() -> usize { + 8 + 8 + 4 + } + pub fn space(num_markets: usize) -> usize { 8 + 8 + 4 + num_markets * CacheInfo::SIZE } pub fn validate(&self, state: &State) -> DriftResult<()> { validate!( - self.cache.len() == state.number_of_markets as usize, + self.cache.len() <= state.number_of_markets as usize, ErrorCode::DefaultError, - "Number of amm positions is different than number of markets" + "Number of amm positions is no larger than number of markets" )?; Ok(()) } diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index da4d9cc8a5..010870185f 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -507,12 +507,6 @@ export class AdminClient extends DriftClient { ): Promise { const currentPerpMarketIndex = this.getStateAccount().numberOfMarkets; - const ammCachePublicKey = getAmmCachePublicKey(this.program.programId); - const ammCacheAccount = await this.connection.getAccountInfo( - ammCachePublicKey - ); - const mustInitializeAmmCache = ammCacheAccount?.data == null; - const initializeMarketIxs = await this.getInitializePerpMarketIx( marketIndex, priceOracle, @@ -540,7 +534,6 @@ export class AdminClient extends DriftClient { curveUpdateIntensity, ammJitIntensity, name, - mustInitializeAmmCache, lpPoolId ); const tx = await this.buildTransaction(initializeMarketIxs); @@ -588,7 +581,6 @@ export class AdminClient extends DriftClient { curveUpdateIntensity = 0, ammJitIntensity = 0, name = DEFAULT_MARKET_NAME, - includeInitAmmCacheIx = false, lpPoolId: number = 0 ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( @@ -597,9 +589,6 @@ export class AdminClient extends DriftClient { ); const ixs: TransactionInstruction[] = []; - if (includeInitAmmCacheIx) { - ixs.push(await this.getInitializeAmmCacheIx()); - } const nameBuffer = encodeName(name); const initPerpIx = await this.program.instruction.initializePerpMarket( @@ -663,9 +652,35 @@ export class AdminClient extends DriftClient { return await this.program.instruction.initializeAmmCache({ accounts: { state: await this.getStatePublicKey(), - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + ammCache: getAmmCachePublicKey(this.program.programId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async resizeAmmCache( + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getInitializeAmmCacheIx(); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getResizeAmmCacheIx(): Promise { + return await this.program.instruction.resizeAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, ammCache: getAmmCachePublicKey(this.program.programId), rent: SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 91470291ed..aadfbaf9e7 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -191,6 +191,8 @@ describe('LP Pool', () => { solUsdLazer = getPythLazerOraclePublicKey(program.programId, 6); await adminClient.initializePythLazerOracle(6); + await adminClient.initializeAmmCache(); + await adminClient.initializePerpMarket( 0, solUsd, diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts index d256c56ed3..756c0a0fa9 100644 --- a/tests/lpPoolCUs.ts +++ b/tests/lpPoolCUs.ts @@ -171,6 +171,8 @@ describe('LP Pool', () => { solUsd = await mockOracleNoProgram(bankrunContextWrapper, 200); + await adminClient.initializeAmmCache(); + adminClient = new TestClient({ connection: bankrunContextWrapper.connection.toConnection(), wallet: new anchor.Wallet(keypair), diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index 4361c9a948..d65cb66a9c 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -176,6 +176,8 @@ describe('LP Pool', () => { const periodicity = new BN(0); + await adminClient.initializeAmmCache(); + await adminClient.initializePerpMarket( 0, spotMarketOracle, From f297f60857ddf682d9e9765ccd96a85b9e5e4d4e Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:44:17 +0000 Subject: [PATCH 220/247] sdk: release v2.146.0-beta.3 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index b77e5ca327..28a36e0c9f 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.2 \ No newline at end of file +2.146.0-beta.3 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 954270e98c..540d72539d 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.2", + "version": "2.146.0-beta.3", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 722f55359158a64d23b458d3d18413f465ddc8e2 Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:51:09 -0700 Subject: [PATCH 221/247] add delete amm cache ix (#1999) --- programs/drift/src/instructions/admin.rs | 22 ++++++++ programs/drift/src/lib.rs | 12 +++++ sdk/src/adminClient.ts | 24 ++++++++- sdk/src/idl/drift.json | 69 +++++++++++++++++++----- tests/lpPool.ts | 23 ++++++++ 5 files changed, 136 insertions(+), 14 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 747a8cb17e..456886ea29 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1166,6 +1166,11 @@ pub fn handle_resize_amm_cache(ctx: Context) -> Result<()> { Ok(()) } +pub fn handle_delete_amm_cache(_ctx: Context) -> Result<()> { + msg!("deleted amm cache"); + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -5562,6 +5567,23 @@ pub struct ResizeAmmCache<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct DeleteAmmCache<'info> { + #[account(mut)] + pub admin: Signer<'info>, + #[account( + has_one = admin + )] + pub state: Box>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump, + close = admin, + )] + pub amm_cache: Box>, +} + #[derive(Accounts)] pub struct DeleteInitializedPerpMarket<'info> { #[account(mut)] diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index fbf629a34d..b9b3c8c35a 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1020,6 +1020,18 @@ pub mod drift { handle_initialize_amm_cache(ctx) } + pub fn resize_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeAmmCache<'info>>, + ) -> Result<()> { + handle_resize_amm_cache(ctx) + } + + pub fn delete_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DeleteAmmCache<'info>>, + ) -> Result<()> { + handle_delete_amm_cache(ctx) + } + pub fn update_initial_amm_cache_info<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, ) -> Result<()> { diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 010870185f..6507487bc3 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -665,7 +665,7 @@ export class AdminClient extends DriftClient { public async resizeAmmCache( txParams?: TxParams ): Promise { - const initializeAmmCacheIx = await this.getInitializeAmmCacheIx(); + const initializeAmmCacheIx = await this.getResizeAmmCacheIx(); const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); @@ -688,6 +688,28 @@ export class AdminClient extends DriftClient { }); } + public async deleteAmmCache( + txParams?: TxParams + ): Promise { + const deleteAmmCacheIx = await this.getDeleteAmmCacheIx(); + + const tx = await this.buildTransaction(deleteAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getDeleteAmmCacheIx(): Promise { + return await this.program.instruction.deleteAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.getStateAccount().admin, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + }); + } + public async updateInitialAmmCacheInfo( perpMarketIndexes: number[], txParams?: TxParams diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index bfdb4ad5d2..9cfdacca01 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4453,6 +4453,58 @@ ], "args": [] }, + { + "name": "resizeAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "deleteAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "updateInitialAmmCacheInfo", "accounts": [ @@ -13058,12 +13110,6 @@ "type": { "option": "u16" } - }, - { - "name": "isolatedPositionDeposit", - "type": { - "option": "u64" - } } ] } @@ -13129,12 +13175,6 @@ "type": { "option": "u16" } - }, - { - "name": "isolatedPositionDeposit", - "type": { - "option": "u64" - } } ] } @@ -15118,6 +15158,9 @@ }, { "name": "SafeMMOracle" + }, + { + "name": "Margin" } ] } @@ -19745,4 +19788,4 @@ "msg": "Invalid Lp Pool Id for Operation" } ] -} +} \ No newline at end of file diff --git a/tests/lpPool.ts b/tests/lpPool.ts index aadfbaf9e7..2ae7911a24 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -1733,4 +1733,27 @@ describe('LP Pool', () => { assert(e.message.includes('0x18')); } }); + + it('can delete amm cache and then init and realloc and update', async () => { + const ammCacheKey = getAmmCachePublicKey(program.programId); + const ammCacheBefore = (await adminClient.program.account.ammCache.fetch( + ammCacheKey + )) as AmmCache; + + await adminClient.deleteAmmCache(); + await adminClient.initializeAmmCache(); + await adminClient.resizeAmmCache(); + await adminClient.updateInitialAmmCacheInfo([0, 1, 2]); + await adminClient.updateAmmCache([0, 1, 2]); + + const ammCacheAfter = (await adminClient.program.account.ammCache.fetch( + ammCacheKey + )) as AmmCache; + + for (let i = 0; i < ammCacheBefore.cache.length; i++) { + assert( + ammCacheBefore.cache[i].position.eq(ammCacheAfter.cache[i].position) + ); + } + }); }); From 757f129469283b68489f5eb01b4a7d95a5dab4b8 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:56:06 +0000 Subject: [PATCH 222/247] sdk: release v2.146.0-beta.4 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 28a36e0c9f..01c8542c48 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.3 \ No newline at end of file +2.146.0-beta.4 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 540d72539d..cf7885d505 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.3", + "version": "2.146.0-beta.4", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 86f59646c074eb6b7e9d7f870b76931e7355f520 Mon Sep 17 00:00:00 2001 From: moosecat <14929853+moosecat2@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:02:05 -0700 Subject: [PATCH 223/247] remove delete amm ixs (#2000) --- programs/drift/src/lib.rs | 6 ------ sdk/src/adminClient.ts | 36 ++++++++++++++++---------------- sdk/src/idl/drift.json | 21 ------------------- tests/lpPool.ts | 44 +++++++++++++++++++-------------------- 4 files changed, 40 insertions(+), 67 deletions(-) diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index b9b3c8c35a..3d13094ebf 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1026,12 +1026,6 @@ pub mod drift { handle_resize_amm_cache(ctx) } - pub fn delete_amm_cache<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, DeleteAmmCache<'info>>, - ) -> Result<()> { - handle_delete_amm_cache(ctx) - } - pub fn update_initial_amm_cache_info<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, ) -> Result<()> { diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 6507487bc3..293a7e32ed 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -736,9 +736,9 @@ export class AdminClient extends DriftClient { return await this.program.instruction.updateInitialAmmCacheInfo({ accounts: { state: await this.getStatePublicKey(), - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, ammCache: getAmmCachePublicKey(this.program.programId), }, remainingAccounts, @@ -796,9 +796,9 @@ export class AdminClient extends DriftClient { { accounts: { state: await this.getStatePublicKey(), - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, ammCache: getAmmCachePublicKey(this.program.programId), }, } @@ -820,9 +820,9 @@ export class AdminClient extends DriftClient { return this.program.instruction.resetAmmCache({ accounts: { state: await this.getStatePublicKey(), - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, ammCache: getAmmCachePublicKey(this.program.programId), systemProgram: anchor.web3.SystemProgram.programId, }, @@ -5545,9 +5545,9 @@ export class AdminClient extends DriftClient { { accounts: { constituent, - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, state: await this.getStatePublicKey(), }, } @@ -6435,9 +6435,9 @@ export class AdminClient extends DriftClient { lpExchangeFeeExcluscionScalar ?? null, { accounts: { - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, state: await this.getStatePublicKey(), perpMarket: this.getPerpMarketAccount(marketIndex).pubkey, }, @@ -6466,9 +6466,9 @@ export class AdminClient extends DriftClient { pausedOperations, { accounts: { - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, state: await this.getStatePublicKey(), perpMarket: this.getPerpMarketAccount(marketIndex).pubkey, }, diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 9cfdacca01..3d02127b5c 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4484,27 +4484,6 @@ ], "args": [] }, - { - "name": "deleteAmmCache", - "accounts": [ - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "ammCache", - "isMut": true, - "isSigner": false - } - ], - "args": [] - }, { "name": "updateInitialAmmCacheInfo", "accounts": [ diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 2ae7911a24..c6a6168472 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -1734,26 +1734,26 @@ describe('LP Pool', () => { } }); - it('can delete amm cache and then init and realloc and update', async () => { - const ammCacheKey = getAmmCachePublicKey(program.programId); - const ammCacheBefore = (await adminClient.program.account.ammCache.fetch( - ammCacheKey - )) as AmmCache; - - await adminClient.deleteAmmCache(); - await adminClient.initializeAmmCache(); - await adminClient.resizeAmmCache(); - await adminClient.updateInitialAmmCacheInfo([0, 1, 2]); - await adminClient.updateAmmCache([0, 1, 2]); - - const ammCacheAfter = (await adminClient.program.account.ammCache.fetch( - ammCacheKey - )) as AmmCache; - - for (let i = 0; i < ammCacheBefore.cache.length; i++) { - assert( - ammCacheBefore.cache[i].position.eq(ammCacheAfter.cache[i].position) - ); - } - }); + // it('can delete amm cache and then init and realloc and update', async () => { + // const ammCacheKey = getAmmCachePublicKey(program.programId); + // const ammCacheBefore = (await adminClient.program.account.ammCache.fetch( + // ammCacheKey + // )) as AmmCache; + + // await adminClient.deleteAmmCache(); + // await adminClient.initializeAmmCache(); + // await adminClient.resizeAmmCache(); + // await adminClient.updateInitialAmmCacheInfo([0, 1, 2]); + // await adminClient.updateAmmCache([0, 1, 2]); + + // const ammCacheAfter = (await adminClient.program.account.ammCache.fetch( + // ammCacheKey + // )) as AmmCache; + + // for (let i = 0; i < ammCacheBefore.cache.length; i++) { + // assert( + // ammCacheBefore.cache[i].position.eq(ammCacheAfter.cache[i].position) + // ); + // } + // }); }); From 84e6e97cb4530b955100d644fa2563a314d1c023 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 20:07:38 +0000 Subject: [PATCH 224/247] sdk: release v2.146.0-beta.5 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 01c8542c48..cd3e6ea7c2 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.4 \ No newline at end of file +2.146.0-beta.5 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index cf7885d505..980f34b3b7 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.4", + "version": "2.146.0-beta.5", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 536f3cea9510cc4a448948d9ff3b5b2b4824dc09 Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 29 Oct 2025 14:18:25 -0700 Subject: [PATCH 225/247] fix tests for amm cache (#2001) --- sdk/src/adminClient.ts | 16 ++++++++++++++-- tests/lpPool.ts | 3 --- tests/lpPoolCUs.ts | 2 -- tests/lpPoolSwap.ts | 2 -- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 293a7e32ed..9698e13716 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -507,6 +507,12 @@ export class AdminClient extends DriftClient { ): Promise { const currentPerpMarketIndex = this.getStateAccount().numberOfMarkets; + const ammCachePublicKey = getAmmCachePublicKey(this.program.programId); + const ammCacheAccount = await this.connection.getAccountInfo( + ammCachePublicKey + ); + const mustInitializeAmmCache = ammCacheAccount?.data == null; + const initializeMarketIxs = await this.getInitializePerpMarketIx( marketIndex, priceOracle, @@ -534,7 +540,8 @@ export class AdminClient extends DriftClient { curveUpdateIntensity, ammJitIntensity, name, - lpPoolId + lpPoolId, + mustInitializeAmmCache ); const tx = await this.buildTransaction(initializeMarketIxs); @@ -581,7 +588,8 @@ export class AdminClient extends DriftClient { curveUpdateIntensity = 0, ammJitIntensity = 0, name = DEFAULT_MARKET_NAME, - lpPoolId: number = 0 + lpPoolId: number = 0, + includeInitAmmCacheIx = false ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, @@ -590,6 +598,10 @@ export class AdminClient extends DriftClient { const ixs: TransactionInstruction[] = []; + if (includeInitAmmCacheIx) { + ixs.push(await this.getInitializeAmmCacheIx()); + } + const nameBuffer = encodeName(name); const initPerpIx = await this.program.instruction.initializePerpMarket( marketIndex, diff --git a/tests/lpPool.ts b/tests/lpPool.ts index c6a6168472..6cd7229776 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -191,8 +191,6 @@ describe('LP Pool', () => { solUsdLazer = getPythLazerOraclePublicKey(program.programId, 6); await adminClient.initializePythLazerOracle(6); - await adminClient.initializeAmmCache(); - await adminClient.initializePerpMarket( 0, solUsd, @@ -1741,7 +1739,6 @@ describe('LP Pool', () => { // )) as AmmCache; // await adminClient.deleteAmmCache(); - // await adminClient.initializeAmmCache(); // await adminClient.resizeAmmCache(); // await adminClient.updateInitialAmmCacheInfo([0, 1, 2]); // await adminClient.updateAmmCache([0, 1, 2]); diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts index 756c0a0fa9..d256c56ed3 100644 --- a/tests/lpPoolCUs.ts +++ b/tests/lpPoolCUs.ts @@ -171,8 +171,6 @@ describe('LP Pool', () => { solUsd = await mockOracleNoProgram(bankrunContextWrapper, 200); - await adminClient.initializeAmmCache(); - adminClient = new TestClient({ connection: bankrunContextWrapper.connection.toConnection(), wallet: new anchor.Wallet(keypair), diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index d65cb66a9c..4361c9a948 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -176,8 +176,6 @@ describe('LP Pool', () => { const periodicity = new BN(0); - await adminClient.initializeAmmCache(); - await adminClient.initializePerpMarket( 0, spotMarketOracle, From 82cee738e426793f0c0b024ae61a2b3a04970ed6 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 21:23:41 +0000 Subject: [PATCH 226/247] sdk: release v2.146.0-beta.6 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index cd3e6ea7c2..08733bb68e 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.5 \ No newline at end of file +2.146.0-beta.6 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 980f34b3b7..d0f3620727 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.5", + "version": "2.146.0-beta.6", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From c8a9debd8a8a3d773701c651b4566d41ddb16cbd Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 29 Oct 2025 17:57:12 -0700 Subject: [PATCH 227/247] v2.145.1 --- CHANGELOG.md | 12 ++++++++++++ Cargo.lock | 2 +- programs/drift/Cargo.toml | 2 +- sdk/package.json | 2 +- sdk/src/idl/drift.json | 4 ++-- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68fb1376ab..baef9c7f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking +## [2.145.1] - 2025-10-20 + +### Features + +### Fixes + +- dlp ([#1998](https://github.com/drift-labs/protocol-v2/pull/1998)) +- dlp ([#1999](https://github.com/drift-labs/protocol-v2/pull/1999)) +- dlp ([#2000](https://github.com/drift-labs/protocol-v2/pull/2000)) + +### Breaking + ## [2.145.0] - 2025-10-28 ### Features diff --git a/Cargo.lock b/Cargo.lock index 910debab7c..6e04c98d96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.145.0" +version = "2.145.1" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 762cfdd509..b2730356e9 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.145.0" +version = "2.145.1" description = "Created with Anchor" edition = "2018" diff --git a/sdk/package.json b/sdk/package.json index d0f3620727..c374981d24 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.6", + "version": "2.145.1", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 3d02127b5c..543c57f3db 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.145.0", + "version": "2.145.1", "name": "drift", "instructions": [ { @@ -19767,4 +19767,4 @@ "msg": "Invalid Lp Pool Id for Operation" } ] -} \ No newline at end of file +} From 0b5c00194a5f423fe948d811b705848e760de214 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 01:04:51 +0000 Subject: [PATCH 228/247] sdk: release v2.146.0-beta.0 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 08733bb68e..c3792566e1 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.6 \ No newline at end of file +2.146.0-beta.0 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index c374981d24..8b90a0bea0 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.145.1", + "version": "2.146.0-beta.0", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From b3ac08b96f61b3458d6b3cf18669a312995ed3d9 Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 29 Oct 2025 18:11:27 -0700 Subject: [PATCH 229/247] bump sdk --- sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/package.json b/sdk/package.json index 8b90a0bea0..d0f3620727 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.0", + "version": "2.146.0-beta.6", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 6df768faa3123986d9de9ea82e101b457c9c6b02 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 01:17:03 +0000 Subject: [PATCH 230/247] sdk: release v2.146.0-beta.7 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index c3792566e1..e8ba3a59a2 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.0 \ No newline at end of file +2.146.0-beta.7 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index d0f3620727..7849e2fd80 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.6", + "version": "2.146.0-beta.7", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 9bd45098cf90c2c258c50faa46370a2bbd4dea4d Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Thu, 30 Oct 2025 09:12:19 -0600 Subject: [PATCH 231/247] feat: min and max 64 constants --- sdk/src/constants/numericConstants.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/src/constants/numericConstants.ts b/sdk/src/constants/numericConstants.ts index e82de7d81e..34f11da0c9 100644 --- a/sdk/src/constants/numericConstants.ts +++ b/sdk/src/constants/numericConstants.ts @@ -1,5 +1,6 @@ import { LAMPORTS_PER_SOL } from '@solana/web3.js'; import { BN } from '@coral-xyz/anchor'; +import { BigNum } from '../factory/bigNum'; export const ZERO = new BN(0); export const ONE = new BN(1); @@ -116,3 +117,6 @@ export const FUEL_START_TS = new BN(1723147200); // unix timestamp export const MAX_PREDICTION_PRICE = PRICE_PRECISION; export const GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE = 99; + +export const MIN_I64 = BigNum.fromPrint('-9223372036854775808').val; +export const MAX_I64 = BigNum.fromPrint('9223372036854775807').val; \ No newline at end of file From 457c499f0db73e4107774fe9d4ad8dd29865d0a7 Mon Sep 17 00:00:00 2001 From: lil perp Date: Thu, 30 Oct 2025 12:20:38 -0400 Subject: [PATCH 232/247] program: add isolated_position_deposit to swift params (#2002) * program: add isolated_position_deposit to swift params * CHANGELOG --- CHANGELOG.md | 2 + programs/drift/src/state/order_params.rs | 2 + .../drift/src/validation/sig_verification.rs | 3 + .../src/validation/sig_verification/tests.rs | 103 ++++++++++++++++++ 4 files changed, 110 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index baef9c7f20..a0a802019e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: add isolated_position_deposit to signed msg params + ### Fixes ### Breaking diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 422857fed2..a5b81b8bb6 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -875,6 +875,7 @@ pub struct SignedMsgOrderParamsMessage { pub max_margin_ratio: Option, pub builder_idx: Option, pub builder_fee_tenth_bps: Option, + pub isolated_position_deposit: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] @@ -888,6 +889,7 @@ pub struct SignedMsgOrderParamsDelegateMessage { pub max_margin_ratio: Option, pub builder_idx: Option, pub builder_fee_tenth_bps: Option, + pub isolated_position_deposit: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index a7b1fbcf0a..c2b6d79be9 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -60,6 +60,7 @@ pub struct VerifiedMessage { pub max_margin_ratio: Option, pub builder_idx: Option, pub builder_fee_tenth_bps: Option, + pub isolated_position_deposit: Option, pub signature: [u8; 64], } @@ -100,6 +101,7 @@ pub fn deserialize_into_verified_message( max_margin_ratio: deserialized.max_margin_ratio, builder_idx: deserialized.builder_idx, builder_fee_tenth_bps: deserialized.builder_fee_tenth_bps, + isolated_position_deposit: deserialized.isolated_position_deposit, signature: *signature, }); } else { @@ -129,6 +131,7 @@ pub fn deserialize_into_verified_message( max_margin_ratio: deserialized.max_margin_ratio, builder_idx: deserialized.builder_idx, builder_fee_tenth_bps: deserialized.builder_fee_tenth_bps, + isolated_position_deposit: deserialized.isolated_position_deposit, signature: *signature, }); } diff --git a/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs index 3c5c2d1c66..250d5c871d 100644 --- a/programs/drift/src/validation/sig_verification/tests.rs +++ b/programs/drift/src/validation/sig_verification/tests.rs @@ -148,6 +148,55 @@ mod sig_verification { assert_eq!(order_params.auction_end_price, Some(237000000i64)); } + #[test] + fn test_deserialize_into_verified_message_non_delegate_with_isolated_position_deposit() { + let signature = [1u8; 64]; + let payload = vec![ + 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, + 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, + 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 0, 0, 0, 1, 1, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, false); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, Some(2)); + assert_eq!(verified_message.delegate_signed_taker_pubkey, None); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.isolated_position_deposit.is_some()); + assert_eq!(verified_message.isolated_position_deposit.unwrap(), 1); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 3456000000u64); + assert_eq!(tp.trigger_price, 240000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 3456000000u64); + assert_eq!(sl.trigger_price, 225000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 3); + assert_eq!(order_params.direction, PositionDirection::Long); + assert_eq!(order_params.base_asset_amount, 3456000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(230000000i64)); + assert_eq!(order_params.auction_end_price, Some(237000000i64)); + } + #[test] fn test_deserialize_into_verified_message_delegate() { let signature = [1u8; 64]; @@ -353,4 +402,58 @@ mod sig_verification { assert_eq!(order_params.auction_start_price, Some(230000000i64)); assert_eq!(order_params.auction_end_price, Some(237000000i64)); } + + #[test] + fn test_deserialize_into_verified_message_delegate_with_isolated_position_deposit() { + let signature = [1u8; 64]; + let payload = vec![ + 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, + 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, + 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, + 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, + 59, 0, 0, 0, 0, 0, 0, 0, 1, 1, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, true); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, None); + assert_eq!( + verified_message.delegate_signed_taker_pubkey, + Some(Pubkey::from_str("HG2iQKnRkkasrLptwMZewV6wT7KPstw9wkA8yyu8Nx3m").unwrap()) + ); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.isolated_position_deposit.is_some()); + assert_eq!(verified_message.isolated_position_deposit.unwrap(), 1); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 1000000000u64); + assert_eq!(tp.trigger_price, 230000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 1000000000u64); + assert_eq!(sl.trigger_price, 250000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 2); + assert_eq!(order_params.direction, PositionDirection::Short); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(240000000i64)); + assert_eq!(order_params.auction_end_price, Some(238000000i64)); + } } From 2600639a334aff915ee1d50522e09a9e805bb2d5 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 30 Oct 2025 12:30:03 -0400 Subject: [PATCH 233/247] sdk: update idl --- sdk/src/idl/drift.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 543c57f3db..f11d15d6d4 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -13089,6 +13089,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -13154,6 +13160,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -19767,4 +19779,4 @@ "msg": "Invalid Lp Pool Id for Operation" } ] -} +} \ No newline at end of file From 5b90d7ce89810b946738af90d133fa016fd06a6c Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:36:23 +0000 Subject: [PATCH 234/247] sdk: release v2.146.0-beta.8 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index e8ba3a59a2..5245bf685c 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.7 \ No newline at end of file +2.146.0-beta.8 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 7849e2fd80..b7fe7aa16d 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.7", + "version": "2.146.0-beta.8", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 86fb94ee7f512ec1966a3ee6c8ba980a37a55ea1 Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 30 Oct 2025 11:13:03 -0700 Subject: [PATCH 235/247] sdk: remove launchcoin lazerid (#2003) --- sdk/src/constants/perpMarkets.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 43b10a39ec..af42b494e5 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1299,7 +1299,6 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ oracleSource: OracleSource.PYTH_LAZER, pythFeedId: '0x6d74813ee17291d5be18a355fe4d43fd300d625caea6554d49f740e7d112141e', - pythLazerId: 1571, }, { fullName: 'PUMP', From 383fdbae6d039784f90011ab21196d57ce883353 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:18:05 +0000 Subject: [PATCH 236/247] sdk: release v2.146.0-beta.9 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 5245bf685c..7bb8416333 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.8 \ No newline at end of file +2.146.0-beta.9 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index b7fe7aa16d..d0b319516a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.8", + "version": "2.146.0-beta.9", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 33634d2ee6fbd37313ef7749e41c05aab7b661e5 Mon Sep 17 00:00:00 2001 From: wphan Date: Thu, 30 Oct 2025 11:43:01 -0700 Subject: [PATCH 237/247] remove pythid (#2004) --- sdk/src/constants/perpMarkets.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index af42b494e5..981ea9521f 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1297,8 +1297,6 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ oracle: new PublicKey('GAzR3C5cn7gGVvuqJB57wSYTPWP3n2Lw4mRJRxvTvqYy'), launchTs: 1747318237000, oracleSource: OracleSource.PYTH_LAZER, - pythFeedId: - '0x6d74813ee17291d5be18a355fe4d43fd300d625caea6554d49f740e7d112141e', }, { fullName: 'PUMP', From 0f930ab6268e183369706ec7d45f11dac9e5c187 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:48:59 +0000 Subject: [PATCH 238/247] sdk: release v2.146.0-beta.10 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 7bb8416333..ae13bce475 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.9 \ No newline at end of file +2.146.0-beta.10 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index d0b319516a..a8fa17963c 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.9", + "version": "2.146.0-beta.10", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From fa9a03dd1b2da7e475199fe6d153896674619a54 Mon Sep 17 00:00:00 2001 From: cha-kos Date: Thu, 30 Oct 2025 15:47:22 -0400 Subject: [PATCH 239/247] Add backwards compatibility for swap client args --- sdk/src/driftClient.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index c5f4c9707a..087a99c0e6 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -5756,6 +5756,7 @@ export class DriftClient { /** * Swap tokens in drift account using titan or jupiter * @param swapClient swap client to find routes and instructions (Titan or Jupiter) + * @param jupiterClient @deprecated Use swapClient instead. Legacy parameter for backward compatibility * @param outMarketIndex the market index of the token you're buying * @param inMarketIndex the market index of the token you're selling * @param outAssociatedTokenAccount the token account to receive the token being sold on titan or jupiter @@ -5771,6 +5772,7 @@ export class DriftClient { */ public async swap({ swapClient, + jupiterClient, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, @@ -5784,7 +5786,9 @@ export class DriftClient { quote, onlyDirectRoutes = false, }: { - swapClient: UnifiedSwapClient | SwapClient; + swapClient?: UnifiedSwapClient | SwapClient; + /** @deprecated Use swapClient instead. Legacy parameter for backward compatibility */ + jupiterClient?: JupiterClient; outMarketIndex: number; inMarketIndex: number; outAssociatedTokenAccount?: PublicKey; @@ -5800,15 +5804,22 @@ export class DriftClient { }; quote?: QuoteResponse; }): Promise { + // Handle backward compatibility: use jupiterClient if swapClient is not provided + const clientToUse = swapClient || jupiterClient; + + if (!clientToUse) { + throw new Error('Either swapClient or jupiterClient must be provided'); + } + let res: { ixs: TransactionInstruction[]; lookupTables: AddressLookupTableAccount[]; }; // Use unified SwapClient if available - if (swapClient instanceof UnifiedSwapClient) { + if (clientToUse instanceof UnifiedSwapClient) { res = await this.getSwapIxV2({ - swapClient, + swapClient: clientToUse, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, @@ -5821,9 +5832,9 @@ export class DriftClient { quote, v6, }); - } else if (swapClient instanceof TitanClient) { + } else if (clientToUse instanceof TitanClient) { res = await this.getTitanSwapIx({ - titanClient: swapClient, + titanClient: clientToUse, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, @@ -5834,10 +5845,10 @@ export class DriftClient { onlyDirectRoutes, reduceOnly, }); - } else if (swapClient instanceof JupiterClient) { + } else if (clientToUse instanceof JupiterClient) { const quoteToUse = quote ?? v6?.quote; res = await this.getJupiterSwapIxV6({ - jupiterClient: swapClient, + jupiterClient: clientToUse, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, From a253f4f34ff72e46a3baee24745c84eb4c5ef070 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Thu, 30 Oct 2025 15:39:14 -0600 Subject: [PATCH 240/247] lukas/fix grpc multi user smol bugs (#1995) * fix: add missing slot check on multi user update slot * feat: min i64 * feat: only flush grpc multi user subscriber if is multi subscribed * fix: prettier and lint --- sdk/src/accounts/grpcMultiUserAccountSubscriber.ts | 9 ++++++++- sdk/src/constants/numericConstants.ts | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts b/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts index b4ad528d55..38ea46c78c 100644 --- a/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts @@ -119,7 +119,10 @@ export class grpcMultiUserAccountSubscriber { this.listeners.set(key, new Set()); this.keyToPk.set(key, userAccountPublicKey); this.pendingAddKeys.add(key); - this.scheduleFlush(); + if (this.isMultiSubscribed) { + // only schedule flush if already subscribed to the multi-subscriber + this.scheduleFlush(); + } } }; @@ -161,6 +164,10 @@ export class grpcMultiUserAccountSubscriber { }, updateData(userAccount: UserAccount, slot: number): void { + const existingData = parent.userData.get(key); + if (existingData && existingData.slot > slot) { + return; + } parent.userData.set(key, { data: userAccount, slot }); perUserEmitter.emit('userAccountUpdate', userAccount); perUserEmitter.emit('update'); diff --git a/sdk/src/constants/numericConstants.ts b/sdk/src/constants/numericConstants.ts index e82de7d81e..1128d77674 100644 --- a/sdk/src/constants/numericConstants.ts +++ b/sdk/src/constants/numericConstants.ts @@ -1,5 +1,6 @@ import { LAMPORTS_PER_SOL } from '@solana/web3.js'; import { BN } from '@coral-xyz/anchor'; +import { BigNum } from '../factory/bigNum'; export const ZERO = new BN(0); export const ONE = new BN(1); @@ -116,3 +117,7 @@ export const FUEL_START_TS = new BN(1723147200); // unix timestamp export const MAX_PREDICTION_PRICE = PRICE_PRECISION; export const GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE = 99; + +// integer constants +export const MAX_I64 = BigNum.fromPrint('9223372036854775807').val; +export const MIN_I64 = BigNum.fromPrint('-9223372036854775808').val; From 0c51fee5b5b83a8002c26c1b68a8d42002b5dd0b Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 21:44:19 +0000 Subject: [PATCH 241/247] sdk: release v2.146.0-beta.11 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index ae13bce475..de25f54e2d 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.10 \ No newline at end of file +2.146.0-beta.11 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index a8fa17963c..77c225be42 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.10", + "version": "2.146.0-beta.11", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From 4c99a39ba7b29612b47efb7749e6be21ecf7ab77 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:14:56 +0000 Subject: [PATCH 242/247] sdk: release v2.146.0-beta.12 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index de25f54e2d..29de64d340 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.146.0-beta.11 \ No newline at end of file +2.146.0-beta.12 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 77c225be42..6d2bbaa487 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.146.0-beta.11", + "version": "2.146.0-beta.12", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", "module": "./lib/browser/index.js", From ccb8cc8f9f0acebacb0ed6814d21d327f0999143 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Fri, 31 Oct 2025 10:32:50 -0600 Subject: [PATCH 243/247] fix: bug with margin removal --- sdk/src/driftClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 508e77e602..da3457de30 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -4222,7 +4222,7 @@ export class DriftClient { const amountWithBuffer = noAmountBuffer || amount.eq(BigNum.fromPrint('-9223372036854775808').val) ? amount - : amount.add(amount.mul(new BN(1000))); // .1% buffer + : amount.add(amount.div(new BN(1000))); // .1% buffer return await this.program.instruction.transferIsolatedPerpPositionDeposit( spotMarketIndex, From d54414238ad7c08e18fe3a2506bc284884c9d6a2 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Sun, 2 Nov 2025 22:50:22 -0700 Subject: [PATCH 244/247] fix: missing swift iso deposit from idl --- sdk/src/idl/drift.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index de37ef704f..66fd6b3d40 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -13246,6 +13246,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -13311,6 +13317,12 @@ "type": { "option": "u16" } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } From 4590e1c2d4be52ad19ba272371679175f4e020e9 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 3 Nov 2025 08:22:42 -0700 Subject: [PATCH 245/247] fix: lint and prettify --- sdk/src/decode/user.ts | 2 +- sdk/src/driftClient.ts | 26 +++++------ sdk/src/marginCalculation.ts | 9 ---- sdk/src/math/position.ts | 10 ++-- sdk/src/math/spotPosition.ts | 5 +- sdk/src/user.ts | 65 ++++++++++++++++++-------- sdk/tests/dlob/helpers.ts | 8 ++-- sdk/tests/user/getMarginCalculation.ts | 62 ++++++++++++++---------- 8 files changed, 109 insertions(+), 78 deletions(-) diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index 759d091c8f..f363b3901a 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -141,7 +141,7 @@ export function decodeUser(buffer: Buffer): UserAccount { maxMarginRatio, positionFlag, isolatedPositionScaledBalance, - }); + }); } const orders: Order[] = []; diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index b35ec5cd62..47e450951f 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -46,7 +46,6 @@ import { PhoenixV1FulfillmentConfigAccount, PlaceAndTakeOrderSuccessCondition, PositionDirection, - PositionFlag, ReferrerInfo, ReferrerNameAccount, SerumV3FulfillmentConfigAccount, @@ -4182,19 +4181,18 @@ export class DriftClient { subAccountId?: number, txParams?: TxParams ): Promise { - const tx =await this.buildTransaction( + const tx = await this.buildTransaction( await this.getTransferIsolatedPerpPositionDepositIx( amount, perpMarketIndex, subAccountId ), txParams - ) - const { txSig } = await this.sendTransaction( - tx, - [], - {...this.opts, skipPreflight: true} ); + const { txSig } = await this.sendTransaction(tx, [], { + ...this.opts, + skipPreflight: true, + }); return txSig; } @@ -4220,9 +4218,10 @@ export class DriftClient { readablePerpMarketIndex: [perpMarketIndex], }); - const amountWithBuffer = noAmountBuffer || amount.eq(BigNum.fromPrint('-9223372036854775808').val) - ? amount - : amount.add(amount.div(new BN(1000))); // .1% buffer + const amountWithBuffer = + noAmountBuffer || amount.eq(BigNum.fromPrint('-9223372036854775808').val) + ? amount + : amount.add(amount.div(new BN(1000))); // .1% buffer return await this.program.instruction.transferIsolatedPerpPositionDeposit( spotMarketIndex, @@ -4265,8 +4264,7 @@ export class DriftClient { amount: BN, perpMarketIndex: number, subAccountId?: number, - userTokenAccount?: PublicKey, - dontSettle?: boolean + userTokenAccount?: PublicKey ): Promise { const userAccountPublicKey = await getUserAccountPublicKey( this.program.programId, @@ -4295,8 +4293,8 @@ export class DriftClient { const amountToWithdraw = amount.gt(depositAmountPlusUnrealizedPnl) ? BigNum.fromPrint('-9223372036854775808').val // min i64 : amount; - console.log("amountToWithdraw", amountToWithdraw.toString()); - console.log("amount", amount.toString()); + console.log('amountToWithdraw', amountToWithdraw.toString()); + console.log('amount', amount.toString()); let associatedTokenAccount = userTokenAccount; if (!associatedTokenAccount) { diff --git a/sdk/src/marginCalculation.ts b/sdk/src/marginCalculation.ts index fe9fa8d7eb..c26333db00 100644 --- a/sdk/src/marginCalculation.ts +++ b/sdk/src/marginCalculation.ts @@ -80,15 +80,6 @@ export class MarginContext { this.marginRatioOverride = ratio; return this; } - - trackMarketMarginRequirement(marketIdentifier: MarketIdentifier): this { - if (this.mode.type !== 'Liquidation') { - throw new Error( - 'InvalidMarginCalculation: Cant track market outside of liquidation mode' - ); - } - return this; - } } export class IsolatedMarginCalculation { diff --git a/sdk/src/math/position.ts b/sdk/src/math/position.ts index 6a5da647ec..d0c3a16a0d 100644 --- a/sdk/src/math/position.ts +++ b/sdk/src/math/position.ts @@ -245,13 +245,17 @@ export function positionIsAvailable(position: PerpPosition): boolean { position.openOrders === 0 && position.quoteAssetAmount.eq(ZERO) && position.lpShares.eq(ZERO) && - position.isolatedPositionScaledBalance.eq(ZERO) - && !positionIsBeingLiquidated(position) + position.isolatedPositionScaledBalance.eq(ZERO) && + !positionIsBeingLiquidated(position) ); } export function positionIsBeingLiquidated(position: PerpPosition): boolean { - return (position.positionFlag & (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) > 0; + return ( + (position.positionFlag & + (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) > + 0 + ); } /** diff --git a/sdk/src/math/spotPosition.ts b/sdk/src/math/spotPosition.ts index ac5c6d7611..d05a7382d4 100644 --- a/sdk/src/math/spotPosition.ts +++ b/sdk/src/math/spotPosition.ts @@ -51,7 +51,10 @@ export function getWorstCaseTokenAmounts( strictOraclePrice ); - if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO) || !includeOpenOrders) { + if ( + (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO)) || + !includeOpenOrders + ) { const { weight, weightedTokenValue } = calculateWeightedTokenValue( tokenAmount, tokenValue, diff --git a/sdk/src/user.ts b/sdk/src/user.ts index a3cc119616..50cf587598 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -107,10 +107,7 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice'; import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber'; -import { - MarginCalculation, - MarginContext, -} from './marginCalculation'; +import { MarginCalculation, MarginContext } from './marginCalculation'; export type MarginType = 'Cross' | 'Isolated'; @@ -716,7 +713,7 @@ export class User { const market = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); - if(!market) return unrealizedPnl; + if (!market) return unrealizedPnl; const oraclePriceData = this.getMMOracleDataForPerpMarket( market.marketIndex ); @@ -1267,7 +1264,6 @@ export class User { return 0; } - let totalCollateral: BN; let maintenanceMarginReq: BN; @@ -1317,7 +1313,7 @@ export class User { perpPosition.marketIndex ); - if(!market) return ZERO; + if (!market) return ZERO; let valuationPrice = this.getOracleDataForPerpMarket( market.marketIndex @@ -1960,13 +1956,23 @@ export class User { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN; - liquidationStatuses: Map<'cross' | number, { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }>; + liquidationStatuses: Map< + 'cross' | number, + { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN } + >; } { // Deprecated signature retained for backward compatibility in type only // but implementation now delegates to the new Map-based API and returns cross margin status. const map = this.getLiquidationStatuses(); const cross = map.get('cross'); - return cross ? { ...cross, liquidationStatuses: map } : { canBeLiquidated: false, marginRequirement: ZERO, totalCollateral: ZERO, liquidationStatuses: map }; + return cross + ? { ...cross, liquidationStatuses: map } + : { + canBeLiquidated: false, + marginRequirement: ZERO, + totalCollateral: ZERO, + liquidationStatuses: map, + }; } /** @@ -1975,18 +1981,28 @@ export class User { * - 'cross' for cross margin * - marketIndex (number) for each isolated perp position */ - public getLiquidationStatuses(marginCalc?: MarginCalculation): Map<'cross' | number, { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }> { + public getLiquidationStatuses( + marginCalc?: MarginCalculation + ): Map< + 'cross' | number, + { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN } + > { // If not provided, use buffer-aware calc for canBeLiquidated checks if (!marginCalc) { const liquidationBuffer = this.getLiquidationBuffer(); - marginCalc = this.getMarginCalculation('Maintenance', { liquidationBuffer }); + marginCalc = this.getMarginCalculation('Maintenance', { + liquidationBuffer, + }); } - const result = new Map<'cross' | number, { - canBeLiquidated: boolean; - marginRequirement: BN; - totalCollateral: BN; - }>(); + const result = new Map< + 'cross' | number, + { + canBeLiquidated: boolean; + marginRequirement: BN; + totalCollateral: BN; + } + >(); // Cross margin status const crossTotalCollateral = marginCalc.totalCollateral; @@ -1998,7 +2014,10 @@ export class User { }); // Isolated positions status - for (const [marketIndex, isoCalc] of marginCalc.isolatedMarginCalculations) { + for (const [ + marketIndex, + isoCalc, + ] of marginCalc.isolatedMarginCalculations) { const isoTotalCollateral = isoCalc.totalCollateral; const isoMarginRequirement = isoCalc.marginRequirement; result.set(marketIndex, { @@ -2015,7 +2034,8 @@ export class User { // Consider on-chain flags OR computed margin status (cross or any isolated) const hasOnChainFlag = (this.getUserAccount().status & - (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > 0; + (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > + 0; const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); return ( hasOnChainFlag || @@ -2031,7 +2051,9 @@ export class User { } /** Returns true if any isolated perp position is currently below its maintenance requirement (no buffer). */ - public isIsolatedMarginBeingLiquidated(marginCalc?: MarginCalculation): boolean { + public isIsolatedMarginBeingLiquidated( + marginCalc?: MarginCalculation + ): boolean { const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); for (const [, isoCalc] of calc.isolatedMarginCalculations) { if (isoCalc.totalCollateral.lt(isoCalc.marginRequirement)) { @@ -4313,7 +4335,10 @@ export class User { ); // margin ratio for this perp - const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.maxMarginRatio); + const customMarginRatio = Math.max( + this.getUserAccount().maxMarginRatio, + marketPosition.maxMarginRatio + ); let marginRatio = new BN( calculateMarketMarginRatio( market, diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index aceb917d62..d39141b1e2 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -27,8 +27,6 @@ import { DataAndSlot, } from '../../src'; import { EventEmitter } from 'events'; -import StrictEventEmitter from 'strict-event-emitter-types'; -import { UserEvents } from '../../src/accounts/types'; export const mockPerpPosition: PerpPosition = { baseAssetAmount: new BN(0), @@ -677,9 +675,9 @@ export class MockUserMap implements UserMapInterface { this.eventEmitter = new EventEmitter(); } - public async subscribe(): Promise { } + public async subscribe(): Promise {} - public async unsubscribe(): Promise { } + public async unsubscribe(): Promise {} public async addPubkey(userAccountPublicKey: PublicKey): Promise { const user = new User({ @@ -738,7 +736,7 @@ export class MockUserMap implements UserMapInterface { ); } - public async updateWithOrderRecord(_record: OrderRecord): Promise { } + public async updateWithOrderRecord(_record: OrderRecord): Promise {} public values(): IterableIterator { return this.userMap.values(); diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts index afc2111996..0df348a19a 100644 --- a/sdk/tests/user/getMarginCalculation.ts +++ b/sdk/tests/user/getMarginCalculation.ts @@ -12,7 +12,6 @@ import { QUOTE_PRECISION, SPOT_MARKET_BALANCE_PRECISION, SpotBalanceType, - MARGIN_PRECISION, OPEN_ORDER_MARGIN_REQUIREMENT, SPOT_MARKET_WEIGHT_PRECISION, PositionFlag, @@ -57,8 +56,7 @@ async function makeMockUser( function getMockOracle(oracleKey: PublicKey) { const data: OraclePriceData = { price: new BN( - (oraclePriceMap[oracleKey.toString()] ?? 1) * - PRICE_PRECISION.toNumber() + (oraclePriceMap[oracleKey.toString()] ?? 1) * PRICE_PRECISION.toNumber() ), slot: new BN(0), confidence: new BN(1), @@ -79,8 +77,10 @@ async function makeMockUser( mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any; mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any; mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any; - mockUser.driftClient.getOracleDataForPerpMarket = getOracleDataForPerpMarket as any; - mockUser.driftClient.getOracleDataForSpotMarket = getOracleDataForSpotMarket as any; + mockUser.driftClient.getOracleDataForPerpMarket = + getOracleDataForPerpMarket as any; + mockUser.driftClient.getOracleDataForSpotMarket = + getOracleDataForSpotMarket as any; return mockUser; } @@ -219,9 +219,9 @@ describe('getMarginCalculation snapshot', () => { myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(200000000).mul( BASE_PRECISION ); - myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-180000000).mul( - QUOTE_PRECISION - ); + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN( + -180000000 + ).mul(QUOTE_PRECISION); const user: User = await makeMockUser( myMockPerpMarkets, @@ -250,9 +250,15 @@ describe('getMarginCalculation snapshot', () => { const spotOracles = [1, 1, 1, 1, 1, 1, 1, 1]; // Pre-fill: maker has 21 base long at entry 1 ($21 notional), taker flat - makerAccount.perpPositions[0].baseAssetAmount = new BN(21).mul(BASE_PRECISION); - makerAccount.perpPositions[0].quoteEntryAmount = new BN(-21).mul(QUOTE_PRECISION); - makerAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-21).mul(QUOTE_PRECISION); + makerAccount.perpPositions[0].baseAssetAmount = new BN(21).mul( + BASE_PRECISION + ); + makerAccount.perpPositions[0].quoteEntryAmount = new BN(-21).mul( + QUOTE_PRECISION + ); + makerAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-21).mul( + QUOTE_PRECISION + ); // Provide exactly $2 in quote collateral to equal 10% maintenance of 20 notional post-fill makerAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; makerAccount.spotPositions[0].scaledBalance = new BN(2).mul( @@ -284,9 +290,9 @@ describe('getMarginCalculation snapshot', () => { maker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-20).mul( QUOTE_PRECISION ); - maker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(-20).mul( - QUOTE_PRECISION - ); + maker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN( + -20 + ).mul(QUOTE_PRECISION); // Align quoteAssetAmount with base value so unrealized PnL = 0 at price 1 maker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-20).mul( QUOTE_PRECISION @@ -298,9 +304,9 @@ describe('getMarginCalculation snapshot', () => { taker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-1).mul( QUOTE_PRECISION ); - taker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(-1).mul( - QUOTE_PRECISION - ); + taker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN( + -1 + ).mul(QUOTE_PRECISION); // Also set taker's quoteAssetAmount consistently taker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-1).mul( QUOTE_PRECISION @@ -347,12 +353,16 @@ describe('getMarginCalculation snapshot', () => { SPOT_MARKET_BALANCE_PRECISION ); // No perp exposure in cross calc - crossAccount.perpPositions[0].baseAssetAmount = new BN(100 * BASE_PRECISION.toNumber()); - crossAccount.perpPositions[0].quoteAssetAmount = new BN(-11000 * QUOTE_PRECISION.toNumber()); - crossAccount.perpPositions[0].positionFlag = PositionFlag.IsolatedPosition; - crossAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(100).mul( - SPOT_MARKET_BALANCE_PRECISION + crossAccount.perpPositions[0].baseAssetAmount = new BN( + 100 * BASE_PRECISION.toNumber() + ); + crossAccount.perpPositions[0].quoteAssetAmount = new BN( + -11000 * QUOTE_PRECISION.toNumber() ); + crossAccount.perpPositions[0].positionFlag = PositionFlag.IsolatedPosition; + crossAccount.perpPositions[0].isolatedPositionScaledBalance = new BN( + 100 + ).mul(SPOT_MARKET_BALANCE_PRECISION); const userCross: User = await makeMockUser( myMockPerpMarkets, @@ -386,8 +396,10 @@ describe('getMarginCalculation snapshot', () => { const isoPosition = crossCalcBuf.isolatedMarginCalculations.get(0); assert(isoPosition?.marginRequirementPlusBuffer.eq(new BN('2000000000'))); - assert(isoPosition?.totalCollateralBuffer.add(isoPosition?.totalCollateral).eq(new BN('-1000000000'))); + assert( + isoPosition?.totalCollateralBuffer + .add(isoPosition?.totalCollateral) + .eq(new BN('-1000000000')) + ); }); }); - - From 28c08aa562fb33375fda93db9b7ea97a033462bc Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 3 Nov 2025 10:14:08 -0700 Subject: [PATCH 246/247] feat: increased buffer on isolated deposit opening position --- sdk/src/driftClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 47e450951f..0cb402b901 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -4221,7 +4221,7 @@ export class DriftClient { const amountWithBuffer = noAmountBuffer || amount.eq(BigNum.fromPrint('-9223372036854775808').val) ? amount - : amount.add(amount.div(new BN(1000))); // .1% buffer + : amount.add(amount.div(new BN(250))); // .4% buffer return await this.program.instruction.transferIsolatedPerpPositionDeposit( spotMarketIndex, From 41c1f4b591bea7c444a686aaf470bbf79224aad0 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 3 Nov 2025 10:30:28 -0700 Subject: [PATCH 247/247] fix: missing check on order increasing size for depositing margin place + take --- sdk/src/driftClient.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 0cb402b901..a6be8b31b4 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -313,7 +313,7 @@ export class DriftClient { if ( isolatedPositionDepositAmount?.gt?.(ZERO) && - this.isOrderIncreasingPosition(orderParams, userAccount) + this.isOrderIncreasingPosition(orderParams, userAccount.subAccountId) ) { preIxs.push( await this.getTransferIsolatedPerpPositionDepositIx( @@ -4835,7 +4835,10 @@ export class DriftClient { isolatedPositionDepositAmount?: BN ): Promise { const preIxs: TransactionInstruction[] = []; - if (isolatedPositionDepositAmount?.gt?.(ZERO)) { + if ( + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(orderParams, subAccountId) + ) { preIxs.push( await this.getTransferIsolatedPerpPositionDepositIx( isolatedPositionDepositAmount as BN, @@ -5333,7 +5336,8 @@ export class DriftClient { const p = params[0]; if ( isVariant(p.marketType, 'perp') && - isolatedPositionDepositAmount?.gt?.(ZERO) + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(p, subAccountId) ) { preIxs.push( await this.getTransferIsolatedPerpPositionDepositIx( @@ -7216,7 +7220,8 @@ export class DriftClient { if ( isVariant(orderParams.marketType, 'perp') && - isolatedPositionDepositAmount?.gt?.(ZERO) + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(orderParams, subAccountId) ) { placeAndTakeIxs.push( await this.getTransferIsolatedPerpPositionDepositIx( @@ -12852,8 +12857,9 @@ export class DriftClient { isOrderIncreasingPosition( orderParams: OptionalOrderParams, - userAccount: UserAccount + subAccountId: number ): boolean { + const userAccount = this.getUserAccount(subAccountId); const perpPosition = userAccount.perpPositions.find( (p) => p.marketIndex === orderParams.marketIndex );