Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
073a618
feat(dex-fee): update DEX fee pubkeys
shamardy Jan 27, 2026
70b4370
feat(dex-fee): update Z-addresses for ZCash/ARRR
shamardy Jan 27, 2026
25c1f55
feat(dex-fee): disable burn feature
shamardy Jan 27, 2026
591d411
feat(dex-fee): change fee rate to 2% and disable KMD exemption
shamardy Jan 27, 2026
6cf8a8c
feat(dex-fee): ignore obsolete KMD discount tests
shamardy Jan 27, 2026
c7cf52f
feat(dex-fee): update test fixtures for new DEX fee pubkey
shamardy Jan 27, 2026
c20b2aa
refactor(dex-fee): remove unused ticker parameters from fee functions
shamardy Jan 27, 2026
ea8a87c
fix(clippy): remove #[test] attr from doctest example
shamardy Jan 27, 2026
dc48908
test(dex-fee): fix tests for 2% fee rate and new pubkey
shamardy Jan 29, 2026
87fdafb
test(dex-fee): update WithBurn tests for uniform 2% rate
shamardy Jan 29, 2026
c8595b0
chore(dex-fee): restore TODO about negotiation version for burn
shamardy Jan 29, 2026
e1736b5
fix(dex-fee): fix failing unit and docker tests
shamardy Jan 29, 2026
d0f12ec
feat(dex-fee): restore GLEEC fee discount (1% vs 2% standard rate)
shamardy Mar 5, 2026
e1d7a53
fix(dex-fee): correct test_max_taker_vol_swap expected value for 1% d…
shamardy Mar 5, 2026
3f0bfde
feat(p2p): change default netid from 8762 to 6133
shamardy Mar 5, 2026
9185bfa
docs(common): fix markdown code block in AGENTS.md
shamardy Mar 5, 2026
90a4779
refactor(dex-fee): address review comments on fee tests
shamardy Mar 6, 2026
6bfe2f7
fix(clippy): suppress new Rust 1.94.0 clippy lints
shamardy Mar 6, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::inquire_extentions::{InquireOption, DEFAULT_DEFAULT_OPTION_BOOL_FORMA
use crate::helpers;
use crate::logging::error_anyhow;

const DEFAULT_NET_ID: u16 = 8762;
const DEFAULT_NET_ID: u16 = 6133;
const DEFAULT_GID: &str = "adex-cli";
const DEFAULT_OPTION_PLACEHOLDER: &str = "Tap enter to skip";
const RPC_PORT_MIN: u16 = 1024;
Expand Down Expand Up @@ -128,7 +128,7 @@ impl Mm2Cfg {
fn inquire_net_id(&mut self) -> Result<()> {
self.netid = CustomType::<u16>::new("What is the network `mm2` is going to be a part, netid:")
.with_default(DEFAULT_NET_ID)
.with_help_message(r#"Network ID number, telling the Komodo DeFi Framework which network to join. 8762 is the current main network, though alternative netids can be used for testing or "private" trades"#)
.with_help_message(r#"Network ID number, telling the Komodo DeFi Framework which network to join. 6133 is the current main network, though alternative netids can be used for testing or "private" trades"#)
.with_placeholder(format!("{DEFAULT_NET_ID}").as_str())
.prompt()
.map_err(|error|
Expand Down Expand Up @@ -283,7 +283,7 @@ impl Mm2Cfg {
.with_formatter(DEFAULT_OPTION_BOOL_FORMATTER)
.with_default_value_formatter(DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER)
.with_default(InquireOption::None)
.with_help_message("Runs Komodo DeFi Framework as a seed node mode (acting as a relay for Komodo DeFi Framework clients). Optional, defaults to false. Use of this mode is not reccomended on the main network (8762) as it could result in a pubkey ban if non-compliant. on alternative testing or private networks, at least one seed node is required to relay information to other Komodo DeFi Framework clients using the same netID.")
.with_help_message("Runs Komodo DeFi Framework as a seed node mode (acting as a relay for Komodo DeFi Framework clients). Optional, defaults to false. Use of this mode is not reccomended on the main network (6133) as it could result in a pubkey ban if non-compliant. on alternative testing or private networks, at least one seed node is required to relay information to other Komodo DeFi Framework clients using the same netID.")
.prompt()
.map_err(|error|
error_anyhow!("Failed to get i_am_a_seed: {error}")
Expand Down
1 change: 1 addition & 0 deletions mm2src/coins/lightning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ impl LightningCoin {
.find(|chan| chan.user_channel_id == uuid.as_u128())
}

#[allow(clippy::result_large_err)] // PaymentError is from external crate
pub(crate) async fn pay_invoice(
&self,
invoice: Invoice,
Expand Down
124 changes: 92 additions & 32 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4178,22 +4178,22 @@ impl DexFee {
DexFee::Standard(dex_fee)
}

/// Returns dex fee discount if KMD is traded
/// Returns DEX fee rate. GLEEC trades get a 50% discount (1% vs 2% base rate).
pub fn dex_fee_rate(base: &str, rel: &str) -> MmNumber {
#[cfg(any(feature = "for-tests", test))]
let fee_discount_tickers: &[&str] = match std::env::var("MYCOIN_FEE_DISCOUNT") {
Ok(_) => &["KMD", "MYCOIN"],
Err(_) => &["KMD"],
Ok(_) => &["GLEEC", "MYCOIN"],
Err(_) => &["GLEEC"],
};

#[cfg(not(any(feature = "for-tests", test)))]
let fee_discount_tickers: &[&str] = &["KMD"];
let fee_discount_tickers: &[&str] = &["GLEEC"];

if fee_discount_tickers.contains(&base) || fee_discount_tickers.contains(&rel) {
// 1/777 - 10%
BigRational::new(9.into(), 7770.into()).into()
// 1% fee (50% discount)
BigRational::new(1.into(), 100.into()).into()
Comment on lines +4192 to +4193
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think discounting more than 50% (e.g. 70%) would incentives more ppl to perform the trades with GLEEC as an intermediate coin.

currently there is no incentive to do such a thing since that requires two swaps each costing 1% fee, so still 2% total (+ more chain fees).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, let's discuss this when the time comes for implementing swaps through Gleec chain.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the discount be changed without a "hardfork"?

} else {
BigRational::new(1.into(), 777.into()).into()
// 2% fee (standard rate)
BigRational::new(2.into(), 100.into()).into()
}
}

Expand Down Expand Up @@ -6480,6 +6480,7 @@ mod tests {

#[test]
fn test_dex_fee_amount() {
// BTC WithBurn, burn enabled by mocking
let base = "BTC";
let btc = TestCoin::new(base);
TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true));
Expand All @@ -6488,22 +6489,24 @@ mod tests {
let amount = 1.into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
let expected_fee = DexFee::WithBurn {
fee_amount: amount.clone() / 777u64.into() * "0.75".into(),
burn_amount: amount / 777u64.into() * "0.25".into(),
fee_amount: amount.clone() * "0.02".into() * "0.75".into(),
burn_amount: amount * "0.02".into() * "0.25".into(),
burn_destination: DexFeeBurnDestination::PreBurnAccount,
};
assert_eq!(expected_fee, actual_fee);
TestCoin::should_burn_dex_fee.clear_mock();

// KMD WithBurn - same 2% rate as other coins (no KMD discount anymore)
// KMD uses should_burn_directly() -> KmdOpReturn
let base = "KMD";
let kmd = TestCoin::new(base);
TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true));
TestCoin::should_burn_directly.mock_safe(|_| MockResult::Return(true));
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.0001").into()));
let rel = "ETH";
let amount = 1.into();
let actual_fee = DexFee::new_from_taker_coin(&kmd, rel, &amount);
let expected_fee = amount.clone() * (9, 7770).into() * MmNumber::from("0.75");
let expected_burn_amount = amount * (9, 7770).into() * MmNumber::from("0.25");
let expected_fee = amount.clone() * "0.02".into() * MmNumber::from("0.75");
let expected_burn_amount = amount * "0.02".into() * MmNumber::from("0.25");
assert_eq!(
DexFee::WithBurn {
fee_amount: expected_fee,
Expand All @@ -6512,26 +6515,31 @@ mod tests {
},
actual_fee
);
TestCoin::should_burn_dex_fee.clear_mock();
TestCoin::should_burn_directly.clear_mock();

// check the case when KMD taker fee is close to dust (0.75 of fee < dust)
let base = "KMD";
let kmd = TestCoin::new(base);
TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true));
TestCoin::should_burn_directly.mock_safe(|_| MockResult::Return(true));
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into()));
// With 2% rate: need amount where fee portion (75%) < min_tx_amount
// fee = amount * 0.02 * 0.75 < 0.00001 => amount < 0.00001 / 0.015 ≈ 0.000667
// Using amount = 0.0006: total = 0.000012, fee (75%) = 0.000009 < min, gets clamped to min
let rel = "BTC";
let amount = (1001 * 777, 90000000).into();
let amount = "0.0006".into();
let actual_fee = DexFee::new_from_taker_coin(&kmd, rel, &amount);
// fee gets clamped to min_tx_amount, burn = total - fee = 0.000012 - 0.00001 = 0.000002
assert_eq!(
DexFee::WithBurn {
fee_amount: "0.00001".into(), // equals to min_tx_amount
burn_amount: "0.00000001".into(),
burn_amount: "0.000002".into(),
burn_destination: DexFeeBurnDestination::KmdOpReturn,
},
actual_fee
);
TestCoin::should_burn_dex_fee.clear_mock();
TestCoin::should_burn_directly.clear_mock();

// BTC WithBurn with smaller min_tx_amount
let base = "BTC";
let btc = TestCoin::new(base);
TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true));
Expand All @@ -6540,55 +6548,58 @@ mod tests {
let amount = 1.into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
let expected_fee = DexFee::WithBurn {
fee_amount: amount.clone() * (9, 7770).into() * "0.75".into(),
burn_amount: amount * (9, 7770).into() * "0.25".into(),
fee_amount: amount.clone() * "0.02".into() * "0.75".into(),
burn_amount: amount * "0.02".into() * "0.25".into(),
burn_destination: DexFeeBurnDestination::PreBurnAccount,
};
assert_eq!(expected_fee, actual_fee);
TestCoin::should_burn_dex_fee.clear_mock();

// whole dex fee (0.001 * 9 / 7770) less than min tx amount (0.00001)
// whole dex fee (amount * 0.02) less than min tx amount (0.00001)
let base = "BTC";
let btc = TestCoin::new(base);
TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true));
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into()));
let rel = "KMD";
let amount: MmNumber = "0.001".parse::<BigDecimal>().unwrap().into();
// 2% of 0.0001 = 0.000002 < min (0.00001)
let amount: MmNumber = "0.0001".parse::<BigDecimal>().unwrap().into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
assert_eq!(DexFee::Standard("0.00001".into()), actual_fee);
TestCoin::should_burn_dex_fee.clear_mock();

