Skip to content

Commit aa69979

Browse files
authored
Add configurable balance checking in token account preparation (#971)
* Update TS SDK. Add tests * Rust SDK. Update tests. * Changeset * Format * Update test * Revert unwanted changes
1 parent 6434e22 commit aa69979

File tree

8 files changed

+270
-15
lines changed

8 files changed

+270
-15
lines changed

.changeset/flat-toes-post.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@orca-so/whirlpools-rust": minor
3+
"@orca-so/whirlpools": minor
4+
---
5+
6+
Add configurable balance checking in token account preparation, allowing users to disable balance validation to get quotes and instructions even with insufficient token balances.

rust-sdk/whirlpool/src/config.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,25 @@ pub fn set_native_mint_wrapping_strategy(
138138
Ok(())
139139
}
140140

141+
/// The default setting for enforcing balance checks during token account preparation.
142+
pub const DEFAULT_ENFORCE_TOKEN_BALANCE_CHECK: bool = false;
143+
144+
/// The currently selected setting for enforcing balance checks during token account preparation.
145+
/// When true, the system will assert that token accounts have sufficient balance before proceeding.
146+
/// When false, balance checks are skipped, allowing users to get quotes and instructions even with insufficient balance.
147+
pub static ENFORCE_TOKEN_BALANCE_CHECK: Mutex<bool> =
148+
Mutex::new(DEFAULT_ENFORCE_TOKEN_BALANCE_CHECK);
149+
150+
/// Sets whether to enforce balance checks during token account preparation.
151+
///
152+
/// # Arguments
153+
///
154+
/// * `enforce_balance_check` - When true, the system will assert that token accounts have sufficient balance. When false, balance checks are skipped.
155+
pub fn set_enforce_token_balance_check(enforce_balance_check: bool) -> Result<(), Box<dyn Error>> {
156+
*ENFORCE_TOKEN_BALANCE_CHECK.try_lock()? = enforce_balance_check;
157+
Ok(())
158+
}
159+
141160
/// Resets the configuration to its default values.
142161
pub fn reset_configuration() -> Result<(), Box<dyn Error>> {
143162
*WHIRLPOOLS_CONFIG_ADDRESS.try_lock()? = SOLANA_MAINNET_WHIRLPOOLS_CONFIG_ADDRESS;
@@ -146,6 +165,7 @@ pub fn reset_configuration() -> Result<(), Box<dyn Error>> {
146165
*FUNDER.try_lock()? = DEFAULT_FUNDER;
147166
*NATIVE_MINT_WRAPPING_STRATEGY.try_lock()? = DEFAULT_NATIVE_MINT_WRAPPING_STRATEGY;
148167
*SLIPPAGE_TOLERANCE_BPS.try_lock()? = DEFAULT_SLIPPAGE_TOLERANCE_BPS;
168+
*ENFORCE_TOKEN_BALANCE_CHECK.try_lock()? = DEFAULT_ENFORCE_TOKEN_BALANCE_CHECK;
149169
Ok(())
150170
}
151171

@@ -214,6 +234,18 @@ mod tests {
214234
reset_configuration().unwrap();
215235
}
216236

237+
#[test]
238+
#[serial]
239+
fn test_set_enforce_token_balance_check() {
240+
set_enforce_token_balance_check(true).unwrap();
241+
assert!(*ENFORCE_TOKEN_BALANCE_CHECK.lock().unwrap());
242+
243+
set_enforce_token_balance_check(false).unwrap();
244+
assert!(!(*ENFORCE_TOKEN_BALANCE_CHECK.lock().unwrap()));
245+
246+
reset_configuration().unwrap();
247+
}
248+
217249
#[test]
218250
#[serial]
219251
fn test_reset_configuration() {
@@ -231,5 +263,9 @@ mod tests {
231263
NativeMintWrappingStrategy::Keypair
232264
);
233265
assert_eq!(*SLIPPAGE_TOLERANCE_BPS.lock().unwrap(), 100);
266+
assert_eq!(
267+
*ENFORCE_TOKEN_BALANCE_CHECK.lock().unwrap(),
268+
DEFAULT_ENFORCE_TOKEN_BALANCE_CHECK
269+
);
234270
}
235271
}

rust-sdk/whirlpool/src/increase_liquidity.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,7 @@ mod tests {
992992

993993
#[tokio::test]
994994
#[serial]
995-
async fn test_increase_liquidity_fails_if_deposit_exceeds_user_balance(
995+
async fn test_increase_liquidity_succeeds_if_deposit_exceeds_user_balance_when_balance_check_not_enforced(
996996
) -> Result<(), Box<dyn Error>> {
997997
let ctx = RpcContext::new().await;
998998

@@ -1015,9 +1015,43 @@ mod tests {
10151015
)
10161016
.await;
10171017

1018+
assert!(
1019+
res.is_ok(),
1020+
"Should succeed when balance checking is disabled even if deposit exceeds balance"
1021+
);
1022+
1023+
Ok(())
1024+
}
1025+
1026+
#[tokio::test]
1027+
#[serial]
1028+
async fn test_increase_liquidity_fails_if_deposit_exceeds_user_balance_when_balance_check_enforced(
1029+
) -> Result<(), Box<dyn Error>> {
1030+
let ctx = RpcContext::new().await;
1031+
crate::set_enforce_token_balance_check(true)?;
1032+
1033+
let minted = setup_all_mints(&ctx).await?;
1034+
let user_atas = setup_all_atas(&ctx, &minted).await?;
1035+
1036+
let mint_a_key = minted.get("A").unwrap();
1037+
let mint_b_key = minted.get("B").unwrap();
1038+
let pool_pubkey = setup_whirlpool(&ctx, *mint_a_key, *mint_b_key, 64).await?;
1039+
1040+
let position_mint = setup_position(&ctx, pool_pubkey, Some((-100, 100)), None).await?;
1041+
1042+
// Attempt
1043+
let res = increase_liquidity_instructions(
1044+
&ctx.rpc,
1045+
position_mint,
1046+
IncreaseLiquidityParam::TokenA(2_000_000_000),
1047+
Some(100),
1048+
Some(ctx.signer.pubkey()),
1049+
)
1050+
.await;
1051+
10181052
assert!(
10191053
res.is_err(),
1020-
"Should fail if user tries depositing more than balance"
1054+
"Should fail if user tries depositing more than balance when balance checking is enforced"
10211055
);
10221056
let err_str = format!("{:?}", res.err().unwrap());
10231057
assert!(
@@ -1027,6 +1061,7 @@ mod tests {
10271061
err_str
10281062
);
10291063

1064+
crate::reset_configuration()?;
10301065
Ok(())
10311066
}
10321067

rust-sdk/whirlpool/src/token.rs

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ use spl_token_2022::ID as TOKEN_2022_PROGRAM_ID;
2020
use std::time::{Duration, SystemTime, UNIX_EPOCH};
2121
use std::{collections::HashMap, error::Error};
2222

23-
use crate::{NativeMintWrappingStrategy, NATIVE_MINT_WRAPPING_STRATEGY};
23+
use crate::{
24+
NativeMintWrappingStrategy, ENFORCE_TOKEN_BALANCE_CHECK, NATIVE_MINT_WRAPPING_STRATEGY,
25+
};
2426

2527
#[derive(Debug, PartialEq, Eq, Hash)]
2628
pub(crate) enum TokenAccountStrategy {
@@ -124,7 +126,8 @@ pub(crate) async fn prepare_token_accounts_instructions(
124126
0
125127
};
126128

127-
if existing_balance < required_balance {
129+
let enforce_balance_check = *ENFORCE_TOKEN_BALANCE_CHECK.try_lock()?;
130+
if enforce_balance_check && existing_balance < required_balance {
128131
return Err(format!("Insufficient balance for mint {}", mint_addresses[i]).into());
129132
}
130133
}
@@ -460,10 +463,10 @@ mod tests {
460463

461464
#[tokio::test]
462465
#[serial]
463-
async fn test_insufficient_balance() {
466+
async fn test_insufficient_balance_check_not_enforced() {
464467
let ctx = RpcContext::new().await;
465468

466-
// Create a mint and token account with small balance using token.rs helpers
469+
// Create a mint and token account with small balance
467470
let mint = setup_mint(&ctx).await.unwrap();
468471
let initial_amount = 1_000u64;
469472
let _ata = setup_ata_with_amount(&ctx, mint, initial_amount)
@@ -479,12 +482,87 @@ mod tests {
479482
)
480483
.await;
481484

482-
// Should fail due to insufficient balance
485+
// Should succeed because balance checking is disabled
486+
assert!(result.is_ok());
487+
}
488+
489+
#[tokio::test]
490+
#[serial]
491+
async fn test_insufficient_balance_check_enforced() {
492+
let ctx = RpcContext::new().await;
493+
crate::set_enforce_token_balance_check(true).unwrap();
494+
495+
// Create a mint and token account with small balance
496+
let mint = setup_mint(&ctx).await.unwrap();
497+
let initial_amount = 1_000u64;
498+
let _ata = setup_ata_with_amount(&ctx, mint, initial_amount)
499+
.await
500+
.unwrap();
501+
502+
// Try to prepare instructions requiring more balance
503+
let required_amount = 2_000u64;
504+
let result = prepare_token_accounts_instructions(
505+
&ctx.rpc,
506+
ctx.signer.pubkey(),
507+
vec![TokenAccountStrategy::WithBalance(mint, required_amount)],
508+
)
509+
.await;
510+
511+
// Should fail due to insufficient balance when checking is enabled
512+
assert!(result.is_err());
513+
assert!(result
514+
.unwrap_err()
515+
.to_string()
516+
.contains("Insufficient balance"));
517+
crate::reset_configuration().unwrap();
518+
}
519+
520+
#[tokio::test]
521+
#[serial]
522+
async fn test_nonexistent_token_account_balance_check_not_enforced() {
523+
let ctx = RpcContext::new().await;
524+
525+
// Create a mint but no token account
526+
let mint = setup_mint(&ctx).await.unwrap();
527+
528+
// Try to prepare instructions requiring balance for non-existent account
529+
let required_amount = 1_000u64;
530+
let result = prepare_token_accounts_instructions(
531+
&ctx.rpc,
532+
ctx.signer.pubkey(),
533+
vec![TokenAccountStrategy::WithBalance(mint, required_amount)],
534+
)
535+
.await;
536+
537+
// Should succeed because balance checking is disabled
538+
assert!(result.is_ok());
539+
}
540+
541+
#[tokio::test]
542+
#[serial]
543+
async fn test_nonexistent_token_account_balance_check_enforced() {
544+
let ctx = RpcContext::new().await;
545+
crate::set_enforce_token_balance_check(true).unwrap();
546+
547+
// Create a mint but no token account
548+
let mint = setup_mint(&ctx).await.unwrap();
549+
550+
// Try to prepare instructions requiring balance for non-existent account
551+
let required_amount = 1_000u64;
552+
let result = prepare_token_accounts_instructions(
553+
&ctx.rpc,
554+
ctx.signer.pubkey(),
555+
vec![TokenAccountStrategy::WithBalance(mint, required_amount)],
556+
)
557+
.await;
558+
559+
// Should fail due to insufficient balance (0) when checking is enabled
483560
assert!(result.is_err());
484561
assert!(result
485562
.unwrap_err()
486563
.to_string()
487564
.contains("Insufficient balance"));
565+
crate::reset_configuration().unwrap();
488566
}
489567

490568
#[tokio::test]

ts-sdk/whirlpool/src/config.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,29 @@ export function setNativeMintWrappingStrategy(
173173
NATIVE_MINT_WRAPPING_STRATEGY = strategy;
174174
}
175175

176+
/**
177+
* The default setting for enforcing balance checks during token account preparation.
178+
*/
179+
export const DEFAULT_ENFORCE_TOKEN_BALANCE_CHECK = false;
180+
181+
/**
182+
* The currently selected setting for enforcing balance checks during token account preparation.
183+
* When true, the system will assert that token accounts have sufficient balance before proceeding.
184+
* When false, balance checks are skipped, allowing users to get quotes and instructions even with insufficient balance.
185+
*/
186+
export let ENFORCE_TOKEN_BALANCE_CHECK = DEFAULT_ENFORCE_TOKEN_BALANCE_CHECK;
187+
188+
/**
189+
* Sets whether to enforce balance checks during token account preparation.
190+
*
191+
* @param {boolean} enforceBalanceCheck - When true, the system will assert that token accounts have sufficient balance. When false, balance checks are skipped.
192+
*/
193+
export function setEnforceTokenBalanceCheck(
194+
enforceBalanceCheck: boolean,
195+
): void {
196+
ENFORCE_TOKEN_BALANCE_CHECK = enforceBalanceCheck;
197+
}
198+
176199
/**
177200
* Resets the configuration to its default state.
178201
*
@@ -185,6 +208,7 @@ export function resetConfiguration() {
185208
FUNDER = DEFAULT_FUNDER;
186209
SLIPPAGE_TOLERANCE_BPS = DEFAULT_SLIPPAGE_TOLERANCE_BPS;
187210
NATIVE_MINT_WRAPPING_STRATEGY = DEFAULT_NATIVE_MINT_WRAPPING_STRATEGY;
211+
ENFORCE_TOKEN_BALANCE_CHECK = DEFAULT_ENFORCE_TOKEN_BALANCE_CHECK;
188212
}
189213

190214
let _payer: KeyPairSigner | undefined;

ts-sdk/whirlpool/src/token.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import {
2626
getAddressEncoder,
2727
lamports,
2828
} from "@solana/kit";
29-
import { NATIVE_MINT_WRAPPING_STRATEGY } from "./config";
29+
import {
30+
NATIVE_MINT_WRAPPING_STRATEGY,
31+
ENFORCE_TOKEN_BALANCE_CHECK,
32+
} from "./config";
3033
import {
3134
getCreateAccountInstruction,
3235
getCreateAccountWithSeedInstruction,
@@ -143,10 +146,12 @@ export async function prepareTokenAccountsInstructions(
143146
const existingBalance = tokenAccount.exists
144147
? tokenAccount.data.amount
145148
: 0n;
146-
assert(
147-
BigInt(spec[mint.address]) <= existingBalance,
148-
`Token account for ${mint.address} does not have the required balance`,
149-
);
149+
if (ENFORCE_TOKEN_BALANCE_CHECK) {
150+
assert(
151+
BigInt(spec[mint.address]) <= existingBalance,
152+
`Token account for ${mint.address} does not have the required balance`,
153+
);
154+
}
150155
}
151156
}
152157

ts-sdk/whirlpool/tests/config.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import {
1515
DEFAULT_NATIVE_MINT_WRAPPING_STRATEGY,
1616
DEFAULT_WHIRLPOOLS_CONFIG_EXTENSION_ADDRESS,
1717
DEFAULT_WHIRLPOOLS_CONFIG_ADDRESSES,
18+
ENFORCE_TOKEN_BALANCE_CHECK,
19+
DEFAULT_ENFORCE_TOKEN_BALANCE_CHECK,
20+
setEnforceTokenBalanceCheck,
1821
} from "../src/config";
1922
import assert from "assert";
2023
import { address, createKeyPairSignerFromPrivateKeyBytes } from "@solana/kit";
@@ -74,6 +77,14 @@ describe("Configuration", () => {
7477
assert.strictEqual(NATIVE_MINT_WRAPPING_STRATEGY, "ata");
7578
});
7679

80+
it("Should be able to set the enforce token balance check", () => {
81+
setEnforceTokenBalanceCheck(true);
82+
assert.strictEqual(ENFORCE_TOKEN_BALANCE_CHECK, true);
83+
84+
setEnforceTokenBalanceCheck(false);
85+
assert.strictEqual(ENFORCE_TOKEN_BALANCE_CHECK, false);
86+
});
87+
7788
it("Should be able to reset the configuration", () => {
7889
resetConfiguration();
7990
assert.strictEqual(
@@ -90,5 +101,9 @@ describe("Configuration", () => {
90101
NATIVE_MINT_WRAPPING_STRATEGY,
91102
DEFAULT_NATIVE_MINT_WRAPPING_STRATEGY,
92103
);
104+
assert.strictEqual(
105+
ENFORCE_TOKEN_BALANCE_CHECK,
106+
DEFAULT_ENFORCE_TOKEN_BALANCE_CHECK,
107+
);
93108
});
94109
});

0 commit comments

Comments
 (0)