Fix Offer Crossing Rounds Transfer-Fee Burn Down To Zero

This commit is contained in:
Gregory Tsipenyuk
2026-05-14 13:33:31 -04:00
parent e568175524
commit 305d784b26
2 changed files with 63 additions and 3 deletions

View File

@@ -652,7 +652,15 @@ limitStepIn(
// under an amendment.
ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false);
stpAmt.out = ofrAmt.out;
ownerGives = mulRatio(ofrAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false);
// Round up for MPT output so the offer owner pays the full
// ceil(amount × rate) fee, matching direct Payment semantics. IOU uses
// floating-point arithmetic so the floor/ceil distinction is sub-epsilon
// there; preserve the historical false to avoid changing IOU behavior.
ownerGives = mulRatio(
ofrAmt.out,
transferRateOut,
QUALITY_ONE,
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
}
}
@@ -671,7 +679,11 @@ limitStepOut(
if (limit < stpAmt.out)
{
stpAmt.out = limit;
ownerGives = mulRatio(stpAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false);
ownerGives = mulRatio(
stpAmt.out,
transferRateOut,
QUALITY_ONE,
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
ofrAmt = offer.limitOut(
ofrAmt,
stpAmt.out,
@@ -770,7 +782,11 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
TAmounts stpAmt{mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true), ofrAmt.out};
// owner pays the transfer fee.
auto ownerGives = mulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ false);
auto ownerGives = mulRatio(
ofrAmt.out,
ofrOutRate,
QUALITY_ONE,
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
auto const funds = offer.isFunded()
? ownerGives // Offer owner is issuer; they have unlimited funds

View File

@@ -2725,6 +2725,50 @@ public:
using namespace jtx;
auto const gw1 = Account("gateway1");
{
auto const issuer = Account("issuer");
auto const sender = Account("sender");
auto const receiver = Account("receiver");
auto const seller = Account("seller");
auto const buyer = Account("buyer");
Env env{*this, features};
env.fund(XRP(10'000), issuer, sender, receiver, seller, buyer);
env.close();
MPTTester mpt{
{.env = env,
.issuer = issuer,
.holders = {sender, receiver, seller, buyer},
.transferFee = 100}};
MPT const token = mpt;
mpt.pay(issuer, sender, 2'000);
mpt.pay(issuer, seller, 2'000);
// A direct holder-to-holder payment of 999 MPT at a 0.1% fee
// requires 1000 from the sender and burns one MPT.
env(pay(sender, receiver, token(999)), Ter(tecPATH_PARTIAL));
env.close();
env(pay(sender, receiver, token(999)), Sendmax(token(1'000)));
env.close();
BEAST_EXPECT(mpt.getBalance(sender) == 1'000);
BEAST_EXPECT(mpt.getBalance(receiver) == 999);
BEAST_EXPECT(mpt.getBalance(issuer) == 3'999);
// CLOB crossing should apply the same fee quantum. The offer
// owner pays ceil(999 * 1.001) = 1000, not floor(...) = 999.
env(offer(seller, XRP(999), token(999)));
env.close();
env(offer(buyer, token(999), XRP(999)));
env.close();
BEAST_EXPECT(mpt.getBalance(seller) == 1'000);
BEAST_EXPECT(mpt.getBalance(buyer) == 999);
BEAST_EXPECT(mpt.getBalance(issuer) == 3'998);
}
auto test = [&](auto&& issue1, auto&& issue2) {
Env env{*this, features};