Skip to content

fix: oracle sync and protocol-wide vault withdrawal protection#2142

Open
alvarengao wants to merge 5 commits intodrift-labs:masterfrom
alvarengao:master
Open

fix: oracle sync and protocol-wide vault withdrawal protection#2142
alvarengao wants to merge 5 commits intodrift-labs:masterfrom
alvarengao:master

Conversation

@alvarengao
Copy link

@alvarengao alvarengao commented Mar 11, 2026

Description

The oracle address defined in sdk/src/constants/spotMarkets.ts for Devnet spot/0 (USDC) was pointing to a stale PythLazer feed, diverging from the current on-chain state (spot/6). This updates the index 0 oracle to the active PYTH_STABLE_COIN_PULL feed to prevent margin calculation failures on Devnet.

Fixes: - #2138

Testing

  • Verified spotMarkets.ts compiles cleanly.
  • Confirmed the new oracle pubkey matches the active Devnet Pyth feed.

Summary by CodeRabbit

  • Chores
    • Updated USDC oracle configuration on Devnet to a different stable-coin price feed for more consistent pricing.
  • Bug Fixes
    • Added client- and program-level checks to prevent withdrawals being sent to the market vault address, reducing risk of invalid or unsafe withdrawal operations.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1cb8e4f8-c1a4-47f1-90e7-5e138e6e34e2

📥 Commits

Reviewing files that changed from the base of the PR and between 0a3cdd1 and b8cf264.

📒 Files selected for processing (1)
  • sdk/src/driftClient.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/src/driftClient.ts

Walkthrough

Updated Devnet USDC oracle config and added both on-chain and client-side guards preventing withdrawals to the spot market vault by disallowing the vault as the destination token account.

Changes

Cohort / File(s) Summary
DevnetSpotMarkets USDC Oracle Configuration
sdk/src/constants/spotMarkets.ts
Replaced Devnet USDC oracle PublicKey with En8hkHLkRe9d9DraYmBTrus518BvmVH448YcvmrFM6Ce, changed oracleSource from PYTH_LAZER_STABLE_COIN to PYTH_STABLE_COIN_PULL, and removed pythLazerId.
Withdraw validation (on-chain constraints)
programs/drift/src/instructions/user.rs
Added account constraint to Withdraw and WithdrawIsolatedPerpPosition requiring user_token_account.key() != spot_market_vault.key() with error InvalidSpotPosition.
Withdraw validation (client runtime guards)
sdk/src/driftClient.ts
Added runtime checks in withdraw and withdrawFromIsolatedPerpPosition to throw if the destination associated token account equals the spot market vault, preventing construction of invalid withdraw instructions.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client (sdk/driftClient)
  participant Program as On-chain Program (Withdraw)
  participant SpotMarket as SpotMarket Account
  participant UserToken as User Token Account

  Note over Client,SpotMarket: Client computes spot market and destination associated token address
  Client->>SpotMarket: fetch spotMarket (marketIndex)
  SpotMarket-->>Client: spotMarket.vault
  Client->>UserToken: compare destinationAddr == spotMarket.vault
  alt destination == vault
    Client-->>Client: throw Error (prevent instruction)
  else destination != vault
    Client->>Program: send Withdraw instruction (destinationAddr)
    Program->>SpotMarket: program-side account constraint checks
    alt constraint violation (destination is vault)
      Program-->>Client: abort (InvalidSpotPosition)
    else
      Program-->>UserToken: transfer tokens to destination
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐇 I hopped through code with whiskers bright,
Swapped an oracle to make things right.
I barred the vault from pockets small,
No one will lose tokens after all.
🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the two main changes: fixing a stale oracle configuration and adding protocol-wide vault withdrawal protection constraints.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@programs/drift/src/instructions/user.rs`:
- Around line 4757-4758: Add the same account-identity guard to the
WithdrawIsolatedPerpPosition instruction that exists on Withdraw: in the
WithdrawIsolatedPerpPosition constraint list ensure you compare
spot_market_vault.mint == user_token_account.mint and add a constraint that
user_token_account.key() != spot_market_vault.key() returning
ErrorCode::InvalidSpotPosition; this mirrors the protection in Withdraw and
prevents the self-transfer path that handle_withdraw_from_isolated_perp_position
performs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4573388c-50bf-42e5-b76e-53e94bd713b4

📥 Commits

Reviewing files that changed from the base of the PR and between f247bcb and ed4f651.

📒 Files selected for processing (1)
  • programs/drift/src/instructions/user.rs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@sdk/src/driftClient.ts`:
- Around line 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ef9e635-5c3d-4e01-b458-fc398cd5f1f1

📥 Commits

Reviewing files that changed from the base of the PR and between ed4f651 and 6978542.

📒 Files selected for processing (1)
  • sdk/src/driftClient.ts

Comment on lines +4379 to +4383
// 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');
}
Copy link

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.

@alvarengao alvarengao changed the title fix(sdk): sync devnet USDC (spot/0) oracle with current on-chain state fix: prevent withdrawal to spot market vault (contract & sdk) Mar 11, 2026
@alvarengao alvarengao changed the title fix: prevent withdrawal to spot market vault (contract & sdk) ix: oracle sync and protocol-wide vault withdrawal protection Mar 11, 2026
@alvarengao alvarengao changed the title ix: oracle sync and protocol-wide vault withdrawal protection fix: oracle sync and protocol-wide vault withdrawal protection Mar 11, 2026
@alvarengao
Copy link
Author

I have refactored the SDK validation logic to address the feedback. Vault protection is now centralized in the core instruction builders (getWithdrawIx and getWithdrawFromIsolatedPerpPositionIx), using dynamic vault derivation. This ensures that all withdrawal paths are protected against accidental transfers to the protocol vaults without relying on hardcoded indices. Ready for final review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant