diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 6ba9b7ac69..cb9bbbde78 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -1230,6 +1230,8 @@ pub struct SendTakerFundingArgs<'a> { pub maker_pub: &'a [u8], /// DEX fee pub dex_fee: &'a DexFee, + /// The extra fee added to the trading volume to cover the taker payment spend transaction fees + pub taker_payment_spend_fee: BigDecimal, /// Additional reward for maker (premium) pub premium_amount: BigDecimal, /// Actual volume of taker's payment @@ -1266,6 +1268,8 @@ pub struct GenTakerFundingSpendArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { pub taker_payment_time_lock: u64, /// The hash of the secret generated by maker pub maker_secret_hash: &'a [u8], + /// The extra fee added to the trading volume to cover the taker payment spend transaction fees + pub taker_payment_spend_fee: BigDecimal, } /// Helper struct wrapping arguments for [TakerCoinSwapOpsV2::validate_taker_funding] @@ -1280,6 +1284,8 @@ pub struct ValidateTakerFundingArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { pub other_pub: &'a Coin::Pubkey, /// DEX fee amount pub dex_fee: &'a DexFee, + /// The extra fee added to the trading volume to cover the taker payment spend transaction fees + pub taker_payment_spend_fee: BigDecimal, /// Additional reward for maker (premium) pub premium_amount: BigDecimal, /// Actual volume of taker's payment @@ -1388,6 +1394,8 @@ impl From for ValidateSwapV2TxError { /// Enum covering error cases that can happen during taker funding spend preimage validation. #[derive(Debug, Display, EnumFromStringify)] pub enum ValidateTakerFundingSpendPreimageError { + /// Error during conversion of BigDecimal amount to coin's specific monetary units (satoshis, wei, etc.). + NumConversion(String), /// Funding tx has no outputs FundingTxNoOutputs, /// Actual preimage fee is either too high or too small @@ -1408,6 +1416,10 @@ pub enum ValidateTakerFundingSpendPreimageError { Rpc(String), } +impl From for ValidateTakerFundingSpendPreimageError { + fn from(err: NumConversError) -> Self { ValidateTakerFundingSpendPreimageError::NumConversion(err.to_string()) } +} + impl From for ValidateTakerFundingSpendPreimageError { fn from(err: TxGenError) -> Self { ValidateTakerFundingSpendPreimageError::TxGenError(format!("{:?}", err)) } } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index a7cfc25c79..f7facdfd10 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1328,8 +1328,17 @@ async fn gen_taker_funding_spend_preimage( let fee = match fee { FundingSpendFeeSetting::GetFromCoin => { - coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await? + let calculated_fee = coin + .get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await?; + let taker_instructed_fee = + sat_from_big_decimal(&args.taker_payment_spend_fee, coin.as_ref().decimals).mm_err(|e| e.into())?; + // If calculated fee is less than instructed fee, use instructed fee because it was never intended + // to be deposited to us (maker). If the calculated fee is larger, we will incur the fee difference + // just to make sure the transaction is confirmed quickly. + // FIXME: Possible abuse vector is that the taker can always send the fee to be just above 90% of the + // expected fee knowing that the maker will always accept incurring the difference. + calculated_fee.max(taker_instructed_fee) }, FundingSpendFeeSetting::UseExact(f) => f, }; @@ -1419,19 +1428,14 @@ pub async fn validate_taker_funding_spend_preimage( ))); } - let expected_fee = coin - .get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await?; - let actual_fee = funding_amount - payment_amount; + let instructed_fee = + sat_from_big_decimal(&gen_args.taker_payment_spend_fee, coin.as_ref().decimals).mm_err(|e| e.into())?; - let fee_div = expected_fee as f64 / actual_fee as f64; - - // FIXME: Should negotiate the fee beforehand, accept fee >= negotiated_fee - if !(0.9..=1.1).contains(&fee_div) { + if actual_fee < instructed_fee { return MmError::err(ValidateTakerFundingSpendPreimageError::UnexpectedPreimageFee(format!( - "Too large difference between expected {} and actual {} fees", - expected_fee, actual_fee + "A fee of {} was used, less than the agreed upon fee of {}", + actual_fee, instructed_fee ))); } @@ -5196,7 +5200,10 @@ where // FIXME: Add taker payment fee + preimage fee // Qs: // 1- Who pays the maker payment fee? Take in considration that a nicer UX would be to hand the taker the full amount they requested. (so account for maker payment fee and taker claimation fee) - let total_amount = &args.dex_fee.total_spend_amount().to_decimal() + &args.premium_amount + &args.trading_amount; + let total_amount = &args.dex_fee.total_spend_amount().to_decimal() + + &args.premium_amount + + &args.trading_amount + + &args.taker_payment_spend_fee; let SwapPaymentOutputsResult { payment_address, @@ -5288,8 +5295,10 @@ where T: UtxoCommonOps + SwapOps, { let maker_htlc_key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); - let total_expected_amount = - &args.dex_fee.total_spend_amount().to_decimal() + &args.premium_amount + &args.trading_amount; + let total_expected_amount = &args.dex_fee.total_spend_amount().to_decimal() + + &args.premium_amount + + &args.trading_amount + + &args.taker_payment_spend_fee; let expected_amount_sat = sat_from_big_decimal(&total_expected_amount, coin.as_ref().decimals)?; diff --git a/mm2src/mm2_main/src/lp_swap/komodefi.swap_v2.pb.rs b/mm2src/mm2_main/src/lp_swap/komodefi.swap_v2.pb.rs index 130fb2e3e1..da06b76b20 100644 --- a/mm2src/mm2_main/src/lp_swap/komodefi.swap_v2.pb.rs +++ b/mm2src/mm2_main/src/lp_swap/komodefi.swap_v2.pb.rs @@ -53,6 +53,8 @@ pub struct TakerNegotiationData { pub maker_coin_swap_contract: ::core::option::Option<::prost::alloc::vec::Vec>, #[prost(bytes = "vec", optional, tag = "8")] pub taker_coin_swap_contract: ::core::option::Option<::prost::alloc::vec::Vec>, + #[prost(uint64, tag = "9")] + pub taker_payment_spend_fee: u64, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index b21947792a..14615cd2f2 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -21,9 +21,10 @@ use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_libp2p::Secp256k1PubkeySerialize; -use mm2_number::MmNumber; +use mm2_number::{BigDecimal, MmNumber}; use mm2_state_machine::prelude::*; use mm2_state_machine::storable_state_machine::*; +use num_traits::ToPrimitive; use primitives::hash::H256; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use secp256k1::PublicKey; @@ -55,6 +56,7 @@ pub struct StoredNegotiationData { maker_coin_swap_contract: Option, taker_coin_swap_contract: Option, taker_secret_hash: BytesJson, + taker_payment_spend_fee: BigDecimal, } /// Represents events produced by maker swap states. @@ -822,6 +824,8 @@ impl fee, @@ -968,6 +972,14 @@ impl p, @@ -997,6 +1009,7 @@ impl>, taker_coin_swap_contract: Option>, taker_secret_hash: Vec, + taker_payment_spend_fee: BigDecimal, } impl NegotiationData { @@ -1024,6 +1038,7 @@ impl Negotiation maker_coin_swap_contract: self.maker_coin_swap_contract.clone().map(|b| b.into()), taker_coin_swap_contract: self.taker_coin_swap_contract.clone().map(|b| b.into()), taker_secret_hash: self.taker_secret_hash.clone().into(), + taker_payment_spend_fee: self.taker_payment_spend_fee.clone(), } } @@ -1044,6 +1059,7 @@ impl Negotiation maker_coin_swap_contract: None, taker_coin_swap_contract: None, taker_secret_hash: stored.taker_secret_hash.into(), + taker_payment_spend_fee: stored.taker_payment_spend_fee, }) } } @@ -1161,6 +1177,7 @@ impl Box::new(Initialized { maker_coin: Default::default(), @@ -501,6 +507,7 @@ impl Box::new(Negotiated { maker_coin_start_block, @@ -518,6 +526,7 @@ impl Box::new(TakerFundingSent { maker_coin_start_block, taker_coin_start_block, @@ -537,6 +547,7 @@ impl { + TakerSwapEvent::Initialized { + taker_payment_fee, + taker_payment_spend_fee, + .. + } => { let swaps_ctx = SwapsContext::from_ctx(&self.ctx).expect("from_ctx should not fail at this point"); let taker_coin_ticker: String = self.taker_coin.ticker().into(); let new_locked = LockedAmountInfo { swap_uuid: self.uuid, locked_amount: LockedAmount { coin: taker_coin_ticker.clone(), - amount: &(&self.taker_volume + &self.dex_fee.total_spend_amount()) + &self.taker_premium, + amount: &(&self.taker_volume + &self.dex_fee.total_spend_amount()) + + &(&self.taker_premium + &taker_payment_spend_fee.amount.clone().into()), trade_fee: Some(taker_payment_fee.clone().into()), }, }; @@ -839,15 +857,24 @@ impl::DbRepr as StateMachineDbRepr>::Event, ) { match event { - TakerSwapEvent::Initialized { taker_payment_fee, .. } - | TakerSwapEvent::Negotiated { taker_payment_fee, .. } => { + TakerSwapEvent::Initialized { + taker_payment_fee, + taker_payment_spend_fee, + .. + } + | TakerSwapEvent::Negotiated { + taker_payment_fee, + taker_payment_spend_fee, + .. + } => { let swaps_ctx = SwapsContext::from_ctx(&self.ctx).expect("from_ctx should not fail at this point"); let taker_coin_ticker: String = self.taker_coin.ticker().into(); let new_locked = LockedAmountInfo { swap_uuid: self.uuid, locked_amount: LockedAmount { coin: taker_coin_ticker.clone(), - amount: &(&self.taker_volume + &self.dex_fee.total_spend_amount()) + &self.taker_premium, + amount: &(&self.taker_volume + &self.dex_fee.total_spend_amount()) + + &(&self.taker_premium + &taker_payment_spend_fee.amount.into()), trade_fee: Some(taker_payment_fee.into()), }, }; @@ -919,14 +946,23 @@ impl fee, + Err(e) => { + let reason = AbortReason::FailedToGetTakerPaymentSpendFee(e.to_string()); + return Self::change_state(Aborted::new(reason), state_machine).await; + }, + }; - // FIXME: This is not taker payment tx fee, this is the funding tx fee. + // TODO: Add the price of maker payment fee (in taker coin) + preimage fee to the total payment value. + let total_payment_value = &(&state_machine.taker_volume + &state_machine.dex_fee.total_spend_amount()) + + &(&state_machine.taker_premium + &taker_payment_spend_fee.amount); + let preimage_value = TradePreimageValue::Exact(total_payment_value.to_decimal()); + + // FIXME: This is not taker payment tx fee, this is the funding tx fee, use consistent naming. let taker_payment_fee = match state_machine .taker_coin .get_sender_trade_fee(preimage_value, stage) @@ -984,6 +1020,7 @@ impl { maker_coin_start_block: u64, taker_coin_start_block: u64, taker_payment_fee: SavedTradeFee, + taker_payment_spend_fee: SavedTradeFee, maker_payment_spend_fee: SavedTradeFee, } @@ -1014,6 +1052,7 @@ impl, taker_payment_fee: SavedTradeFee, + taker_payment_spend_fee: SavedTradeFee, maker_payment_spend_fee: SavedTradeFee, } @@ -1231,6 +1276,7 @@ impl, + taker_payment_spend_fee: SavedTradeFee, } #[async_trait] @@ -1304,6 +1353,8 @@ impl, + taker_payment_spend_fee: SavedTradeFee, taker_funding: TakerCoin::Tx, funding_spend_preimage: TxPreimageWithSig, maker_payment: MakerCoin::Tx, @@ -1443,6 +1497,7 @@ impl