// 75% of dex fee (0.03 * 9/7770 * 0.75) is over the min tx amount (0.00001)
// 75% of dex fee is over the min tx amount (0.00001)
// but non-kmd burn amount is less than the min tx amount
let base = "BTC";
let btc = TestCoin::new(base);
TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true));
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into()));
let rel = "KMD";
let amount: MmNumber = "0.03".parse::<BigDecimal>().unwrap().into();
// 2% of 0.001 = 0.00002, fee = 0.000015 > min, burn = 0.000005 < min
let amount: MmNumber = "0.001".parse::<BigDecimal>().unwrap().into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
assert_eq!(DexFee::Standard(amount * (9, 7770).into()), actual_fee);
assert_eq!(DexFee::Standard(amount * "0.02".into()), actual_fee);
TestCoin::should_burn_dex_fee.clear_mock();

// burning from eth currently not supported
let base = "USDT-ERC20";
let btc = TestCoin::new(base);
let erc20 = TestCoin::new(base);
TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(false));
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into()));
let rel = "BTC";
let amount: MmNumber = "1".parse::<BigDecimal>().unwrap().into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
assert_eq!(DexFee::Standard(amount / "777".into()), actual_fee);
let actual_fee = DexFee::new_from_taker_coin(&erc20, rel, &amount);
assert_eq!(DexFee::Standard(amount * "0.02".into()), actual_fee);
TestCoin::should_burn_dex_fee.clear_mock();

