Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion programs/drift/src/instructions/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4754,7 +4754,8 @@ pub struct Withdraw<'info> {
pub drift_signer: AccountInfo<'info>,
#[account(
mut,
constraint = &spot_market_vault.mint.eq(&user_token_account.mint)
constraint = &spot_market_vault.mint.eq(&user_token_account.mint),
constraint = user_token_account.key() != spot_market_vault.key() @ ErrorCode::InvalidSpotPosition
)]
pub user_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
pub token_program: Interface<'info, TokenInterface>,
Expand Down
5 changes: 2 additions & 3 deletions sdk/src/constants/spotMarkets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,13 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [
symbol: 'USDC',
marketIndex: 0,
poolId: 0,
oracle: new PublicKey('9VCioxmni2gDLv11qufWzT3RDERhQE4iY5Gf7NTfYyAV'),
oracleSource: OracleSource.PYTH_LAZER_STABLE_COIN,
oracle: new PublicKey('En8hkHLkRe9d9DraYmBTrus518BvmVH448YcvmrFM6Ce'),
oracleSource: OracleSource.PYTH_STABLE_COIN_PULL,
mint: new PublicKey('8zGuJQqwhZafTah7Uc7Z4tXRnguqkn5KLFAP8oV6PHe2'),
precision: new BN(10).pow(SIX),
precisionExp: SIX,
pythFeedId:
'0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a',
pythLazerId: 7,
},
{
symbol: 'SOL',
Expand Down
11 changes: 11 additions & 0 deletions sdk/src/driftClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3788,6 +3788,11 @@ export class DriftClient {
txParams?: TxParams,
updateFuel = false
): Promise<TransactionSignature> {
// Ensure the destination account is not the vault itself to prevent state inconsistency
const spotMarket = this.getSpotMarketAccount(marketIndex);
if (spotMarket.vault.equals(associatedTokenAddress)) {
throw Error('Destination associatedTokenAddress cannot be the same as the spot market vault');
}
const additionalSigners: Array<Signer> = [];

const withdrawIxs = await this.getWithdrawalIxs(
Expand Down Expand Up @@ -4371,6 +4376,12 @@ export class DriftClient {
subAccountId?: number,
txParams?: TxParams
): Promise<TransactionSignature> {
// Ensure the destination account is not the vault itself to prevent state inconsistency
const spotMarket = this.getSpotMarketAccount(QUOTE_SPOT_MARKET_INDEX);
if (spotMarket.vault.equals(userTokenAccount)) {
throw Error('Destination userTokenAccount cannot be the same as the spot market vault');
}
Comment on lines +4379 to +4383
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Move this validation into the shared ix path and derive the vault from the perp market.

This check is currently bypassable because getWithdrawFromIsolatedPerpPositionIxsBundle() and getWithdrawFromIsolatedPerpPositionIx() are public. It also hardcodes QUOTE_SPOT_MARKET_INDEX, while the actual instruction uses perpMarketAccount.quoteSpotMarketIndex at Line 4475. That matters here because WithdrawIsolatedPerpPosition in programs/drift/src/instructions/user.rs (Lines 4941-4971) has no equivalent on-chain user_token_account != spot_market_vault constraint, so this wrapper is not enough to prevent the invalid vault destination case.

🔧 Suggested fix
 public async withdrawFromIsolatedPerpPosition(
 	amount: BN,
 	perpMarketIndex: number,
 	userTokenAccount: PublicKey,
 	subAccountId?: number,
 	txParams?: TxParams
 ): Promise<TransactionSignature> {
-		// Ensure the destination account is not the vault itself to prevent state inconsistency
-		const spotMarket = this.getSpotMarketAccount(QUOTE_SPOT_MARKET_INDEX);
-		if (spotMarket.vault.equals(userTokenAccount)) {
-			throw Error('Destination userTokenAccount cannot be the same as the spot market vault');
-		}
-
 		const instructions =
 			await this.getWithdrawFromIsolatedPerpPositionIxsBundle(
 				amount,
private assertDestinationIsNotVault(
	spotMarketVault: PublicKey,
	destination: PublicKey,
	label: string
): void {
	if (spotMarketVault.equals(destination)) {
		throw new Error(`Destination ${label} cannot be the same as the spot market vault`);
	}
}

public async getWithdrawFromIsolatedPerpPositionIx(
	amount: BN,
	perpMarketIndex: number,
	userTokenAccount: PublicKey,
	subAccountId?: number
): Promise<TransactionInstruction> {
	const userAccountPublicKey = await getUserAccountPublicKey(
		this.program.programId,
		this.authority,
		subAccountId ?? this.activeSubAccountId
	);
	const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex);
	const spotMarketIndex = perpMarketAccount.quoteSpotMarketIndex;
	const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex);

	this.assertDestinationIsNotVault(
		spotMarketAccount.vault,
		userTokenAccount,
		'userTokenAccount'
	);

	// existing logic...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/src/driftClient.ts` around lines 4379 - 4383, The current vault-check is
bypassable and hardcodes QUOTE_SPOT_MARKET_INDEX; move this validation into the
shared instruction path by adding a private helper (e.g.,
assertDestinationIsNotVault(spotMarketVault: PublicKey, destination: PublicKey,
label: string)) and call it from the shared withdraw-to-user-token instruction
builders (getWithdrawFromIsolatedPerpPositionIx and
getWithdrawFromIsolatedPerpPositionIxsBundle) after deriving spotMarketIndex
from perpMarketAccount.quoteSpotMarketIndex and obtaining
spotMarketAccount.vault; remove use of the hardcoded QUOTE_SPOT_MARKET_INDEX in
this flow so the check always runs and throws if userTokenAccount equals the
spot market vault.


const instructions =
await this.getWithdrawFromIsolatedPerpPositionIxsBundle(
amount,
Expand Down