diff --git a/.github/workflows/ci-pre-commit.yml b/.github/workflows/ci-pre-commit.yml index 56b736778..a23b9fdb1 100644 --- a/.github/workflows/ci-pre-commit.yml +++ b/.github/workflows/ci-pre-commit.yml @@ -42,7 +42,7 @@ jobs: sudo apt-get install -y libudev-dev pkg-config - name: Install Anchor CLI run: | - cargo install --git https://github.com/coral-xyz/anchor avm + cargo install --locked --git https://github.com/coral-xyz/anchor avm avm install 0.31.0 avm use 0.31.0 echo "$HOME/.avm/bin" >> $GITHUB_PATH diff --git a/.github/workflows/test-and-release-svm-contract.yml b/.github/workflows/test-and-release-svm-contract.yml index 0e882cddf..7799fce25 100644 --- a/.github/workflows/test-and-release-svm-contract.yml +++ b/.github/workflows/test-and-release-svm-contract.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v2 - name: Install Solana Verify CLI run: | - cargo install solana-verify --git https://github.com/Ellipsis-Labs/solana-verifiable-build --rev 5ff03e0 + cargo install --locked solana-verify --git https://github.com/solana-foundation/solana-verifiable-build --rev 5ff03e0 - name: Build run: solana-verify build - name: Run tests diff --git a/Cargo.lock b/Cargo.lock index b5821e09a..7658164ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1944,7 +1944,7 @@ dependencies = [ [[package]] name = "express-relay" -version = "0.8.0" +version = "0.9.0" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/contracts/svm/Cargo.lock b/contracts/svm/Cargo.lock index 443b6065d..35ccc065e 100644 --- a/contracts/svm/Cargo.lock +++ b/contracts/svm/Cargo.lock @@ -1649,7 +1649,7 @@ dependencies = [ [[package]] name = "express-relay" -version = "0.8.0" +version = "0.9.0" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/contracts/svm/programs/express_relay/Cargo.toml b/contracts/svm/programs/express_relay/Cargo.toml index cd2f7106d..42e6c673e 100644 --- a/contracts/svm/programs/express_relay/Cargo.toml +++ b/contracts/svm/programs/express_relay/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "express-relay" -version = "0.8.0" +version = "0.9.0" description = "Pyth Express Relay program for handling permissioning and bid distribution" repository = "https://github.com/pyth-network/per" license = "Apache-2.0" diff --git a/contracts/svm/programs/express_relay/src/lib.rs b/contracts/svm/programs/express_relay/src/lib.rs index 15fb765d9..ffd4c4650 100644 --- a/contracts/svm/programs/express_relay/src/lib.rs +++ b/contracts/svm/programs/express_relay/src/lib.rs @@ -29,9 +29,11 @@ use { system_program::System, }, anchor_spl::token_interface::{ + transfer_checked, Mint, TokenAccount, TokenInterface, + TransferChecked, }, }; @@ -182,6 +184,35 @@ pub mod express_relay { ) } + pub fn withdraw_spl_fees(ctx: Context) -> Result<()> { + let amount = ctx.accounts.express_relay_fee_receiver_ata.amount; + if amount == 0 { + return Ok(()); + } + + let metadata_bump = ctx.bumps.express_relay_metadata; + let signer_seeds: &[&[u8]] = &[SEED_METADATA, &[metadata_bump]]; + let signer = &[signer_seeds]; + let cpi_accounts = TransferChecked { + from: ctx + .accounts + .express_relay_fee_receiver_ata + .to_account_info(), + to: ctx.accounts.fee_receiver_admin_ta.to_account_info(), + mint: ctx.accounts.mint_fee.to_account_info(), + authority: ctx.accounts.express_relay_metadata.to_account_info(), + }; + transfer_checked( + CpiContext::new_with_signer( + ctx.accounts.token_program_fee.to_account_info(), + cpi_accounts, + signer, + ), + amount, + ctx.accounts.mint_fee.decimals, + ) + } + pub fn swap_internal(ctx: Context, data: SwapV2Args) -> Result<()> { ctx.accounts.check_raw_constraints(data.fee_token)?; check_deadline(data.deadline)?; @@ -224,7 +255,6 @@ pub mod express_relay { amount_user_after_fees, )?; - Ok(()) } @@ -412,6 +442,35 @@ pub struct WithdrawFees<'info> { pub express_relay_metadata: Account<'info, ExpressRelayMetadata>, } +#[derive(Accounts)] +pub struct WithdrawSplFees<'info> { + pub admin: Signer<'info>, + + #[account(mut, seeds = [SEED_METADATA], bump, has_one = admin)] + pub express_relay_metadata: Account<'info, ExpressRelayMetadata>, + + #[account( + mut, + associated_token::mint = mint_fee, + associated_token::authority = express_relay_metadata, + associated_token::token_program = token_program_fee, + )] + pub express_relay_fee_receiver_ata: Box>, + + /// this can just be any token account for this mint + #[account( + mut, + token::mint = mint_fee, + token::token_program = token_program_fee, + )] + pub fee_receiver_admin_ta: Box>, + + #[account(mint::token_program = token_program_fee)] + pub mint_fee: Box>, + + pub token_program_fee: Interface<'info, TokenInterface>, +} + #[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Copy, Debug)] pub enum FeeToken { Searcher, diff --git a/contracts/svm/testing/src/express_relay/mod.rs b/contracts/svm/testing/src/express_relay/mod.rs index 5112090ab..c13b77e27 100644 --- a/contracts/svm/testing/src/express_relay/mod.rs +++ b/contracts/svm/testing/src/express_relay/mod.rs @@ -9,3 +9,4 @@ pub mod set_swap_platform_fee; pub mod submit_bid; pub mod swap; pub mod withdraw_fees; +pub mod withdraw_spl_fees; diff --git a/contracts/svm/testing/src/express_relay/withdraw_spl_fees.rs b/contracts/svm/testing/src/express_relay/withdraw_spl_fees.rs new file mode 100644 index 000000000..d4ae940b5 --- /dev/null +++ b/contracts/svm/testing/src/express_relay/withdraw_spl_fees.rs @@ -0,0 +1,38 @@ +use { + super::helpers::get_express_relay_metadata_key, + anchor_lang::{ + InstructionData, + ToAccountMetas, + }, + express_relay::accounts::WithdrawSplFees, + solana_sdk::{ + instruction::Instruction, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + }, +}; + +pub fn withdraw_spl_fees_instruction( + admin: &Keypair, + express_relay_fee_receiver_ata: Pubkey, + fee_receiver_admin_ta: Pubkey, + mint_fee: Pubkey, + token_program_fee: Pubkey, +) -> Instruction { + let express_relay_metadata = get_express_relay_metadata_key(); + + Instruction { + program_id: express_relay::id(), + data: express_relay::instruction::WithdrawSplFees {}.data(), + accounts: WithdrawSplFees { + admin: admin.pubkey(), + express_relay_metadata, + express_relay_fee_receiver_ata, + fee_receiver_admin_ta, + mint_fee, + token_program_fee, + } + .to_account_metas(None), + } +} diff --git a/contracts/svm/testing/tests/withdraw_spl_fees.rs b/contracts/svm/testing/tests/withdraw_spl_fees.rs new file mode 100644 index 000000000..3f61c247d --- /dev/null +++ b/contracts/svm/testing/tests/withdraw_spl_fees.rs @@ -0,0 +1,135 @@ +use { + anchor_lang::error::ErrorCode as AnchorErrorCode, + anchor_spl::{ + associated_token::{ + get_associated_token_address_with_program_id, + spl_associated_token_account::instruction::create_associated_token_account_idempotent, + }, + token::spl_token, + }, + solana_sdk::{ + instruction::InstructionError, + signature::Keypair, + signer::Signer, + }, + testing::{ + express_relay::{ + helpers::get_express_relay_metadata_key, + withdraw_spl_fees::withdraw_spl_fees_instruction, + }, + helpers::{ + assert_custom_error, + generate_and_fund_key, + submit_transaction, + }, + setup::{ + setup, + SetupResult, + }, + token::Token, + }, +}; + +#[test] +fn test_withdraw_spl_fees() { + let SetupResult { mut svm, admin, .. } = setup(None).expect("setup failed"); + + let express_relay_metadata = get_express_relay_metadata_key(); + let fee_receiver_admin = generate_and_fund_key(&mut svm); + let fee_token = Token::create_mint(&mut svm, spl_token::ID, 6); + + let express_relay_fee_receiver_ata = get_associated_token_address_with_program_id( + &express_relay_metadata, + &fee_token.mint, + &fee_token.token_program, + ); + let fee_receiver_admin_ata = get_associated_token_address_with_program_id( + &fee_receiver_admin.pubkey(), + &fee_token.mint, + &fee_token.token_program, + ); + + fee_token.airdrop(&mut svm, &express_relay_metadata, 3.5); + + let create_admin_ata_ix = create_associated_token_account_idempotent( + &admin.pubkey(), + &fee_receiver_admin.pubkey(), + &fee_token.mint, + &fee_token.token_program, + ); + let withdraw_ix = withdraw_spl_fees_instruction( + &admin, + express_relay_fee_receiver_ata, + fee_receiver_admin_ata, + fee_token.mint, + fee_token.token_program, + ); + submit_transaction( + &mut svm, + &[create_admin_ata_ix, withdraw_ix], + &admin, + &[&admin], + ) + .unwrap(); + + assert!(Token::token_balance_matches( + &mut svm, + &express_relay_fee_receiver_ata, + 0, + )); + assert!(Token::token_balance_matches( + &mut svm, + &fee_receiver_admin_ata, + fee_token.get_amount_with_decimals(3.5), + )); +} + +#[test] +fn test_withdraw_spl_fees_fail_wrong_admin() { + let SetupResult { mut svm, .. } = setup(None).expect("setup failed"); + let wrong_admin = generate_and_fund_key(&mut svm); + + let express_relay_metadata = get_express_relay_metadata_key(); + let fee_receiver_admin = Keypair::new(); + let fee_token = Token::create_mint(&mut svm, spl_token::ID, 6); + let express_relay_fee_receiver_ata = get_associated_token_address_with_program_id( + &express_relay_metadata, + &fee_token.mint, + &fee_token.token_program, + ); + let fee_receiver_admin_ata = get_associated_token_address_with_program_id( + &fee_receiver_admin.pubkey(), + &fee_token.mint, + &fee_token.token_program, + ); + fee_token.airdrop(&mut svm, &express_relay_metadata, 1.0); + let create_admin_ata_ix = create_associated_token_account_idempotent( + &wrong_admin.pubkey(), + &fee_receiver_admin.pubkey(), + &fee_token.mint, + &fee_token.token_program, + ); + submit_transaction( + &mut svm, + &[create_admin_ata_ix], + &wrong_admin, + &[&wrong_admin], + ) + .unwrap(); + + let withdraw_ix = withdraw_spl_fees_instruction( + &wrong_admin, + express_relay_fee_receiver_ata, + fee_receiver_admin_ata, + fee_token.mint, + fee_token.token_program, + ); + let tx_result = submit_transaction(&mut svm, &[withdraw_ix], &wrong_admin, &[&wrong_admin]) + .expect_err("Transaction should have failed"); + + assert_custom_error( + tx_result.err, + 0, + InstructionError::Custom(AnchorErrorCode::ConstraintHasOne.into()), + ); +} diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml index 541a5b06c..6904741a3 100644 --- a/sdk/rust/Cargo.toml +++ b/sdk/rust/Cargo.toml @@ -21,5 +21,5 @@ solana-rpc-client = { workspace = true } borsh = { workspace = true } spl-associated-token-account = { workspace = true } spl-token = { workspace = true } -express-relay = { version = "0.8.0", path = "../../contracts/svm/programs/express_relay" } +express-relay = { version = "0.9.0", path = "../../contracts/svm/programs/express_relay" } spl-memo-client = { workspace = true }