Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ resolver = "2"
[profile.release]
lto = "fat"
codegen-units = 1
overflow-checks = true

[profile.release.build-override]
opt-level = 3
Expand Down
17 changes: 12 additions & 5 deletions programs/drift/src/instructions/keeper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3180,12 +3180,13 @@ pub fn handle_force_delete_user<'c: 'info, 'info>(

let slot = Clock::get()?.slot;
let now = Clock::get()?.unix_timestamp;
let mut remaining_accounts_iter = 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_iter,
Comment on lines +3183 to +3189

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 | 🟠 Major | 🏗️ Heavy lift

Shared transfer-hook iterator can break later CPIs in the same instruction.

A single remaining_accounts_iter is reused across the whole function, but the transfer-hook path consumes iterator entries. Since this function can perform multiple hook-enabled transfers (inside the spot-position loop), the first hook CPI can drain accounts required by later hook CPIs and cause runtime failure.

Use per-transfer account windows/iterators (or make hook-account consumption bounded to one transfer) instead of one shared draining iterator.

Also applies to: 3317-3321, 3339-3343

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@programs/drift/src/instructions/keeper.rs` around lines 3183 - 3189, The
issue is that a single remaining_accounts_iter is initialized once and reused
across the entire function, but the transfer-hook execution path consumes
iterator entries. When the spot-position loop processes multiple hook-enabled
transfers, the first hook CPI drains accounts from the shared iterator that are
needed by subsequent hook CPIs. To fix this, create isolated account windows or
iterators for each transfer within the loop instead of relying on one shared
draining iterator. This means calculating the account slice needed for each
individual transfer (considering load_maps consumption and hook-specific
accounts) and passing only that slice to the hook execution, or rebuilding the
iterator state for each transfer iteration.

&MarketSet::new(),
&get_market_set_for_spot_positions(&user.spot_positions),
slot,
Expand Down Expand Up @@ -3305,7 +3306,6 @@ pub fn handle_force_delete_user<'c: 'info, 'info>(
true,
)?;

// TODO: support transfer hook tokens
send_from_program_vault(
&token_program,
&spot_market_vault_account_info,
Expand All @@ -3314,7 +3314,11 @@ pub fn handle_force_delete_user<'c: 'info, 'info>(
state.signer_nonce,
token_amount.cast()?,
&mint_account_info,
None,
if spot_market.has_transfer_hook() {
Some(&mut remaining_accounts_iter)
} else {
None
},
)?;
} else {
update_spot_balances(
Expand All @@ -3325,15 +3329,18 @@ pub fn handle_force_delete_user<'c: 'info, 'info>(
false,
)?;

// TODO: support transfer hook tokens
receive(
token_program,
&keeper_vault_account_info,
&spot_market_vault_account_info,
&ctx.accounts.keeper.to_account_info(),
token_amount.cast()?,
&mint_account_info,
None,
if spot_market.has_transfer_hook() {
Some(&mut remaining_accounts_iter)
} else {
None
},
)?;
}

Expand Down
17 changes: 12 additions & 5 deletions programs/drift/src/instructions/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1319,12 +1319,13 @@ pub fn handle_transfer_pools<'c: 'info, 'info>(
"cant transfer between the same pool"
)?;

let mut remaining_accounts_iter = 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_iter,
Comment on lines +1322 to +1328

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 | 🟠 Major | 🏗️ Heavy lift

Dual hook transfers can fail due to shared draining iterator.

remaining_accounts_iter is shared between deposit-side and borrow-side token CPIs. When both sides require transfer-hook accounts, the first hook transfer can consume the iterator, leaving the second without required accounts.

Please split/segment remaining accounts per transfer invocation (or change consumption semantics so each call reads only its own required extras).

Also applies to: 1698-1702, 1731-1735

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@programs/drift/src/instructions/user.rs` around lines 1322 - 1328, The shared
remaining_accounts_iter is being consumed by both deposit-side and borrow-side
token CPI calls, causing the first transfer hook to deplete accounts needed by
the second transfer. Either segment the remaining_accounts into separate slices
before passing to each transfer operation (one for deposit operations and one
for borrow operations), or refactor the code to pass only the required account
slice to each transfer invocation instead of sharing a single peekable iterator.
This applies to all locations where dual hook transfers occur (around lines
1322-1328, 1698-1702, and 1731-1735).

