Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- For swap opportunities, the searcher sdks will now add a memo instruction to the bid transaction if the quote requester so desires. This allows the quote requester to track which on-chain transactions correspond to quotes they requested. [458](https://github.com/pyth-network/per/pull/458)

### Fixed

- For swap opportunities, when a user wants to swap SOL but doesn't have enough funds, the sdk will never try to wrap (on behalf of the user) an amount exceeding the SOL balance of the user.

## [Rust: 0.7.0, Python 0.22.0, Javascript 0.23.0] - 2025-03-25

### Changed
Expand Down
18 changes: 9 additions & 9 deletions auction-server/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,21 +167,21 @@ impl std::fmt::Display for InstructionError {
write!(
f,
"Invalid from account in sol transfer instruction. Expected: {:?} found: {:?}",
found, expected
expected, found
)
}
InstructionError::InvalidToAccountTransferInstruction { expected, found } => {
write!(
f,
"Invalid to account in sol transfer instruction. Expected: {:?} found: {:?}",
found, expected
expected, found
)
}
InstructionError::InvalidAmountTransferInstruction { expected, found } => {
write!(
f,
"Invalid amount in sol transfer instruction. Expected: {:?} found: {:?}",
found, expected
expected, found
)
}
InstructionError::InvalidSyncNativeInstructionCount(address) => {
Expand All @@ -208,42 +208,42 @@ impl std::fmt::Display for InstructionError {
write!(
f,
"Invalid destination account in close account instruction. Expected: {:?} found: {:?}",
found, expected
expected, found
)
}
InstructionError::InvalidOwnerCloseAccountInstruction { expected, found } => {
write!(
f,
"Invalid owner in close account instruction. Expected: {:?} found: {:?}",
found, expected
expected, found
)
}
InstructionError::InvalidMintInCreateAtaInstruction { expected, found } => {
write!(
f,
"Invalid mint in create associated token account instruction. Expected: {:?} found: {:?}",
found, expected
expected, found
)
}
InstructionError::InvalidOwnerInCreateAtaInstruction { expected, found } => {
write!(
f,
"Invalid owner in create associated token account instruction. Expected: {:?} found: {:?}",
found, expected
expected, found
)
}
InstructionError::InvalidPayerInCreateAtaInstruction { expected, found } => {
write!(
f,
"Invalid payer in create associated token account instruction. Expected: {:?} found: {:?}",
found, expected
expected, found
)
}
InstructionError::InvalidTokenProgramInCreateAtaInstruction { expected, found } => {
write!(
f,
"Invalid token program in create associated token account instruction. Expected: {:?} found: {:?}",
found, expected
expected, found
Comment thread
guibescos marked this conversation as resolved.
)
}
InstructionError::InvalidSystemProgramInCreateAtaInstruction(program) => {
Expand Down
32 changes: 22 additions & 10 deletions auction-server/src/auction/service/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,7 @@ impl Service<Svm> {
tx: &VersionedTransaction,
swap_data: &express_relay_svm::SwapArgs,
swap_accounts: &SwapAccounts,
opportunity_swap_data: &OpportunitySvmProgramSwap,
) -> Result<(), RestError> {
let transfer_instructions = self.extract_transfer_instructions(tx).await?;
if transfer_instructions.len() > 1 {
Expand All @@ -845,6 +846,11 @@ impl Service<Svm> {

// User have to wrap Sol
if swap_accounts.mint_user == spl_token::native_mint::id() {
// Sometimes the user doesn't have enough SOL, but we want the transaction to fail in the Express Relay program with InsufficientUserFunds
// Therefore we allow the user to wrap less SOL than needed so it doesn't fail in the transfer instruction
let amount_user_to_wrap =
opportunity_swap_data.get_user_amount_to_wrap(swap_data.amount_user);

if transfer_instructions.len() != 1 {
return Err(RestError::InvalidInstruction(
None,
Expand Down Expand Up @@ -874,11 +880,14 @@ impl Service<Svm> {
},
));
}
if swap_data.amount_user != transfer_instruction.lamports {
// todo: remove swap_data.amount_user != transfer_instruction.lamports once searchers have updated their sdk
if swap_data.amount_user != transfer_instruction.lamports
&& amount_user_to_wrap != transfer_instruction.lamports
{
return Err(RestError::InvalidInstruction(
Some(transfer_instruction.index),
InstructionError::InvalidAmountTransferInstruction {
expected: swap_data.amount_user,
expected: amount_user_to_wrap,
found: transfer_instruction.lamports,
},
));
Expand Down Expand Up @@ -1300,8 +1309,9 @@ impl Service<Svm> {
tx: &VersionedTransaction,
swap_data: &express_relay_svm::SwapArgs,
swap_accounts: &SwapAccounts,
opportunity_swap_data: &OpportunitySvmProgramSwap,
) -> Result<(), RestError> {
self.check_transfer_instruction(tx, swap_data, swap_accounts)
self.check_transfer_instruction(tx, swap_data, swap_accounts, opportunity_swap_data)
.await?;
if swap_accounts.mint_user == spl_token::native_mint::id() {
// User has to wrap Sol
Expand Down Expand Up @@ -1410,6 +1420,7 @@ impl Service<Svm> {
&bid_data.transaction,
&swap_data,
&swap_accounts,
opportunity_swap_data,
)
.await?;

Expand Down Expand Up @@ -1831,6 +1842,7 @@ mod tests {
AccountMeta,
Instruction,
},
native_token::LAMPORTS_PER_SOL,
packet::PACKET_DATA_SIZE,
pubkey::Pubkey,
signature::Keypair,
Expand Down Expand Up @@ -1985,7 +1997,7 @@ mod tests {
token_program_searcher: spl_token::id(),
fee_token: fee_token.clone(),
referral_fee_bps,
user_mint_user_balance: 0,
user_mint_user_balance: LAMPORTS_PER_SOL,
token_account_initialization_configs:
TokenAccountInitializationConfigs::searcher_payer(),
memo: None,
Expand Down Expand Up @@ -2021,7 +2033,7 @@ mod tests {
token_program_searcher: spl_token::id(),
fee_token: fee_token.clone(),
referral_fee_bps,
user_mint_user_balance: 0,
user_mint_user_balance: LAMPORTS_PER_SOL,
token_account_initialization_configs:
TokenAccountInitializationConfigs::searcher_payer(),
memo: None,
Expand Down Expand Up @@ -2057,7 +2069,7 @@ mod tests {
token_program_searcher: spl_token::id(),
fee_token: fee_token.clone(),
referral_fee_bps,
user_mint_user_balance: 0,
user_mint_user_balance: LAMPORTS_PER_SOL,
token_account_initialization_configs: TokenAccountInitializationConfigs {
user_ata_mint_user: TokenAccountInitializationConfig::SearcherPayer,
..TokenAccountInitializationConfigs::searcher_payer()
Expand Down Expand Up @@ -2095,7 +2107,7 @@ mod tests {
token_program_searcher: spl_token::id(),
fee_token: fee_token.clone(),
referral_fee_bps,
user_mint_user_balance: 0,
user_mint_user_balance: LAMPORTS_PER_SOL,
token_account_initialization_configs:
TokenAccountInitializationConfigs::searcher_payer(),
memo: None,
Expand Down Expand Up @@ -2140,7 +2152,7 @@ mod tests {
token_program_searcher: spl_token::id(),
fee_token: fee_token.clone(),
referral_fee_bps,
user_mint_user_balance: 0,
user_mint_user_balance: LAMPORTS_PER_SOL,
token_account_initialization_configs:
TokenAccountInitializationConfigs::searcher_payer(),
memo: None,
Expand Down Expand Up @@ -2177,7 +2189,7 @@ mod tests {
token_program_searcher: spl_token::id(),
fee_token: fee_token.clone(),
referral_fee_bps,
user_mint_user_balance: 0,
user_mint_user_balance: LAMPORTS_PER_SOL,
token_account_initialization_configs: TokenAccountInitializationConfigs {
user_ata_mint_user: TokenAccountInitializationConfig::UserPayer,
user_ata_mint_searcher: TokenAccountInitializationConfig::UserPayer,
Expand Down Expand Up @@ -2216,7 +2228,7 @@ mod tests {
token_program_searcher: spl_token::id(),
fee_token,
referral_fee_bps,
user_mint_user_balance: 0,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

how were all these passing previously?

Copy link
Copy Markdown
Contributor Author

@guibescos guibescos Mar 31, 2025

Choose a reason for hiding this comment

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

the user_mint_user_balance wasn't used for anything in verification nor in the sdks before this PR

user_mint_user_balance: LAMPORTS_PER_SOL,
token_account_initialization_configs:
TokenAccountInitializationConfigs::searcher_payer(),
memo: Some("memo".to_string()),
Expand Down
31 changes: 28 additions & 3 deletions auction-server/src/opportunity/entities/opportunity_svm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,21 @@ use {
},
::express_relay::FeeToken as ProgramFeeToken,
express_relay::state::FEE_SPLIT_PRECISION,
express_relay_api_types::{
opportunity as api,
opportunity::QuoteTokensWithTokenPrograms,
express_relay_api_types::opportunity::{
self as api,
QuoteTokensWithTokenPrograms,
},
serde::{
Deserialize,
Serialize,
},
solana_sdk::{
clock::Slot,
program_pack::Pack,
pubkey::Pubkey,
rent::Rent,
},
spl_token_2022::state::Account as TokenAccount,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this seems fine since you're using the token 2022 token account (which is greater than 165 bytes, i think). i don't think there's any real edge cases, this seems to be overcompensating for the user in the case that they have enough SOL to pay for legacy token accounts but not for token 2022 token accounts. but probably worth a comment just noting this down for devX reasons

Copy link
Copy Markdown
Contributor Author

@guibescos guibescos Mar 31, 2025

Choose a reason for hiding this comment

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

it's quite tricky because TokenAccount::LEN is actually 165, so this code might not work for token22, won't fix now but added a todo

std::ops::Deref,
time::{
Duration,
Expand Down Expand Up @@ -277,6 +280,28 @@ pub fn get_opportunity_swap_data(opp: &OpportunitySvm) -> &OpportunitySvmProgram
}
}

impl OpportunitySvmProgramSwap {
pub fn get_user_amount_to_wrap(&self, amount_user: u64) -> u64 {
let number_of_atas_paid_by_user = [
&self.token_account_initialization_configs.user_ata_mint_user,
&self
.token_account_initialization_configs
.user_ata_mint_searcher,
]
.iter()
.filter(|&&config| matches!(config, TokenAccountInitializationConfig::UserPayer))
.count();

std::cmp::min(
amount_user,
self.user_mint_user_balance.saturating_sub(
number_of_atas_paid_by_user as u64
* Rent::default().minimum_balance(TokenAccount::LEN), // todo: token2022 accounts can be bigger than this, this hack might not work for them
),
)
}
}

impl From<TokenAccountInitializationConfig> for api::TokenAccountInitializationConfig {
fn from(val: TokenAccountInitializationConfig) -> Self {
match val {
Expand Down
1 change: 1 addition & 0 deletions sdk/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,7 @@ export class Client {
opportunity.token_account_initialization_configs.user_ata_mint_user,
},
memo: opportunity.memo ?? undefined,
userMintUserBalance: new anchor.BN(opportunity.user_mint_user_balance),
};
} else {
console.warn("Unsupported opportunity", opportunity);
Expand Down
69 changes: 48 additions & 21 deletions sdk/js/src/svm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function getExpressRelayProgram(chain: string): PublicKey {
}

export const FEE_SPLIT_PRECISION = new anchor.BN(10000);
const RENT_TOKEN_ACCOUNT_LAMPORTS = 2039280;
Comment thread
guibescos marked this conversation as resolved.

export function getConfigRouterPda(
chain: string,
Expand Down Expand Up @@ -250,6 +251,7 @@ function extractSwapInfo(swapOpportunity: OpportunitySvmSwap): {
tokenProgramSearcher: PublicKey;
tokenInitializationConfigs: TokenAccountInitializationConfigs;
memo?: string;
userMintUserBalance: anchor.BN;
Comment thread
guibescos marked this conversation as resolved.
} {
const tokenProgramSearcher = swapOpportunity.tokens.tokenProgramSearcher;
const tokenProgramUser = swapOpportunity.tokens.tokenProgramUser;
Expand All @@ -263,6 +265,7 @@ function extractSwapInfo(swapOpportunity: OpportunitySvmSwap): {
const router = swapOpportunity.routerAccount;
const tokenInitializationConfigs = swapOpportunity.tokenInitializationConfigs;
const memo = swapOpportunity.memo;
const userMintUserBalance = swapOpportunity.userMintUserBalance;
return {
searcherToken,
tokenProgramSearcher,
Expand All @@ -274,6 +277,7 @@ function extractSwapInfo(swapOpportunity: OpportunitySvmSwap): {
router,
tokenInitializationConfigs,
memo,
userMintUserBalance,
};
}

Expand Down Expand Up @@ -426,6 +430,27 @@ function getBidAmountIncludingFees(
return bidAmount;
}

function getUserAmountToWrap(
amountUser: anchor.BN,
userMintUserBalance: anchor.BN,
tokenAccountInitializationConfigs: TokenAccountInitializationConfigs,
): anchor.BN {
const numberOfAtasPaidByUser = [
tokenAccountInitializationConfigs.userAtaMintUser,
tokenAccountInitializationConfigs.userAtaMintSearcher,
].filter((config) => config === "user_payer").length;

return anchor.BN.min(
amountUser,
anchor.BN.max(
userMintUserBalance.sub(
new anchor.BN(numberOfAtasPaidByUser * RENT_TOKEN_ACCOUNT_LAMPORTS),
),
new anchor.BN(0),
),
);
}

export async function constructSwapBid(
tx: Transaction,
searcher: PublicKey,
Expand All @@ -436,8 +461,13 @@ export async function constructSwapBid(
feeReceiverRelayer: PublicKey,
relayerSigner: PublicKey,
): Promise<BidSvmSwap> {
const { userToken, searcherToken, user, tokenInitializationConfigs } =
extractSwapInfo(swapOpportunity);
const {
userToken,
searcherToken,
user,
tokenInitializationConfigs,
userMintUserBalance,
} = extractSwapInfo(swapOpportunity);

if (swapOpportunity.memo) {
tx.instructions.push(createMemoInstruction(swapOpportunity.memo));
Expand All @@ -462,27 +492,24 @@ export async function constructSwapBid(
}

if (userToken.equals(NATIVE_MINT)) {
if (swapOpportunity.tokens.type === "searcher_specified") {
tx.instructions.push(
...getWrapSolInstructions(
searcher,
user,
getBidAmountIncludingFees(swapOpportunity, bidAmount),
false,
), // this account creation is handled in the ata initialization section
);
} else {
tx.instructions.push(
...getWrapSolInstructions(
searcher,
user,
new anchor.BN(
const amountUser =
swapOpportunity.tokens.type === "user_specified"
? new anchor.BN(
swapOpportunity.tokens.userTokenAmountIncludingFees.toString(),
),
),
);
}
)
: getBidAmountIncludingFees(swapOpportunity, bidAmount);

const amountUserToWrap = getUserAmountToWrap(
amountUser,
userMintUserBalance,
tokenInitializationConfigs,
);

tx.instructions.push(
...getWrapSolInstructions(searcher, user, amountUserToWrap, false), // this account creation is handled in the ata initialization section
);
}

const swapInstruction = await constructSwapInstruction(
searcher,
swapOpportunity,
Expand Down
Loading
Loading