Fix MPT transfer fee on small escrow

This commit is contained in:
Gregory Tsipenyuk
2026-06-25 11:12:24 -04:00
parent 129b690c26
commit 7686d16cb7
2 changed files with 88 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
@@ -213,10 +214,25 @@ escrowUnlockApplyHelper<MPTIssue>(
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
{
// compute transfer fee, if any
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
if (view.rules().enabled(featureMPTokensV2))
{
XRPL_ASSERT(
lockedRate >= kParityRate,
"xrpl::escrowUnlockApplyHelper<MPTIssue> : lockedRate is at least parity");
// MPTs are integral, so round the delivered amount down and
// charge any fractional transfer fee to the escrowed amount.
auto const delivered =
mulRatio(amount.mpt(), kParityRate.value, lockedRate.value, false);
finalAmt = STAmount(amount.asset(), delivered.value());
}
else
{
// compute transfer fee, if any
auto const xferFee =
amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
}
return unlockEscrowMPT(
view,

View File

@@ -3679,6 +3679,72 @@ struct EscrowToken_test : public beast::unit_test::Suite
}
}
void
testMPTSplitEscrowTransferFee(FeatureBitset features)
{
using namespace test::jtx;
using namespace std::literals;
bool const withMPTokensV2 = features[featureMPTokensV2];
testcase(
std::string("MPT Split Escrow Transfer Fee ") +
(withMPTokensV2 ? "with MPTokensV2" : "without MPTokensV2"));
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
env.fund(XRP(1'000), alice, bob, gw);
env.close();
MPTTester const mpt({
.env = env,
.issuer = gw,
.holders = {alice, bob},
.transferFee = 1'000,
.flags = tfMPTCanEscrow | tfMPTCanTransfer,
});
env(pay(gw, alice, mpt(10'000)));
env.close();
static constexpr int escrowCount = 10;
static constexpr int splitAmount = 10;
static constexpr int totalLocked = escrowCount * splitAmount;
std::array<std::uint32_t, escrowCount> seqs{};
for (auto& seq : seqs)
{
seq = env.seq(alice);
env(escrow::create(alice, bob, mpt(splitAmount)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
}
BEAST_EXPECT(env.balance(alice, mpt) == mpt(10'000 - totalLocked));
BEAST_EXPECT(env.balance(bob, mpt) == mpt(0));
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-10'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == totalLocked);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == totalLocked);
for (auto const seq : seqs)
{
env(escrow::finish(bob, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
}
auto const feeBurned = withMPTokensV2 ? escrowCount : 0;
BEAST_EXPECT(env.balance(alice, mpt) == mpt(10'000 - totalLocked));
BEAST_EXPECT(env.balance(bob, mpt) == mpt(totalLocked - feeBurned));
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-10'000 + feeBurned));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
}
void
testMPTRequireAuth(FeatureBitset features)
{
@@ -3997,6 +4063,8 @@ public:
testMPTWithFeats(feats);
testMPTWithFeats(feats - fixTokenEscrowV1);
}
testMPTSplitEscrowTransferFee(all - featureMPTokensV2);
testMPTSplitEscrowTransferFee(all);
}
};