diff --git a/Cargo.lock b/Cargo.lock index 36fb1396..090cecad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -759,6 +759,7 @@ dependencies = [ "solana-transaction-status", "spl-associated-token-account 4.0.0", "spl-token 6.0.0", + "spl-token-2022 5.0.2", "sqlx", "strum", "time", @@ -7402,6 +7403,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "solana-zk-sdk" +version = "2.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cd4e64123a40e1d38ed74cd6de4a4ee1dc02b6e91dac404cf96cc1f9b046e0" +dependencies = [ + "aes-gcm-siv 0.11.1", + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "itertools 0.12.1", + "lazy_static", + "merlin", + "num-derive", + "num-traits", + "rand 0.7.3", + "serde", + "serde_derive", + "serde_json", + "sha3 0.9.1", + "solana-program 2.0.23", + "solana-sdk 2.0.23", + "subtle", + "thiserror", + "zeroize", +] + [[package]] name = "solana-zk-token-sdk" version = "1.18.26" @@ -7643,6 +7673,20 @@ dependencies = [ "spl-program-error 0.5.0", ] +[[package]] +name = "spl-pod" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e38c99f09d58df06ca9a29fc0211786a4c34f4d099c1df27b1abaa206569a4" +dependencies = [ + "borsh 1.5.3", + "bytemuck", + "bytemuck_derive", + "solana-program 2.0.23", + "solana-zk-sdk", + "spl-program-error 0.5.0", +] + [[package]] name = "spl-program-error" version = "0.4.4" @@ -7709,6 +7753,20 @@ dependencies = [ "spl-type-length-value 0.5.0", ] +[[package]] +name = "spl-tlv-account-resolution" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ffc5c76926c1cbc8d92ac1d9341328308d15bae04a67da5a226e36d5bc395a" +dependencies = [ + "bytemuck", + "solana-program 2.0.23", + "spl-discriminator 0.3.0", + "spl-pod 0.4.0", + "spl-program-error 0.5.0", + "spl-type-length-value 0.6.0", +] + [[package]] name = "spl-token" version = "4.0.3" @@ -7787,6 +7845,68 @@ dependencies = [ "thiserror", ] +[[package]] +name = "spl-token-2022" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b788a8c34a917b68b4ed2cdec255d03cc09ccba21545dac39c08a97fce640f" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program 2.0.23", + "solana-security-txt", + "solana-zk-sdk", + "spl-memo 5.0.0", + "spl-pod 0.4.0", + "spl-token 6.0.0", + "spl-token-confidential-transfer-ciphertext-arithmetic", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface 0.4.2", + "spl-token-metadata-interface 0.5.1", + "spl-transfer-hook-interface 0.8.2", + "spl-type-length-value 0.6.0", + "thiserror", +] + +[[package]] +name = "spl-token-confidential-transfer-ciphertext-arithmetic" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48df72fb98b4069979aa4806d4a634ad6f08cb0358e732e6fbac231c5dc075bd" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "solana-curve25519", + "solana-zk-sdk", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae2ce92a0b9673c44207b21d99526b96d557d5a25752f36c38fae37c49129c3b" +dependencies = [ + "bytemuck", + "solana-curve25519", + "solana-zk-sdk", + "thiserror", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-generation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216517cc8cd26dfe768521542f221f91049be102d1eefd8054cde881d1b5d267" +dependencies = [ + "curve25519-dalek", + "solana-zk-sdk", + "thiserror", +] + [[package]] name = "spl-token-group-interface" version = "0.2.5" @@ -7813,6 +7933,19 @@ dependencies = [ "spl-program-error 0.5.0", ] +[[package]] +name = "spl-token-group-interface" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c54728921a6f180525c4f3a4cc3b231db8da940681c8009259faa65d84c6196" +dependencies = [ + "bytemuck", + "solana-program 2.0.23", + "spl-discriminator 0.3.0", + "spl-pod 0.4.0", + "spl-program-error 0.5.0", +] + [[package]] name = "spl-token-metadata-interface" version = "0.3.5" @@ -7841,6 +7974,20 @@ dependencies = [ "spl-type-length-value 0.5.0", ] +[[package]] +name = "spl-token-metadata-interface" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c7a6d2a2a4f23c8f051ba2f7edb5689f1b534b4126bd9b4ae4177685e9a341" +dependencies = [ + "borsh 1.5.3", + "solana-program 2.0.23", + "spl-discriminator 0.3.0", + "spl-pod 0.4.0", + "spl-program-error 0.5.0", + "spl-type-length-value 0.6.0", +] + [[package]] name = "spl-transfer-hook-interface" version = "0.6.5" @@ -7873,6 +8020,22 @@ dependencies = [ "spl-type-length-value 0.5.0", ] +[[package]] +name = "spl-transfer-hook-interface" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b82e5ad62ad342d8fbb907b2a7aede7a5a258ce16366c26a686acc1df431fc63" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program 2.0.23", + "spl-discriminator 0.3.0", + "spl-pod 0.4.0", + "spl-program-error 0.5.0", + "spl-tlv-account-resolution 0.8.1", + "spl-type-length-value 0.6.0", +] + [[package]] name = "spl-type-length-value" version = "0.4.6" @@ -7899,6 +8062,19 @@ dependencies = [ "spl-program-error 0.5.0", ] +[[package]] +name = "spl-type-length-value" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69c08deb8332a6c051f6e846b5e9c962695295dd97e0e151dccf3ef85dcf01b4" +dependencies = [ + "bytemuck", + "solana-program 2.0.23", + "spl-discriminator 0.3.0", + "spl-pod 0.4.0", + "spl-program-error 0.5.0", +] + [[package]] name = "sqlformat" version = "0.2.6" diff --git a/auction-server/Cargo.toml b/auction-server/Cargo.toml index 664bdc6a..5c64fb1e 100644 --- a/auction-server/Cargo.toml +++ b/auction-server/Cargo.toml @@ -57,6 +57,7 @@ strum.workspace = true spl-associated-token-account = { workspace = true } spl-token = { workspace = true } mockall_double = "0.3.1" +spl-token-2022 = "5.0.0" [dev-dependencies] mockall = "0.13.1" diff --git a/auction-server/src/auction/service/verification.rs b/auction-server/src/auction/service/verification.rs index d3418f12..025f1371 100644 --- a/auction-server/src/auction/service/verification.rs +++ b/auction-server/src/auction/service/verification.rs @@ -70,19 +70,26 @@ use { U256, }, }, + express_relay::error::ErrorCode, litesvm::types::FailedTransactionMetadata, solana_sdk::{ address_lookup_table::state::AddressLookupTable, clock::Slot, commitment_config::CommitmentConfig, compute_budget, - instruction::CompiledInstruction, + instruction::{ + CompiledInstruction, + InstructionError, + }, pubkey::Pubkey, signature::Signature, signer::Signer as _, system_instruction::SystemInstruction, system_program, - transaction::VersionedTransaction, + transaction::{ + TransactionError, + VersionedTransaction, + }, }, spl_associated_token_account::{ get_associated_token_address, @@ -1245,7 +1252,19 @@ impl Service { .await; match simulation { Ok(simulation) => { - if simulation.value.err.is_some() { + if let Some(transaction_error) = simulation.value.err { + if let TransactionError::InstructionError(_, instruction_error) = + transaction_error + { + if instruction_error + == InstructionError::Custom(ErrorCode::InsufficientUserFunds.into()) + { + // This path only works as long as none of the other accepted programs use this error number, which is currently true since express relay is the only program in the whitelist that uses anchor error codes + // TODO: Also check the instruction index here so we are sure it's coming from the swap instruction + return Ok(()); + } + } + let msgs = simulation.value.logs.unwrap_or_default(); Err(RestError::SimulationError { result: Default::default(), @@ -1255,6 +1274,7 @@ impl Service { Ok(()) } } + Err(e) => { tracing::error!("Error while simulating swap bid: {:?}", e); Err(RestError::TemporarilyUnavailable) diff --git a/auction-server/src/opportunity/service/check_user_token_account_balance.rs b/auction-server/src/opportunity/service/check_user_token_account_balance.rs new file mode 100644 index 00000000..89e817d7 --- /dev/null +++ b/auction-server/src/opportunity/service/check_user_token_account_balance.rs @@ -0,0 +1,84 @@ +use { + super::{ + get_token_program::GetTokenProgramInput, + ChainTypeSvm, + Service, + }, + crate::{ + api::RestError, + kernel::entities::ChainId, + }, + solana_sdk::{ + commitment_config::CommitmentConfig, + pubkey::Pubkey, + }, + spl_associated_token_account::get_associated_token_address_with_program_id, + spl_token_2022::{ + extension::StateWithExtensions as TokenAccountWithExtensions, + state::Account as TokenAccount, + }, +}; + +pub struct CheckUserTokenBalanceInput { + pub chain_id: ChainId, + pub user: Pubkey, + pub mint_user: Pubkey, + pub amount_user: u64, +} + +impl Service { + pub async fn check_user_token_balance( + &self, + input: CheckUserTokenBalanceInput, + ) -> Result { + let config = self.get_config(&input.chain_id)?; + + let native_amount_user = if input.mint_user == spl_token::native_mint::id() { + config + .rpc_client + .get_account_with_commitment(&input.user, CommitmentConfig::processed()) + .await + .map_err(|err| { + tracing::error!(error = ?err, "Failed to get user wallet"); + RestError::TemporarilyUnavailable + })? + .value + .map(|account| account.lamports) + .unwrap_or_default() + } else { + 0 + }; + + let user_ata_mint_user = get_associated_token_address_with_program_id( + &input.user, + &input.mint_user, + &self + .get_token_program(GetTokenProgramInput { + chain_id: input.chain_id.clone(), + mint: input.mint_user, + }) + .await?, + ); + let amount_user: u64 = config + .rpc_client + .get_account_with_commitment(&user_ata_mint_user, CommitmentConfig::processed()) + .await + .map_err(|err| { + tracing::error!(error = ?err, "Failed to get user token account"); + RestError::TemporarilyUnavailable + })? + .value + .map(|account| { + TokenAccountWithExtensions::::unpack(account.data.as_slice()) + .map_err(|err| { + tracing::error!(error = ?err, "Failed to deserialize user token account"); + RestError::TemporarilyUnavailable + }) + .map(|token_account_with_extensions| token_account_with_extensions.base.amount) + }) + .transpose()? + .unwrap_or_default(); + + Ok(input.amount_user <= amount_user.saturating_add(native_amount_user)) + } +} diff --git a/auction-server/src/opportunity/service/get_quote.rs b/auction-server/src/opportunity/service/get_quote.rs index f78a3899..d7e9e4aa 100644 --- a/auction-server/src/opportunity/service/get_quote.rs +++ b/auction-server/src/opportunity/service/get_quote.rs @@ -29,6 +29,7 @@ use { }, service::{ add_opportunity::AddOpportunityInput, + check_user_token_account_balance::CheckUserTokenBalanceInput, get_express_relay_metadata::GetExpressRelayMetadata, }, }, @@ -526,12 +527,26 @@ impl Service { ), }; - let (transaction, expiration_time) = match input.quote_create.user_wallet_address { - None => (None, None), - Some(_) => ( + let user_has_enough_balance = + if let Some(user_wallet_address) = input.quote_create.user_wallet_address { + self.check_user_token_balance(CheckUserTokenBalanceInput { + chain_id: input.quote_create.chain_id.clone(), + user: user_wallet_address, + mint_user: user_token.token, + amount_user: swap_data.amount_user, + }) + .await? + } else { + false + }; + + let (transaction, expiration_time) = if user_has_enough_balance { + ( Some(winner_bid.chain_data.transaction.clone()), Some(deadline), - ), + ) + } else { + (None, None) }; Ok(entities::Quote { diff --git a/auction-server/src/opportunity/service/mod.rs b/auction-server/src/opportunity/service/mod.rs index 0760fd1a..b98e54b8 100644 --- a/auction-server/src/opportunity/service/mod.rs +++ b/auction-server/src/opportunity/service/mod.rs @@ -63,6 +63,7 @@ pub mod remove_invalid_or_expired_opportunities; pub mod remove_opportunities; pub mod verification; +mod check_user_token_account_balance; mod get_express_relay_metadata; mod get_spoof_info; mod get_token_program;