&MarketSet::new(),
&get_writable_spot_market_set_from_many(vec![
deposit_from_market_index,
Expand Down Expand Up @@ -1686,7 +1687,6 @@ pub fn handle_transfer_pools<'c: 'info, 'info>(
.find(|acc| acc.key() == spot_market_mint.key())
.map(|acc| InterfaceAccount::try_from(acc).unwrap());

// TODO: support transfer hook tokens
controller::token::send_from_program_vault(
token_program,
&ctx.accounts.deposit_from_spot_market_vault,
Expand All @@ -1695,7 +1695,11 @@ pub fn handle_transfer_pools<'c: 'info, 'info>(
state.signer_nonce,
deposit_transfer,
&mint_account_info,
None,
if deposit_from_spot_market.has_transfer_hook() {
Some(&mut remaining_accounts_iter)
} else {
None
},
)?;
}

Expand All @@ -1716,7 +1720,6 @@ pub fn handle_transfer_pools<'c: 'info, 'info>(
.find(|acc| acc.key() == spot_market_mint.key())
.map(|acc| InterfaceAccount::try_from(acc).unwrap());

// TODO: support transfer hook tokens
controller::token::send_from_program_vault(
token_program,
&ctx.accounts.borrow_to_spot_market_vault,
Expand All @@ -1725,7 +1728,11 @@ pub fn handle_transfer_pools<'c: 'info, 'info>(
state.signer_nonce,
borrow_transfer,
&mint_account_info,
None,
if borrow_to_spot_market.has_transfer_hook() {
Some(&mut remaining_accounts_iter)
} else {
None
},
)?;
}

Expand Down
6 changes: 4 additions & 2 deletions sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"build": "yarn clean && tsc -p tsconfig.json && tsc -p tsconfig.browser.json && node scripts/postbuild.js",
"build:browser": "yarn clean && tsc -p tsconfig.json && tsc -p tsconfig.browser.json && node scripts/postbuild.js --force-env browser",

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 | ⚡ Quick win

Critical: prebuild:browser is never invoked by build:browser.

The prebuild:browser script is defined on line 19 but the build:browser command does not call it. Per the PR objectives, the shim script must run before the browser build to create the helius-laserstream shim for compilation. Without this, the browser build will fail when the optional dependency is not installed.

