Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add good errors to the svm program #406

Merged
merged 9 commits into from
Mar 3, 2025
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
2 changes: 2 additions & 0 deletions contracts/svm/programs/express_relay/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ pub enum ErrorCode {
InsufficientRent,
#[msg("Invalid referral fee")]
InvalidReferralFee,
#[msg("Insufficient user funds")]
InsufficientUserFunds,
}
19 changes: 15 additions & 4 deletions contracts/svm/programs/express_relay/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,22 @@ pub mod express_relay {
pub fn swap(ctx: Context<Swap>, data: SwapArgs) -> Result<()> {
check_deadline(data.deadline)?;

let PostFeeSwapArgs {
amount_searcher_after_fees,
amount_user_after_fees,
} = ctx.accounts.transfer_swap_fees(&data)?;
let (
transfer_swap_fees,
PostFeeSwapArgs {
amount_searcher_after_fees,
amount_user_after_fees,
},
) = ctx.accounts.compute_swap_fees(&data)?;

// We want the program to never fail in the CPI transfers after `check_enough_balances`.
// This guarantees auction server than when a simulated transaction fails with the InsufficientUserFunds error,
// the transaction was correct and executable other than the user having insufficient balance.
// The checks above this line combined with `check_enough_balances` should guarantee that the CPIs will never fail
// and `check_enough_balances` should be the last check.
ctx.accounts.check_enough_balances(&data)?;

ctx.accounts.transfer_swap_fees_cpi(&transfer_swap_fees)?;
// Transfer tokens
transfer_token_if_needed(
&ctx.accounts.searcher_ta_mint_searcher,
Expand Down
53 changes: 33 additions & 20 deletions contracts/svm/programs/express_relay/src/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,29 @@ pub struct PostFeeSwapArgs {


impl<'info> Swap<'info> {
pub fn transfer_swap_fees(&self, args: &SwapArgs) -> Result<PostFeeSwapArgs> {
let (post_fee_swap_args, transfer_swap_fees) = match args.fee_token {
pub fn compute_swap_fees<'a>(
&'a self,
args: &SwapArgs,
) -> Result<(TransferSwapFeeArgs<'info, 'a>, PostFeeSwapArgs)> {
match args.fee_token {
FeeToken::Searcher => {
let SwapFeesWithRemainingAmount {
fees,
remaining_amount,
} = self
.express_relay_metadata
.compute_swap_fees(args.referral_fee_bps, args.amount_searcher)?;
(
PostFeeSwapArgs {
amount_searcher_after_fees: remaining_amount,
amount_user_after_fees: args.amount_user,
},
Ok((
TransferSwapFeeArgs {
fees,
from: &self.searcher_ta_mint_searcher,
authority: &self.searcher,
},
)
PostFeeSwapArgs {
amount_searcher_after_fees: remaining_amount,
amount_user_after_fees: args.amount_user,
},
))
}
FeeToken::User => {
let SwapFeesWithRemainingAmount {
Expand All @@ -50,26 +53,22 @@ impl<'info> Swap<'info> {
} = self
.express_relay_metadata
.compute_swap_fees(args.referral_fee_bps, args.amount_user)?;
(
PostFeeSwapArgs {
amount_searcher_after_fees: args.amount_searcher,
amount_user_after_fees: remaining_amount,
},
Ok((
TransferSwapFeeArgs {
fees,
from: &self.user_ata_mint_user,
authority: &self.user,
},
)
PostFeeSwapArgs {
amount_searcher_after_fees: args.amount_searcher,
amount_user_after_fees: remaining_amount,
},
))
}
};

self.transfer_swap_fees_cpi(&transfer_swap_fees)?;

Ok(post_fee_swap_args)
}
}

fn transfer_swap_fees_cpi<'a>(&self, args: &TransferSwapFeeArgs<'info, 'a>) -> Result<()> {
pub fn transfer_swap_fees_cpi<'a>(&self, args: &TransferSwapFeeArgs<'info, 'a>) -> Result<()> {
self.transfer_swap_fee_cpi(args.fees.router_fee, &self.router_fee_receiver_ta, args)?;
self.transfer_swap_fee_cpi(args.fees.relayer_fee, &self.relayer_fee_receiver_ata, args)?;
self.transfer_swap_fee_cpi(
Expand All @@ -96,6 +95,20 @@ impl<'info> Swap<'info> {
)?;
Ok(())
}

pub fn check_enough_balances(&self, args: &SwapArgs) -> Result<()> {
require_gte!(
self.searcher_ta_mint_searcher.amount,
args.amount_searcher,
ErrorCode::InsufficientSearcherFunds
);
require_gte!(
self.user_ata_mint_user.amount,
args.amount_user,
ErrorCode::InsufficientUserFunds
);
Ok(())
}
}

pub struct TransferSwapFeeArgs<'info, 'a> {
Expand Down
236 changes: 236 additions & 0 deletions contracts/svm/testing/tests/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1180,4 +1180,240 @@ fn test_swap_fail_wrong_relayer_signer() {
);
}


#[test]
fn test_swap_insufficient_balance_user() {
let SwapSetupResult {
mut svm,
user,
searcher,
token_searcher,
token_user,
router_ta_mint_user,
relayer_signer,
..
} = setup_swap(SwapSetupParams {
platform_fee_bps: 1000,
token_program_searcher: spl_token::ID,
token_decimals_searcher: 6,
token_program_user: spl_token::ID,
token_decimals_user: 6,
});

let express_relay_metadata = get_express_relay_metadata(&mut svm);

// user token fee
let swap_args = SwapArgs {
deadline: svm.get_sysvar::<Clock>().unix_timestamp,
amount_searcher: token_searcher.get_amount_with_decimals(1.),
amount_user: token_user.get_amount_with_decimals(11.), // <--- more than user has
referral_fee_bps: 1500,
fee_token: FeeToken::User,
};

let instructions = build_swap_instructions(
searcher.pubkey(),
user.pubkey(),
None,
None,
router_ta_mint_user,
express_relay_metadata.fee_receiver_relayer,
token_searcher.mint,
token_user.mint,
Some(token_searcher.token_program),
Some(token_user.token_program),
swap_args,
None,
None,
relayer_signer.pubkey(),
);
let result = submit_transaction(
&mut svm,
&instructions,
&searcher,
&[&searcher, &user, &relayer_signer],
)
.unwrap_err();
assert_custom_error(
result.err,
4,
InstructionError::Custom(ErrorCode::InsufficientUserFunds.into()),
);
}

#[test]
fn test_swap_insufficient_balance_searcher() {
let SwapSetupResult {
mut svm,
user,
searcher,
token_searcher,
token_user,
router_ta_mint_searcher,
relayer_signer,
..
} = setup_swap(SwapSetupParams {
platform_fee_bps: 1000,
token_program_searcher: spl_token::ID,
token_decimals_searcher: 6,
token_program_user: spl_token::ID,
token_decimals_user: 6,
});

let express_relay_metadata = get_express_relay_metadata(&mut svm);

// searcher token fee
let swap_args = SwapArgs {
deadline: svm.get_sysvar::<Clock>().unix_timestamp,
amount_searcher: token_searcher.get_amount_with_decimals(11.), // <--- more than searcher has
amount_user: token_user.get_amount_with_decimals(1.),
referral_fee_bps: 1500,
fee_token: FeeToken::Searcher,
};

let instructions = build_swap_instructions(
searcher.pubkey(),
user.pubkey(),
None,
None,
router_ta_mint_searcher,
express_relay_metadata.fee_receiver_relayer,
token_searcher.mint,
token_user.mint,
Some(token_searcher.token_program),
Some(token_user.token_program),
swap_args,
None,
None,
relayer_signer.pubkey(),
);
let result = submit_transaction(
&mut svm,
&instructions,
&searcher,
&[&searcher, &user, &relayer_signer],
)
.unwrap_err();

assert_custom_error(
result.err,
4,
InstructionError::Custom(ErrorCode::InsufficientSearcherFunds.into()),
);
}

#[test]
fn test_swap_insufficient_balance_both_user_and_searcher() {
let SwapSetupResult {
mut svm,
user,
searcher,
token_searcher,
token_user,
router_ta_mint_user,
relayer_signer,
..
} = setup_swap(SwapSetupParams {
platform_fee_bps: 1000,
token_program_searcher: spl_token::ID,
token_decimals_searcher: 6,
token_program_user: spl_token::ID,
token_decimals_user: 6,
});

let express_relay_metadata = get_express_relay_metadata(&mut svm);

// user token fee
let swap_args = SwapArgs {
deadline: svm.get_sysvar::<Clock>().unix_timestamp,
amount_searcher: token_searcher.get_amount_with_decimals(11.), // <--- more than searcher has
amount_user: token_user.get_amount_with_decimals(11.), // <--- more than user has
referral_fee_bps: 1500,
fee_token: FeeToken::User,
};

let instructions = build_swap_instructions(
searcher.pubkey(),
user.pubkey(),
None,
None,
router_ta_mint_user,
express_relay_metadata.fee_receiver_relayer,
token_searcher.mint,
token_user.mint,
Some(token_searcher.token_program),
Some(token_user.token_program),
swap_args,
None,
None,
relayer_signer.pubkey(),
);
let result = submit_transaction(
&mut svm,
&instructions,
&searcher,
&[&searcher, &user, &relayer_signer],
)
.unwrap_err();
assert_custom_error(
result.err,
4,
InstructionError::Custom(ErrorCode::InsufficientSearcherFunds.into()),
);
}

#[test]
fn test_swap_exact_balance_user() {
let SwapSetupResult {
mut svm,
user,
searcher,
token_searcher,
token_user,
router_ta_mint_user,
relayer_signer,
..
} = setup_swap(SwapSetupParams {
platform_fee_bps: 1000,
token_program_searcher: spl_token::ID,
token_decimals_searcher: 6,
token_program_user: spl_token::ID,
token_decimals_user: 6,
});

let express_relay_metadata = get_express_relay_metadata(&mut svm);

// user token fee
let swap_args = SwapArgs {
deadline: svm.get_sysvar::<Clock>().unix_timestamp,
amount_searcher: token_searcher.get_amount_with_decimals(10.),
amount_user: token_user.get_amount_with_decimals(10.), // exact balance of user
referral_fee_bps: 1500,
fee_token: FeeToken::User,
};

let instructions = build_swap_instructions(
searcher.pubkey(),
user.pubkey(),
None,
None,
router_ta_mint_user,
express_relay_metadata.fee_receiver_relayer,
token_searcher.mint,
token_user.mint,
Some(token_searcher.token_program),
Some(token_user.token_program),
swap_args,
None,
None,
relayer_signer.pubkey(),
);
submit_transaction(
&mut svm,
&instructions,
&searcher,
&[&searcher, &user, &relayer_signer],
)
.unwrap();
}
// TODO Add test for having no relayer signer
Loading