// NUCLEUS WithBurn
let base = "NUCLEUS";
let btc = TestCoin::new(base);
let nucleus = TestCoin::new(base);
TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true));
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.000001").into()));
let rel = "IRIS";
let amount: MmNumber = "0.008".parse::<BigDecimal>().unwrap().into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
let std_fee = amount / "777".into();
let actual_fee = DexFee::new_from_taker_coin(&nucleus, rel, &amount);
let std_fee = amount * "0.02".into();
let fee_amount = std_fee.clone() * "0.75".into();
let burn_amount = std_fee - fee_amount.clone();
assert_eq!(
Expand All @@ -6607,12 +6618,61 @@ mod tests {
TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true));
TestCoin::dex_pubkey.mock_safe(|_| MockResult::Return(DEX_BURN_ADDR_RAW_PUBKEY.as_slice()));
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into()));
let rel = "KMD";
let amount: MmNumber = "0.03".parse::<BigDecimal>().unwrap().into();
let rel = "KMD";
let actual_fee = DexFee::new_with_taker_pubkey(&btc, rel, &amount, DEX_BURN_ADDR_RAW_PUBKEY.as_slice());
assert_eq!(DexFee::NoFee, actual_fee);
TestCoin::should_burn_dex_fee.clear_mock();
TestCoin::dex_pubkey.clear_mock();

// ============================================================================
// Production behavior (burn disabled)
// ============================================================================

// Standard 2% fee for BTC (burn disabled in production)
let base = "BTC";
let btc = TestCoin::new(base);
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.0001").into()));
let rel = "ETH";
let amount: MmNumber = 1.into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
assert_eq!(DexFee::Standard("0.02".into()), actual_fee);

// Large trade amount
let base = "BTC";
let btc = TestCoin::new(base);
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into()));
let rel = "ETH";
let amount: MmNumber = "1000".parse::<BigDecimal>().unwrap().into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
assert_eq!(DexFee::Standard("20".into()), actual_fee);

// Fractional amount with precise 2% calculation
let base = "BTC";
let btc = TestCoin::new(base);
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into()));
let rel = "ETH";
let amount: MmNumber = "0.5".parse::<BigDecimal>().unwrap().into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
assert_eq!(DexFee::Standard("0.01".into()), actual_fee);

// GLEEC discount test: 1% fee instead of 2%
let gleec = TestCoin::new("GLEEC");
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into()));
let rel = "BTC";
let amount: MmNumber = 1.into();
let actual_fee = DexFee::new_from_taker_coin(&gleec, rel, &amount);
assert_eq!(DexFee::Standard("0.01".into()), actual_fee);

// GLEEC as maker_ticker also gets discount
let btc = TestCoin::new("BTC");
TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into()));
let rel = "GLEEC";
let amount: MmNumber = 1.into();
let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount);
assert_eq!(DexFee::Standard("0.01".into()), actual_fee);

TestCoin::min_tx_amount.clear_mock();
}
}