🔧 Proposed fix to invoke prebuild:browser
-"build:browser": "yarn clean && tsc -p tsconfig.json && tsc -p tsconfig.browser.json && node scripts/postbuild.js --force-env browser",
+"build:browser": "yarn prebuild:browser && yarn clean && tsc -p tsconfig.json && tsc -p tsconfig.browser.json && node scripts/postbuild.js --force-env browser",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"build:browser": "yarn clean && tsc -p tsconfig.json && tsc -p tsconfig.browser.json && node scripts/postbuild.js --force-env browser",
"build:browser": "yarn prebuild:browser && yarn clean && tsc -p tsconfig.json && tsc -p tsconfig.browser.json && node scripts/postbuild.js --force-env browser",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/package.json` at line 16, The build:browser script currently does not
invoke the prebuild:browser script that is required to run beforehand. Modify
the build:browser command to call prebuild:browser first before executing the
TypeScript compilation and post-build steps. This ensures that the
helius-laserstream shim is created during the prebuild phase so it's available
for the subsequent browser build compilation to use. You should structure the
command so that prebuild:browser runs and completes successfully before the tsc
and postbuild commands execute.

"build:minify": "node build-browser.js",
"clean": "rm -rf lib",
"prebuild": "node scripts/mock-optional-deps.js",
"prebuild:browser": "node scripts/mock-optional-deps.js",
"clean": "node -e \"const fs = require('fs'); fs.rmSync('lib', { recursive: true, force: true })\"",
"test": "mocha -r ts-node/register tests/**/*.ts --ignore 'tests/dlob/**/*.ts'",
"test:match": "mocha -r ts-node/register --ignore 'tests/dlob/**/*.ts'",
"test:inspect": "mocha --inspect-brk -r ts-node/register tests/**/*.ts",
Expand Down Expand Up @@ -104,7 +106,7 @@
},
"description": "SDK for Drift Protocol",
"engines": {
"node": "^24.0.0"
"node": ">=18.0.0"
},
"resolutions": {
"@solana/web3.js": "1.98.0",
Expand Down
101 changes: 101 additions & 0 deletions sdk/scripts/mock-optional-deps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const fs = require('fs');
const path = require('path');

const targetDir = path.join(__dirname, '..', 'node_modules', 'helius-laserstream');

let packageResolvable = false;
try {
require.resolve('helius-laserstream');
packageResolvable = true;
} catch (_) {
// not installed or not resolvable
}

if (!packageResolvable) {
console.log('helius-laserstream is not installed. Creating a local shim for compilation...');
try {
fs.mkdirSync(targetDir, { recursive: true });
} catch (e) {
console.error('Failed to create directory for helius-laserstream shim:', e);
process.exit(1);
}

const packageJson = {
name: 'helius-laserstream',
version: '0.1.8',
main: 'index.js',
types: 'index.d.ts'
};

const indexJs = `module.exports = {
CommitmentLevel: {
PROCESSED: 0,
CONFIRMED: 1,
FINALIZED: 2
},
CompressionAlgorithms: {
identity: 0,
deflate: 1,
gzip: 2,
zstd: 3
},
subscribe: () => {
throw new Error('helius-laserstream is shimmed and not available on this platform.');
}
};`;

const indexDts = `export interface LaserstreamConfig {
apiKey?: string;
endpoint?: string;
maxReconnectAttempts?: number;
channelOptions?: any;
}
export interface SubscribeRequest {
slots?: any;
accounts?: any;
transactions?: any;
blocks?: any;
blocksMeta?: any;
accountsDataSlice?: any;
commitment?: any;
entry?: any;
transactionsStatus?: any;
}
export interface SubscribeUpdate {
account?: {
slot: string | number;
account: {
pubkey: string;
owner: string;
lamports: string | number;
data: Uint8Array | number[] | string;
executable: boolean;
rentEpoch: string | number;
};
};
}
export enum CommitmentLevel {
PROCESSED = 0,
CONFIRMED = 1,
FINALIZED = 2
}
export enum CompressionAlgorithms {
identity = 0,
deflate = 1,
gzip = 2,
zstd = 3
}
export function subscribe(...args: any[]): any;`;

try {
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(packageJson, null, 2));
fs.writeFileSync(path.join(targetDir, 'index.js'), indexJs);
fs.writeFileSync(path.join(targetDir, 'index.d.ts'), indexDts);
console.log('Shim created successfully.');
} catch (e) {
console.error('Failed to write files for helius-laserstream shim:', e);
process.exit(1);
}
} else {
console.log('helius-laserstream is already installed.');
}
27 changes: 27 additions & 0 deletions sdk/src/driftClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2319,6 +2319,12 @@ export class DriftClient {
tokenPrograms.add(tokenProgram.toBase58());

this.addTokenMintToRemainingAccounts(spotMarket, remainingAccounts);
if (this.isTransferHook(spotMarket)) {
await this.addExtraAccountMetasToRemainingAccounts(
spotMarket.mint,
remainingAccounts
);
}
}

for (const tokenProgram of tokenPrograms) {
Expand Down Expand Up @@ -4114,6 +4120,27 @@ export class DriftClient {
});
}

if (depositAmount === undefined || !depositAmount.isZero()) {
this.addTokenMintToRemainingAccounts(depositFromSpotMarket, remainingAccounts);
if (this.isTransferHook(depositFromSpotMarket)) {
await this.addExtraAccountMetasToRemainingAccounts(
depositFromSpotMarket.mint,
remainingAccounts
);
}
}

if (borrowAmount === undefined || !borrowAmount.isZero()) {
const borrowToSpotMarket = this.getSpotMarketAccount(borrowToMarketIndex);
this.addTokenMintToRemainingAccounts(borrowToSpotMarket, remainingAccounts);
if (this.isTransferHook(borrowToSpotMarket)) {
await this.addExtraAccountMetasToRemainingAccounts(
borrowToSpotMarket.mint,
remainingAccounts
);
}
}

return await this.program.instruction.transferPools(
depositFromMarketIndex,
depositToMarketIndex,
Expand Down
Loading