Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(trading-proto-upgrade): fees fixes among other things #2109

Open
wants to merge 42 commits into
base: dev
Choose a base branch
from

Conversation

mariocynicys
Copy link
Collaborator

The main feat here is taker payment spend fee negotiation (I guess we will refactor namings in the swaps v2 code and rename this to taker payment fee/funding spend fee):

The negotiation goes as follows:

  • Taker and maker each decide how much this fee should be at the beginning of the swap.
  • The taker will send the maker their proposed fee during negotiation,
    if the maker deems the fee as low enough (less than 90% of the maker's own calculated fee),
    they will refuse to trade before the trade starts. Otherwise, they
    will continue and use this fee for validation later.
  • The maker will validate that the taker has accounted for this fee in
    the funding transaction.
  • The taker will validate that the taker payment preimage (generated by the maker)
    uses the negotiated fee or greater (greater number will deduct from the maker's trading volume anyway).
    If not, the taker will stop the trade since the maker is clearly stealing funds that isn't his
    which might affect the completion of the swap if the fee is low.

Proto v2 issue pool:

  • was already fixed:
    • immediate taker refund using S2
    • accept_only_from our order peer.
  • fixed in this PR:
    • use a u8 fork id
    • negotiate funding spend fee and let taker add it to the funding tx
  • still missing:
    • research: concurrent conf
    • recover_funds_for_swap rpc for swaps v2
    • investigation of why test_taker_completes_swap_after_restart is flaky
    • other refactors

Fixmes & discuss comments are intended to be covered in the PR.

@shamardy shamardy requested review from shamardy and laruh May 6, 2024 15:00
@shamardy shamardy changed the title Proto upgrade fixups feat(trading-proto-upgrade): fees fixes among other things May 6, 2024
@shamardy
Copy link
Collaborator

shamardy commented May 6, 2024

@mariocynicys please add label for if this is under review or in progress.

@mariocynicys mariocynicys added under review in progress Changes will be made from the author and removed under review in progress Changes will be made from the author labels May 6, 2024
@laruh
Copy link
Member

laruh commented May 7, 2024

@mariocynicys please merge dev to feature branch as it contains build fixes
https://github.com/KomodoPlatform/komodo-defi-framework/actions/runs/8965111556/job/24617995991?pr=2109#step:8:574

error[E0463]: can't find crate for `core`
  |
  = note: the `x86_64-apple-darwin` target may not be installed
  = help: consider downloading the target with `rustup target add x86_64-apple-darwin`
  = help: consider building the standard library from source with `cargo build -Zbuild-std`

@@ -822,6 +824,8 @@ impl<MakerCoin: MmCoin + MakerCoinSwapOpsV2, TakerCoin: MmCoin + TakerCoinSwapOp
},
};

