From edfdcb47ae0d60e5352a02c5b15380c08c1313e1 Mon Sep 17 00:00:00 2001 From: shamardy Date: Thu, 11 Sep 2025 04:05:18 +0300 Subject: [PATCH 1/4] eth/swapv2: validate maker payment offline (no RPC required) --- .../eth/eth_swap_v2/eth_maker_swap_v2.rs | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs index 489d4871b4..9af4e3b594 100644 --- a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs @@ -1,6 +1,4 @@ -use super::{ - validate_amount, validate_from_to_addresses, EthPaymentType, PaymentMethod, PrepareTxDataError, ZERO_VALUE, -}; +use super::{validate_amount, EthPaymentType, PaymentMethod, PrepareTxDataError, ZERO_VALUE}; use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; use crate::eth::{ decode_contract_call, get_function_input_data, u256_from_big_decimal, EthCoin, EthCoinType, SignedEthTx, @@ -18,7 +16,6 @@ use futures::compat::Future01CompatExt; use mm2_err_handle::mm_error::MmError; use mm2_err_handle::prelude::{MapToMmResult, MmResultExt}; use std::convert::TryInto; -use web3::types::TransactionId; const ETH_MAKER_PAYMENT: &str = "ethMakerPayment"; const ERC20_MAKER_PAYMENT: &str = "erc20MakerPayment"; @@ -122,49 +119,68 @@ impl EthCoin { "NFT protocol is not supported for ETH and ERC20 Swaps".to_string(), )); } + let maker_swap_v2_contract = self .swap_v2_contracts .ok_or_else(|| { ValidatePaymentError::InternalError("Expected swap_v2_contracts to be Some, but found None".to_string()) })? .maker_swap_v2_contract; + let taker_secret_hash = args.taker_secret_hash.try_into()?; let maker_secret_hash = args.maker_secret_hash.try_into()?; validate_amount(&args.amount).map_to_mm(ValidatePaymentError::InternalError)?; let swap_id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); - let tx_from_rpc = self - .transaction(TransactionId::Hash(args.maker_payment_tx.tx_hash())) - .await?; - let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { - ValidatePaymentError::TxDoesNotExist(format!( - "Didn't find provided tx {:?} on ETH node", - args.maker_payment_tx.tx_hash() - )) - })?; + let tx = args.maker_payment_tx; let maker_address = public_to_address(args.maker_pub); - validate_from_to_addresses(tx_from_rpc, maker_address, maker_swap_v2_contract).map_mm_err()?; + let taker_address = self.my_addr().await; + + match tx.unsigned().action() { + Action::Call(to) => { + if *to != maker_swap_v2_contract { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx was sent to wrong address, expected {:?}, got {:?}", + maker_swap_v2_contract, to + ))); + } + }, + Action::Create => { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Tx action must be Call, found Create instead".to_string(), + )); + }, + } + + if tx.sender() != maker_address { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx was sent from wrong address, expected {:?}, got {:?}", + maker_address, + tx.sender() + ))); + } let validation_args = { let amount = u256_from_big_decimal(&args.amount, self.decimals).map_mm_err()?; MakerValidationArgs { swap_id, amount, - taker: self.my_addr().await, + taker: taker_address, taker_secret_hash, maker_secret_hash, payment_time_lock: args.time_lock, } }; + match self.coin_type { EthCoinType::Eth => { let function = MAKER_SWAP_V2.function(ETH_MAKER_PAYMENT)?; - let decoded = decode_contract_call(function, &tx_from_rpc.input.0)?; - validate_eth_maker_payment_data(&decoded, &validation_args, function, tx_from_rpc.value)?; + let decoded = decode_contract_call(function, tx.unsigned().data())?; + validate_eth_maker_payment_data(&decoded, &validation_args, function, tx.unsigned().value())?; }, EthCoinType::Erc20 { token_addr, .. } => { let function = MAKER_SWAP_V2.function(ERC20_MAKER_PAYMENT)?; - let decoded = decode_contract_call(function, &tx_from_rpc.input.0)?; + let decoded = decode_contract_call(function, tx.unsigned().data())?; validate_erc20_maker_payment_data(&decoded, &validation_args, function, token_addr)?; }, EthCoinType::Nft { .. } => { @@ -173,6 +189,8 @@ impl EthCoin { )); }, } + + // Offline checks passed; on-chain confirmation happens later. Ok(()) } From eede9be340109ecf35138ba40c1ad71f16297415 Mon Sep 17 00:00:00 2001 From: shamardy Date: Thu, 11 Sep 2025 11:11:47 +0300 Subject: [PATCH 2/4] review fix: perform sender (tx.sender) check before action/ABI validation in `validate_maker_payment_v2_impl` --- .../coins/eth/eth_swap_v2/eth_maker_swap_v2.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs index 9af4e3b594..2aeb839429 100644 --- a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs @@ -133,9 +133,17 @@ impl EthCoin { let swap_id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); let tx = args.maker_payment_tx; + let maker_address = public_to_address(args.maker_pub); - let taker_address = self.my_addr().await; + if tx.sender() != maker_address { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx was sent from wrong address, expected {:?}, got {:?}", + maker_address, + tx.sender() + ))); + } + let taker_address = self.my_addr().await; match tx.unsigned().action() { Action::Call(to) => { if *to != maker_swap_v2_contract { @@ -152,14 +160,6 @@ impl EthCoin { }, } - if tx.sender() != maker_address { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Payment tx was sent from wrong address, expected {:?}, got {:?}", - maker_address, - tx.sender() - ))); - } - let validation_args = { let amount = u256_from_big_decimal(&args.amount, self.decimals).map_mm_err()?; MakerValidationArgs { From c13654bf7dd25c29cb83fe7ba974e3fa1f5d7bc1 Mon Sep 17 00:00:00 2001 From: shamardy Date: Thu, 11 Sep 2025 11:27:13 +0300 Subject: [PATCH 3/4] review fix: reuse `validate_from_to_addresses` with `SignedEthTx`; convert Web3 tx to `SignedEthTx` in taker/NFT paths --- .../eth/eth_swap_v2/eth_maker_swap_v2.rs | 32 +++---------------- .../eth/eth_swap_v2/eth_taker_swap_v2.rs | 12 ++++--- mm2src/coins/eth/eth_swap_v2/mod.rs | 30 +++++++++++------ mm2src/coins/eth/nft_swap_v2/mod.rs | 6 ++-- 4 files changed, 37 insertions(+), 43 deletions(-) diff --git a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs index 2aeb839429..023c97653f 100644 --- a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs @@ -1,4 +1,6 @@ -use super::{validate_amount, EthPaymentType, PaymentMethod, PrepareTxDataError, ZERO_VALUE}; +use super::{ + validate_amount, validate_from_to_addresses, EthPaymentType, PaymentMethod, PrepareTxDataError, ZERO_VALUE, +}; use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; use crate::eth::{ decode_contract_call, get_function_input_data, u256_from_big_decimal, EthCoin, EthCoinType, SignedEthTx, @@ -133,39 +135,15 @@ impl EthCoin { let swap_id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); let tx = args.maker_payment_tx; - let maker_address = public_to_address(args.maker_pub); - if tx.sender() != maker_address { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Payment tx was sent from wrong address, expected {:?}, got {:?}", - maker_address, - tx.sender() - ))); - } - - let taker_address = self.my_addr().await; - match tx.unsigned().action() { - Action::Call(to) => { - if *to != maker_swap_v2_contract { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Payment tx was sent to wrong address, expected {:?}, got {:?}", - maker_swap_v2_contract, to - ))); - } - }, - Action::Create => { - return MmError::err(ValidatePaymentError::WrongPaymentTx( - "Tx action must be Call, found Create instead".to_string(), - )); - }, - } + validate_from_to_addresses(tx, maker_address, maker_swap_v2_contract).map_mm_err()?; let validation_args = { let amount = u256_from_big_decimal(&args.amount, self.decimals).map_mm_err()?; MakerValidationArgs { swap_id, amount, - taker: taker_address, + taker: self.my_addr().await, taker_secret_hash, maker_secret_hash, payment_time_lock: args.time_lock, diff --git a/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs index 42ca6f4a22..a9120852ca 100644 --- a/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs @@ -3,10 +3,10 @@ use super::{ PaymentMethod, PrepareTxDataError, SpendTxSearchParams, ZERO_VALUE, }; use crate::eth::{ - decode_contract_call, get_function_input_data, u256_from_big_decimal, EthCoin, EthCoinType, ParseCoinAssocTypes, - RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs, SignedEthTx, SwapTxTypeWithSecretHash, - TakerPaymentStateV2, TransactionErr, ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs, - TAKER_SWAP_V2, + decode_contract_call, get_function_input_data, signed_tx_from_web3_tx, u256_from_big_decimal, EthCoin, EthCoinType, + ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs, SignedEthTx, + SwapTxTypeWithSecretHash, TakerPaymentStateV2, TransactionErr, ValidateSwapV2TxError, ValidateSwapV2TxResult, + ValidateTakerFundingArgs, TAKER_SWAP_V2, }; use crate::{ FindPaymentSpendError, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, SearchForFundingSpendErr, @@ -172,7 +172,9 @@ impl EthCoin { )) })?; let taker_address = public_to_address(args.taker_pub); - validate_from_to_addresses(tx_from_rpc, taker_address, taker_swap_v2_contract).map_mm_err()?; + let signed_tx = signed_tx_from_web3_tx(tx_from_rpc.clone()) + .map_err(|err| ValidateSwapV2TxError::WrongPaymentTx(format!("Could not parse tx: {:?}", err)))?; + validate_from_to_addresses(&signed_tx, taker_address, taker_swap_v2_contract).map_mm_err()?; let validation_args = { let dex_fee = u256_from_big_decimal(&args.dex_fee.fee_amount().into(), self.decimals).map_mm_err()?; diff --git a/mm2src/coins/eth/eth_swap_v2/mod.rs b/mm2src/coins/eth/eth_swap_v2/mod.rs index 0e1eb8ffd2..552b6ddce5 100644 --- a/mm2src/coins/eth/eth_swap_v2/mod.rs +++ b/mm2src/coins/eth/eth_swap_v2/mod.rs @@ -6,13 +6,13 @@ use common::now_sec; use derive_more::Display; use enum_derives::EnumFromStringify; use ethabi::{Contract, Token}; -use ethcore_transaction::SignedTransaction as SignedEthTx; +use ethcore_transaction::{Action, SignedTransaction as SignedEthTx}; use ethereum_types::{Address, H256, U256}; use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::{MmError, MmResult}; use mm2_number::BigDecimal; use num_traits::Signed; -use web3::types::{Transaction as Web3Tx, TransactionId}; +use web3::types::TransactionId; pub(crate) mod eth_maker_swap_v2; pub(crate) mod eth_taker_swap_v2; @@ -175,21 +175,33 @@ impl EthCoin { } pub(crate) fn validate_from_to_addresses( - tx_from_rpc: &Web3Tx, + signed_tx: &SignedEthTx, expected_from: Address, expected_to: Address, ) -> Result<(), MmError> { - if tx_from_rpc.from != Some(expected_from) { + if signed_tx.sender() != expected_from { return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( - "Payment tx {tx_from_rpc:?} was sent from wrong address, expected {expected_from:?}" + "Payment tx {signed_tx:?} was sent from wrong address, expected {expected_from:?}" ))); } + // (in NFT case) as NFT owner calls "safeTransferFrom" directly, then in Transaction 'to' field we expect token_address - if tx_from_rpc.to != Some(expected_to) { - return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( - "Payment tx {tx_from_rpc:?} was sent to wrong address, expected {expected_to:?}", - ))); + match signed_tx.unsigned().action() { + Action::Call(to) => { + if *to != expected_to { + return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( + "Payment tx was sent to wrong address, expected {:?}, got {:?}", + expected_to, to + ))); + } + }, + Action::Create => { + return MmError::err(ValidatePaymentV2Err::WrongPaymentTx( + "Tx action must be Call, found Create instead".to_string(), + )); + }, } + Ok(()) } diff --git a/mm2src/coins/eth/nft_swap_v2/mod.rs b/mm2src/coins/eth/nft_swap_v2/mod.rs index 24b1687fbd..a55c66a336 100644 --- a/mm2src/coins/eth/nft_swap_v2/mod.rs +++ b/mm2src/coins/eth/nft_swap_v2/mod.rs @@ -8,7 +8,7 @@ use mm2_number::BigDecimal; use num_traits::Signed; use web3::types::TransactionId; -use super::ContractType; +use super::{signed_tx_from_web3_tx, ContractType}; use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; use crate::eth::eth_swap_v2::{validate_from_to_addresses, PaymentMethod, PrepareTxDataError, ZERO_VALUE}; use crate::eth::{ @@ -92,7 +92,9 @@ impl EthCoin { args.maker_payment_tx.tx_hash() )) })?; - validate_from_to_addresses(tx_from_rpc, maker_address, *token_address).map_mm_err()?; + let signed_tx = signed_tx_from_web3_tx(tx_from_rpc.clone()) + .map_err(|err| ValidatePaymentError::WrongPaymentTx(format!("Could not parse tx: {:?}", err)))?; + validate_from_to_addresses(&signed_tx, maker_address, *token_address).map_mm_err()?; let (decoded, bytes_index) = get_decoded_tx_data_and_bytes_index(contract_type, &tx_from_rpc.input.0)?; From 1f3d813c7907b0e6ab739cb3c0d88b7be9778fb0 Mon Sep 17 00:00:00 2001 From: shamardy Date: Thu, 11 Sep 2025 12:01:18 +0300 Subject: [PATCH 4/4] review fix: display address instead of using debug --- mm2src/coins/eth/eth_swap_v2/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/eth/eth_swap_v2/mod.rs b/mm2src/coins/eth/eth_swap_v2/mod.rs index 552b6ddce5..ad01de3b29 100644 --- a/mm2src/coins/eth/eth_swap_v2/mod.rs +++ b/mm2src/coins/eth/eth_swap_v2/mod.rs @@ -1,4 +1,5 @@ use crate::eth::{decode_contract_call, signed_tx_from_web3_tx, EthCoin, EthCoinType, Transaction, TransactionErr}; +use crate::hd_wallet::DisplayAddress; use crate::{FindPaymentSpendError, MarketCoinOps}; use common::executor::Timer; use common::log::{error, info}; @@ -181,7 +182,8 @@ pub(crate) fn validate_from_to_addresses( ) -> Result<(), MmError> { if signed_tx.sender() != expected_from { return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( - "Payment tx {signed_tx:?} was sent from wrong address, expected {expected_from:?}" + "Payment tx {signed_tx:?} was sent from wrong address, expected {}", + expected_from.display_address() ))); } @@ -190,8 +192,9 @@ pub(crate) fn validate_from_to_addresses( Action::Call(to) => { if *to != expected_to { return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( - "Payment tx was sent to wrong address, expected {:?}, got {:?}", - expected_to, to + "Payment tx was sent to wrong address, expected {}, got {}", + expected_to.display_address(), + to.display_address() ))); } },