Expand Down
24 changes: 17 additions & 7 deletions mm2src/coins/qrc20/qrc20_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use keys::Address;
use mm2_core::mm_ctx::MmCtxBuilder;
use mm2_number::bigdecimal::Zero;
use mm2_test_helpers::electrums::tqtum_electrums;
use mm2_test_helpers::for_tests::DEX_FEE_ADDR_RAW_PUBKEY_LEGACY;
use rpc::v1::types::ToTxHash;
use std::convert::TryFrom;
use std::mem::discriminant;
Expand Down Expand Up @@ -321,6 +322,7 @@ fn test_wait_for_confirmations_excepted() {
#[test]
fn test_validate_fee() {
// priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG
// TODO: Update test fixtures with transactions to new DEX fee address once swaps exist

use common::DEX_FEE_ADDR_RAW_PUBKEY;
let priv_key = [
Expand All @@ -330,11 +332,15 @@ fn test_validate_fee() {
let (_ctx, coin) = qrc20_coin_for_test(priv_key, None);

// QRC20 transfer tx "f97d3a43dbea0993f1b7a6a299377d4ee164c84935a1eb7d835f70c9429e6a1d"
// This tx was sent to the OLD dex fee address, so we mock dex_pubkey to return the legacy address
let tx = TransactionEnum::UtxoTx("010000000160fd74b5714172f285db2b36f0b391cd6883e7291441631c8b18f165b0a4635d020000006a47304402205d409e141111adbc4f185ae856997730de935ac30a0d2b1ccb5a6c4903db8171022024fc59bbcfdbba283556d7eeee4832167301dc8e8ad9739b7865f67b9676b226012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000625403a08601012844a9059cbb000000000000000000000000ca1e04745e8ca0c60d8c5881531d51bec470743f00000000000000000000000000000000000000000000000000000000000f424014d362e096e873eb7907e205fadc6175c6fec7bc44c200ada205000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acfe967d5f".into());
let sender_pub = hex::decode("03693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9").unwrap();

let amount = BigDecimal::from_str("0.01").unwrap();

// Mock to use legacy fee address for this historical tx fixture
<Qrc20Coin as SwapOps>::dex_pubkey.mock_safe(|_| MockResult::Return(DEX_FEE_ADDR_RAW_PUBKEY_LEGACY.as_slice()));

let result = block_on(coin.validate_fee(ValidateFeeArgs {
fee_tx: &tx,
expected_sender: &sender_pub,
Expand All @@ -344,12 +350,12 @@ fn test_validate_fee() {
}));
assert!(result.is_ok());

// wrong dex address
<Qrc20Coin as SwapOps>::dex_pubkey.mock_safe(|_| {
MockResult::Return(Box::leak(Box::new(
hex::decode("03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc05").unwrap(),
)))
});
// wrong dex address - use a completely different pubkey
let wrong_pubkey: &'static [u8] = &[
3, 188, 44, 123, 166, 113, 186, 228, 166, 252, 131, 82, 68, 201, 118, 43, 65, 100, 123, 152, 39, 212, 120, 10,
137, 169, 73, 185, 132, 168, 221, 204, 5,
];
<Qrc20Coin as SwapOps>::dex_pubkey.mock_safe(move |_| MockResult::Return(wrong_pubkey));
let err = block_on(coin.validate_fee(ValidateFeeArgs {
fee_tx: &tx,
expected_sender: &sender_pub,
Expand All @@ -364,7 +370,9 @@ fn test_validate_fee() {
ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("QRC20 Fee tx was sent to wrong address")),
_ => panic!("Expected `WrongPaymentTx` wrong receiver address, found {:?}", err),
}
<Qrc20Coin as SwapOps>::dex_pubkey.clear_mock();

// Restore legacy mock for remaining tests with the historical tx fixture
<Qrc20Coin as SwapOps>::dex_pubkey.mock_safe(|_| MockResult::Return(DEX_FEE_ADDR_RAW_PUBKEY_LEGACY.as_slice()));

let err = block_on(coin.validate_fee(ValidateFeeArgs {
fee_tx: &tx,
Expand Down Expand Up @@ -414,6 +422,8 @@ fn test_validate_fee() {
_ => panic!("Expected `WrongPaymentTx` invalid fee value, found {:?}", err),
}

<Qrc20Coin as SwapOps>::dex_pubkey.clear_mock();

// QTUM tx "8a51f0ffd45f34974de50f07c5bf2f0949da4e88433f8f75191953a442cf9310"
let tx = TransactionEnum::UtxoTx("020000000113640281c9332caeddd02a8dd0d784809e1ad87bda3c972d89d5ae41f5494b85010000006a47304402207c5c904a93310b8672f4ecdbab356b65dd869a426e92f1064a567be7ccfc61ff02203e4173b9467127f7de4682513a21efb5980e66dbed4da91dff46534b8e77c7ef012102baefe72b3591de2070c0da3853226b00f082d72daa417688b61cb18c1d543d1afeffffff020001b2c4000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acbc4dd20c2f0000001976a9144208fa7be80dcf972f767194ad365950495064a488ac76e70800".into());
let sender_pub = hex::decode("02baefe72b3591de2070c0da3853226b00f082d72daa417688b61cb18c1d543d1a").unwrap();
Expand Down
Loading