// FIXME: I am lost here, is this the fee for funding spend or taker payment spend?
// if it's the former, where is the fee for the the taker payment spend?
let taker_payment_spend_trade_fee = match state_machine.taker_coin.get_receiver_trade_fee(stage).compat().await
{
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is the fee paid by the receiver of a payment, it was an htlc spend fee in the legacy swap protocol. For v2, it should be 0 for maker like we agreed before (but maybe not in this PR, and we make it the taker payment spend fee for now), and for taker it should be the fee needed to spend the maker payment which is the same as the legacy protocol. Is this clear enough?

/// Get fee to be paid by receiver per whole swap and check if the wallet has sufficient balance to pay the fee.
fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut<TradeFee>;

Copy link
Collaborator

Choose a reason for hiding this comment

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

and for taker it should be the fee needed to spend the maker payment which is the same as the legacy protocol.

As discussed in DM with @mariocynicys, this should be different than legacy protocol too since the redeem script is a bit different due to the immediate refund path. I will make this in progress again @mariocynicys

@shamardy shamardy added in progress Changes will be made from the author and removed under review labels May 7, 2024
Actually should be called funding spend fee or taker payment fee, but we are just following the current code naming at the moment.

The negotiation goes as follows:
1- Taker and maker each decide how much this fee should be at the beginning of the swap.

2- The taker will send the maker their proposed fee during negotiation,
   if the maker deems the fee as low enough (less than 90% of the maker's own calculated fee),
   they will refuse to trade before the trade starts. Otherwise, they
   will continue and use this fee for validation later.

3- The maker will validate that the taker has accounted for this fee in
   the funding transaction.

4- The taker will validate that the taker payment preimage (generated by the maker)
   uses the negotiated fee or greater (greater number will deduct from the maker's trading volume anyway).
   If not, the taker will stop the trade since the maker is clearly stealing funds that isn't his
   which might affect the completion of the swap if the fee is low.
…o u64

The big decimal might be a fraction less than zero and gets converted to
zero when sent over the wire (because we send the fee as u64).

Scale it properly before sending it.
noticed this in the integration tests, the fee might be a fraction so
clamping it to one yields a fee higher than the actually one, thus fails
the ratio check
use a signature-size dummy date instead
taker payment spend is actually taker payment here, next commit should fix the naming inconsistencies
Copy link
Member

@laruh laruh left a comment

Choose a reason for hiding this comment

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

Thank you for the PR! here are some small notes from my side

mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs Outdated Show resolved Hide resolved
mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs Outdated Show resolved Hide resolved
mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs Outdated Show resolved Hide resolved
@laruh
Copy link
Member

laruh commented May 31, 2024

ps @mariocynicys this pr started to have conflicts

@laruh
Copy link
Member

laruh commented Jun 18, 2024

ps @mariocynicys lint check fails

Copy link
Collaborator

@shamardy shamardy left a comment

Choose a reason for hiding this comment

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

Thanks a lot for the enhancements! Next review iteration!

mm2src/coins/lp_coins.rs Show resolved Hide resolved
mm2src/coins/lp_coins.rs Show resolved Hide resolved
mm2src/coins/lp_coins.rs Show resolved Hide resolved
mm2src/coins/lp_coins.rs Outdated Show resolved Hide resolved
Comment on lines 1683 to 1687
/// Get the fee to be paid for the processing of the maker payment transaction.
async fn get_maker_payment_fee(&self, stage: &FeeApproxStage) -> TradePreimageResult<TradeFee>;

/// Get the fee to be paid for the processing of the maker payment spend transaction aka the claimation transaction.
async fn get_maker_payment_spend_fee(&self, stage: &FeeApproxStage) -> TradePreimageResult<TradeFee>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why would we need the get_maker_payment_spend_fee function in the future? I understand that get_maker_payment_fee will be used so that the taker can cover this fee on the funding transaction, but I don't see a need for get_maker_payment_spend_fee

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess this is just to calculate fee from the taker side in a right way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think it should be used here:

let maker_payment_spend_fee = match state_machine.maker_coin.get_receiver_trade_fee(stage).compat().await {

In place of get_receiver_trade_fee, but I am trying to figure out why didn't i use it O_O

mm2src/coins/lp_coins.rs Outdated Show resolved Hide resolved
mm2src/coins/utxo/utxo_common.rs Outdated Show resolved Hide resolved
Comment on lines 1076 to 1077
// expected fee knowing that the maker will always accept incurring the difference.
calculated_fee.max(taker_instructed_fee)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not always use the calculated_fee? if it's less than or equal to the taker_instructed_fee makers can get some additional amount which is better than giving it to the miners.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We actually discussed this before xD

IIRC, the point was to not compromise the swap success if the maker wants to greedily cut on the fees. to stop that, any additional funds that were designated to be used as fees should only be used as fees otherwise the swap will fail.

Comment on lines 1168 to 1174
let instructed_fee =
sat_from_big_decimal(&gen_args.taker_payment_fee, coin.as_ref().decimals).mm_err(|e| e.into())?;

let fee_div = expected_fee as f64 / actual_fee as f64;

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ooh, I see why you used the max of calculated_fee and taker_instructed_fee above. I don't think this validation is actually needed at all, this is done from the taker side, and once the swap starts any additional fee or reduced fee will either reduce the maker amount or benefit him so the maker can choose any fee on his side when generating the preimage and the taker doesn't have to validate it. Also for the below, maker negotiates a fee at the start / negotiation phase and taker either approves it or not, so there is no room for abuse.

// FIXME: Possible abuse vector is that the taker can always send the fee to be just above 90% of the

Copy link
Collaborator

Choose a reason for hiding this comment

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

once the swap starts any additional fee or reduced fee will either reduce the maker amount or benefit him so the maker can choose any fee on his side when generating the preimage and the taker doesn't have to validate it

I guess we have to lookout for the refund case though, since if maker chooses a very high fee and doesn't complete the swap, taker will lose funds on refund.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also for the below, maker negotiates a fee at the start / negotiation phase and taker either approves it or not, so there is no room for abuse.

What about removing the fee pumping logic (so we won't calculate calculated_fee and won't use the max) and use the taker_instructed_fee right away. Re-thinking about it, the max-ing might not make that much of a difference (most of the time), and this would also stop the attack vector where the maker sets a high fee just to hit the taker on his refund.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Using the taker_instructed_fee is the right idea IMHO but the problem is fees spikes as always. There are 2 solutions:

  1. Either just use taker_instructed_fee, taker will want to optimize for this since they wouldn't want to overpay and they will want a fast swap at the same time so there would be no attack vector and if the taker payment transaction takes too much time to confirm due to a fee spike there is always the option of immediate refund. We should allow maker paying less for fees if possible since it will benefit either the maker on success or taker on refund, so taker will validate the fee to be either taker_instructed_fee or less.

  2. The other solution is more complicated and it's a borrow from lightning where the maker and taker renegotiate the fee if there is a fee spike but this complicates things a lot.

Let's use option one for now and wait until this is tested in a real environment. For fee spikes we can allow the taker to use a bit higher fee from his side (10% higher or we can set it more accurately once we see real data) if needed in the future.

always rename discuss markers to fixmes since i forget about them xD
there is no calculatable tx_size for other tx (non-htlc) (like funding and maker payment txs). their fees depend really on the utxos we have and whether they are segwit or not.
used get_sender_trade_fee for now, though I think this method needs reviewing since its doc comment doesn't make a lot of sense
@laruh
Copy link
Member

laruh commented Nov 11, 2024

ps @mariocynicys this pr started to have conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants