fix: Fix non-canonical MPT amount (#7117)

Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Gregory Tsipenyuk
2026-05-23 02:40:26 -04:00
committed by GitHub
parent dfb9b8ed9a
commit dcd2ff0b5f
12 changed files with 1074 additions and 4 deletions

View File

@@ -19,8 +19,10 @@
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/UintTypes.h>
@@ -1222,6 +1224,50 @@ operator-(STAmount const& value)
STAmount::Unchecked{});
}
static bool
hasInvalidAmount(STBase const& field, int depth, beast::Journal j);
static bool
hasInvalidAmount(STObject const& object, int depth, beast::Journal j)
{
return std::ranges::any_of(
object, [&](STBase const& field) { return hasInvalidAmount(field, depth, j); });
}
static bool
hasInvalidAmount(STArray const& array, int depth, beast::Journal j)
{
return std::ranges::any_of(
array, [&](STObject const& object) { return hasInvalidAmount(object, depth, j); });
}
static bool
hasInvalidAmount(STBase const& field, int depth, beast::Journal j)
{
if (depth > 10)
{
JLOG(j.error()) << "hasInvalidAmount: depth exceeds 10";
return true;
}
if (auto const amount = dynamic_cast<STAmount const*>(&field))
return !isLegalMPT(*amount) || !isLegalNet(*amount);
if (auto const object = dynamic_cast<STObject const*>(&field))
return hasInvalidAmount(*object, depth + 1, j);
if (auto const array = dynamic_cast<STArray const*>(&field))
return hasInvalidAmount(*array, depth + 1, j);
return false;
}
bool
hasInvalidAmount(STBase const& field, beast::Journal j)
{
return hasInvalidAmount(field, 0, j);
}
//------------------------------------------------------------------------------
//
// Arithmetic

View File

@@ -27,6 +27,7 @@
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h> // IWYU pragma: keep
@@ -256,6 +257,15 @@ Transactor::preflight2(PreflightContext const& ctx)
return tesSUCCESS;
}
NotTEC
Transactor::preflightUniversal(PreflightContext const& ctx)
{
if (ctx.rules.enabled(fixCleanup3_2_0) && hasInvalidAmount(ctx.tx, ctx.j))
return temBAD_AMOUNT;
return tesSUCCESS;
}
//------------------------------------------------------------------------------
Transactor::Transactor(ApplyContext& ctx)

View File

@@ -1080,4 +1080,35 @@ NoModifiedUnmodifiableFields::finalize(
return true;
}
void
ValidAmounts::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const& after)
{
if (!isDelete && after)
afterEntries_.push_back(after);
}
bool
ValidAmounts::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j) const
{
bool const badLedgerEntry = std::ranges::any_of(
afterEntries_, [&](auto const& sle) { return hasInvalidAmount(*sle, j); });
if (badLedgerEntry)
{
JLOG(j.fatal())
<< "Invariant failed: ledger entry contains non-canonical MPT or XRP amount";
return !view.rules().enabled(fixCleanup3_2_0);
}
return true;
}
} // namespace xrpl

View File

@@ -139,6 +139,11 @@ CheckCash::preclaim(PreclaimContext const& ctx)
}(ctx.tx)};
STAmount const sendMax = sleCheck->at(sfSendMax);
// A legacy Check may contain a non-canonical MPT sfSendMax. Universal
// preflight only validates the CheckCash transaction, not the stored Check.
if (ctx.view.rules().enabled(fixCleanup3_2_0) && !isLegalMPT(sendMax))
return tefBAD_LEDGER;
if (!equalTokens(value.asset(), sendMax.asset()))
{
JLOG(ctx.j.warn()) << "Check cash does not match check currency.";

View File

@@ -81,6 +81,16 @@ EscrowCreate::makeTxConsequences(PreflightContext const& ctx)
return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::kZero};
}
bool
EscrowCreate::checkExtraFeatures(PreflightContext const& ctx)
{
// Only require featureMPTokensV1 when the escrow amount is an MPT and
// fixCleanup3_2_0 is active; XRP/IOU escrows are unaffected by this gate.
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds<MPTIssue>())
return ctx.rules.enabled(featureMPTokensV1);
return true;
}
template <ValidIssueType T>
static NotTEC
escrowCreatePreflightHelper(PreflightContext const& ctx);
@@ -103,7 +113,7 @@ template <>
NotTEC
escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureMPTokensV1))
if (!ctx.rules.enabled(fixCleanup3_2_0) && !ctx.rules.enabled(featureMPTokensV1))
return temDISABLED;
auto const amount = ctx.tx[sfAmount];

View File

@@ -4065,6 +4065,63 @@ class Invariants_test : public beast::unit_test::Suite
using namespace test::jtx;
testcase << "MPT";
MPTIssue const nonCanonicalMPTIssue{makeMptID(1, AccountID(0x4985601))};
auto const nonCanonicalMPTAmount = [&](SField const& field) {
return STAmount{
field,
nonCanonicalMPTIssue,
kMaxMpTokenAmount + std::uint64_t{1},
0,
false,
STAmount::Unchecked{}};
};
auto const negativeMPTAmount = [&](SField const& field) {
return STAmount{field, nonCanonicalMPTIssue, 2, 0, true, STAmount::Unchecked{}};
};
auto const nonCanonicalMPTPayment = [&]() {
return STTx{ttPAYMENT, [&](STObject& tx) {
tx.setFieldAmount(sfAmount, nonCanonicalMPTAmount(sfAmount));
}};
};
doInvariantCheck(
Env{*this, defaultAmendments() - fixCleanup3_2_0},
{},
[](Account const&, Account const&, ApplyContext&) { return true; },
XRPAmount{},
nonCanonicalMPTPayment(),
{tesSUCCESS, tesSUCCESS});
doInvariantCheck(
{{"ledger entry contains non-canonical MPT or XRP amount"}},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const sle = ac.view().peek(keylet::account(a1.id()));
if (!sle)
return false;
auto sleNew = std::make_shared<SLE>(keylet::check(a1.id(), (*sle)[sfSequence]));
sleNew->setAccountID(sfAccount, a1.id());
sleNew->setAccountID(sfDestination, a2.id());
sleNew->setFieldAmount(sfSendMax, nonCanonicalMPTAmount(sfSendMax));
ac.view().insert(sleNew);
return true;
});
doInvariantCheck(
{{"ledger entry contains non-canonical MPT or XRP amount"}},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const sle = ac.view().peek(keylet::account(a1.id()));
if (!sle)
return false;
auto sleNew = std::make_shared<SLE>(keylet::check(a1.id(), (*sle)[sfSequence]));
sleNew->setAccountID(sfAccount, a1.id());
sleNew->setAccountID(sfDestination, a2.id());
sleNew->setFieldAmount(sfSendMax, negativeMPTAmount(sfSendMax));
ac.view().insert(sleNew);
return true;
});
// MPT OutstandingAmount > MaximumAmount
doInvariantCheck(
{{"OutstandingAmount overflow"}},

View File

@@ -25,11 +25,13 @@
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/vault.h>
#include <test/jtx/xchain_bridge.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
@@ -49,6 +51,7 @@
#include <xrpl/protocol/SOTemplate.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -57,13 +60,17 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <array>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <limits>
#include <memory>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
@@ -2115,6 +2122,864 @@ class MPToken_test : public beast::unit_test::Suite
BEAST_EXPECT(txWithAmounts.empty());
}
void
testNonCanonicalMPTAmountCleanup(FeatureBitset features)
{
using namespace test::jtx;
using namespace std::literals;
FeatureBitset const withoutFix = features - fixCleanup3_2_0;
FeatureBitset const withFix = features | fixCleanup3_2_0;
FeatureBitset const withoutFixAndV2 = withoutFix - featureMPTokensV2;
FeatureBitset const withFixAndWithoutV2 = withFix - featureMPTokensV2;
Account const alice{"alice"};
Account const bob{"bob"};
Account const gw{"gw"};
using MPTValue = MPTAmount::value_type;
MPTValue const mptMin = std::numeric_limits<MPTValue>::min();
MPTValue const mptMax = std::numeric_limits<MPTValue>::max();
std::uint64_t const u64Max = std::numeric_limits<std::uint64_t>::max();
std::uint64_t const firstInvalidMPTMantissa = static_cast<std::uint64_t>(mptMax) + 1;
MPTValue const alice0 = 10'000;
MPTValue const gw0 = -20'000;
TER const success = tesSUCCESS;
TER const invariantFailed = tecINVARIANT_FAILED;
TER const pathPartial = tecPATH_PARTIAL;
TER const badAmountTer = temBAD_AMOUNT;
struct BadMPTAmount
{
std::string_view name;
std::uint64_t mantissa;
bool negative;
MPTValue mptValue;
TER issuerToHolderPreFixTer;
TER holderSourcePreFixTer;
MPTValue issuerToHolderAliceAfterPreFix;
MPTValue issuerToHolderIssuerAfterPreFix;
MPTValue issuerToHolderAliceAfterPostFix;
MPTValue issuerToHolderIssuerAfterPostFix;
};
// clang-format off
std::array<BadMPTAmount, 7> const badMPTAmounts = {{
{ .name="INT64_MAX + 1", .mantissa=firstInvalidMPTMantissa, .negative=false, .mptValue=mptMin, .issuerToHolderPreFixTer=invariantFailed, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0, .issuerToHolderIssuerAfterPreFix=gw0, .issuerToHolderAliceAfterPostFix=alice0 - 1, .issuerToHolderIssuerAfterPostFix=gw0 + 1},
{ .name="INT64_MAX + 10", .mantissa=firstInvalidMPTMantissa + 9, .negative=false, .mptValue=mptMin + 9, .issuerToHolderPreFixTer=invariantFailed, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0, .issuerToHolderIssuerAfterPreFix=gw0, .issuerToHolderAliceAfterPostFix=alice0 - 1, .issuerToHolderIssuerAfterPostFix=gw0 + 1},
{ .name="UINT64_MAX - 9998", .mantissa=u64Max - 9'998, .negative=false, .mptValue=MPTValue{-9'999}, .issuerToHolderPreFixTer=success, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0 - 9'999, .issuerToHolderIssuerAfterPreFix=gw0 + 9'999, .issuerToHolderAliceAfterPostFix=alice0 - 10'000, .issuerToHolderIssuerAfterPostFix=gw0 + 10'000},
{ .name="UINT64_MAX - 9", .mantissa=u64Max - 9, .negative=false, .mptValue=MPTValue{-10}, .issuerToHolderPreFixTer=success, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0 - 10, .issuerToHolderIssuerAfterPreFix=gw0 + 10, .issuerToHolderAliceAfterPostFix=alice0 - 11, .issuerToHolderIssuerAfterPostFix=gw0 + 11},
{ .name="UINT64_MAX - 1", .mantissa=u64Max - 1, .negative=false, .mptValue=MPTValue{-2}, .issuerToHolderPreFixTer=success, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0 - 2, .issuerToHolderIssuerAfterPreFix=gw0 + 2, .issuerToHolderAliceAfterPostFix=alice0 - 3, .issuerToHolderIssuerAfterPostFix=gw0 + 3},
{ .name="UINT64_MAX", .mantissa=u64Max, .negative=false, .mptValue=MPTValue{-1}, .issuerToHolderPreFixTer=success, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0 - 1, .issuerToHolderIssuerAfterPreFix=gw0 + 1, .issuerToHolderAliceAfterPostFix=alice0 - 2, .issuerToHolderIssuerAfterPostFix=gw0 + 2},
{ .name="-2", .mantissa=std::uint64_t{2}, .negative=true, .mptValue=MPTValue{-2}, .issuerToHolderPreFixTer=badAmountTer, .holderSourcePreFixTer=badAmountTer, .issuerToHolderAliceAfterPreFix=alice0, .issuerToHolderIssuerAfterPreFix=gw0, .issuerToHolderAliceAfterPostFix=alice0 - 1, .issuerToHolderIssuerAfterPostFix=gw0 + 1}
}};
// clang-format on
auto const badMPTAmount = [&](MPTIssue const& issue, BadMPTAmount const& bad) {
return STAmount{issue, bad.mantissa, 0, bad.negative, STAmount::Unchecked{}};
};
auto const makeIssue = [&](Env& env) {
MPTTester const mpt{
{.env = env,
.issuer = gw,
.holders = {alice, bob},
.pay = 10'000,
.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanEscrow | tfMPTCanClawback}};
return MPTIssue{mpt.issuanceID()};
};
auto const withNonCanonicalMPTAmount =
[](JTx jt, SField const& field, STAmount const& amount, Account const& signer) {
STTx tx{*jt.stx};
tx.setFieldAmount(field, amount);
tx.sign(signer.pk(), signer.sk());
jt.stx = std::make_shared<STTx const>(tx);
return jt;
};
auto const roundTrip = [](STTx const& tx) {
Serializer s;
tx.add(s);
SerialIter sit{s.slice()};
return STTx{sit};
};
auto const expectRoundTripBadMPT =
[&](JTx const& jt, SField const& field, BadMPTAmount const& bad) {
auto const roundTripped = roundTrip(*jt.stx);
auto const persisted = roundTripped.getFieldAmount(field);
BEAST_EXPECT(persisted.holds<MPTIssue>());
BEAST_EXPECT(persisted.mantissa() == bad.mantissa);
BEAST_EXPECT(persisted.exponent() == 0);
BEAST_EXPECT(persisted.negative() == bad.negative);
BEAST_EXPECT(persisted.mpt().value() == bad.mptValue);
if (!bad.negative)
BEAST_EXPECT(persisted.mantissa() > kMaxMpTokenAmount);
};
for (auto const& bad : badMPTAmounts)
{
testcase("fixCleanup3_2_0 rejects non-canonical MPT Payment amounts");
{
Env env{*this, withoutFixAndV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto malformedHolderToHolder = withNonCanonicalMPTAmount(
env.jt(pay(alice, bob, STAmount{issue, std::uint64_t{1}})),
sfAmount,
badAmount,
alice);
expectRoundTripBadMPT(malformedHolderToHolder, sfAmount, bad);
malformedHolderToHolder.ter = bad.holderSourcePreFixTer;
env.submit(malformedHolderToHolder);
env.close();
BEAST_EXPECT(
(env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
env.enableFeature(fixCleanup3_2_0);
env.close();
env(env.jt(pay(bob, alice, STAmount{issue, std::uint64_t{1}})), Ter{tesSUCCESS});
env.close();
BEAST_EXPECT(
(env.balance(alice, issue).value() == STAmount{MPTAmount{10'001}, issue}));
BEAST_EXPECT(
(env.balance(bob, issue).value() == STAmount{MPTAmount{9'999}, issue}));
BEAST_EXPECT(
(env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
}
{
Env env{*this, envconfig(), withoutFixAndV2, nullptr, beast::Severity::Disabled};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto malformedIssuerToHolder = withNonCanonicalMPTAmount(
env.jt(pay(gw, alice, STAmount{issue, std::uint64_t{1}})),
sfAmount,
badAmount,
gw);
expectRoundTripBadMPT(malformedIssuerToHolder, sfAmount, bad);
malformedIssuerToHolder.ter = bad.issuerToHolderPreFixTer;
env.submit(malformedIssuerToHolder);
env.close();
BEAST_EXPECT(
(env.balance(alice, issue).value() ==
STAmount{MPTAmount{bad.issuerToHolderAliceAfterPreFix}, issue}));
BEAST_EXPECT(
(env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(gw, issue).value() ==
STAmount{MPTAmount{bad.issuerToHolderIssuerAfterPreFix}, issue}));
env.enableFeature(fixCleanup3_2_0);
env.close();
env(env.jt(pay(alice, gw, STAmount{issue, std::uint64_t{1}})), Ter{tesSUCCESS});
env.close();
BEAST_EXPECT(
(env.balance(alice, issue).value() ==
STAmount{MPTAmount{bad.issuerToHolderAliceAfterPostFix}, issue}));
BEAST_EXPECT(
(env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(gw, issue).value() ==
STAmount{MPTAmount{bad.issuerToHolderIssuerAfterPostFix}, issue}));
}
{
Env env{*this, withoutFixAndV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto malformedHolderToIssuer = withNonCanonicalMPTAmount(
env.jt(pay(alice, gw, STAmount{issue, std::uint64_t{1}})),
sfAmount,
badAmount,
alice);
expectRoundTripBadMPT(malformedHolderToIssuer, sfAmount, bad);
malformedHolderToIssuer.ter = bad.holderSourcePreFixTer;
env.submit(malformedHolderToIssuer);
env.close();
BEAST_EXPECT(
(env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
env.enableFeature(fixCleanup3_2_0);
env.close();
env(env.jt(pay(gw, alice, STAmount{issue, std::uint64_t{1}})), Ter{tesSUCCESS});
env.close();
BEAST_EXPECT(
(env.balance(alice, issue).value() == STAmount{MPTAmount{10'001}, issue}));
BEAST_EXPECT(
(env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(gw, issue).value() == STAmount{MPTAmount{-20'001}, issue}));
}
{
Env env{*this, withFixAndWithoutV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(pay(alice, bob, STAmount{issue, std::uint64_t{1}})),
sfAmount,
badAmount,
alice);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
{
Env env{*this, withFixAndWithoutV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(pay(gw, alice, STAmount{issue, std::uint64_t{1}})),
sfAmount,
badAmount,
gw);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
{
Env env{*this, withFixAndWithoutV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(pay(alice, gw, STAmount{issue, std::uint64_t{1}})),
sfAmount,
badAmount,
alice);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
testcase("fixCleanup3_2_0 rejects non-canonical MPT Check amounts");
{
Env env{*this, envconfig(), withoutFix, nullptr, beast::Severity::Disabled};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badSendMax = badMPTAmount(issue, bad);
auto const checkSeq = env.seq(alice);
auto tx = withNonCanonicalMPTAmount(
env.jt(check::create(alice, bob, STAmount{issue, std::uint64_t{10}})),
sfSendMax,
badSendMax,
alice);
tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
env.submit(tx);
env.close();
auto const checkKeylet = keylet::check(alice.id(), checkSeq);
auto const sleCheck = env.le(checkKeylet);
BEAST_EXPECT((sleCheck != nullptr) == !bad.negative);
if (sleCheck && !bad.negative)
{
auto const persisted = sleCheck->getFieldAmount(sfSendMax);
BEAST_EXPECT(persisted.holds<MPTIssue>());
BEAST_EXPECT(persisted.mantissa() == bad.mantissa);
BEAST_EXPECT(persisted.negative() == bad.negative);
}
}
{
Env env{*this, envconfig(), withoutFix, nullptr, beast::Severity::Disabled};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badSendMax = badMPTAmount(issue, bad);
auto const checkSeq = env.seq(alice);
auto tx = withNonCanonicalMPTAmount(
env.jt(check::create(alice, bob, STAmount{issue, std::uint64_t{10}})),
sfSendMax,
badSendMax,
alice);
tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
env.submit(tx);
env.close();
auto const checkKeylet = keylet::check(alice.id(), checkSeq);
BEAST_EXPECT((env.le(checkKeylet) != nullptr) == !bad.negative);
if (!bad.negative)
{
// CheckCancel has no amount fields, but it must be able to
// remove a malformed legacy Check while the fix is disabled.
env(env.jt(check::cancel(alice, checkKeylet.key)), Ter{tesSUCCESS});
env.close();
BEAST_EXPECT(env.le(checkKeylet) == nullptr);
}
}
{
Env env{*this, envconfig(), withoutFix, nullptr, beast::Severity::Disabled};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badSendMax = badMPTAmount(issue, bad);
auto const checkSeq = env.seq(alice);
auto tx = withNonCanonicalMPTAmount(
env.jt(check::create(alice, bob, STAmount{issue, std::uint64_t{10}})),
sfSendMax,
badSendMax,
alice);
tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
env.submit(tx);
env.close();
auto const checkKeylet = keylet::check(alice.id(), checkSeq);
BEAST_EXPECT((env.le(checkKeylet) != nullptr) == !bad.negative);
if (!bad.negative)
{
env.enableFeature(fixCleanup3_2_0);
env.close();
// Once the fix is enabled, CheckCancel should still remove
// a legacy Check because it does not consume the bad amount.
env(env.jt(check::cancel(alice, checkKeylet.key)), Ter{tesSUCCESS});
env.close();
BEAST_EXPECT(env.le(checkKeylet) == nullptr);
}
}
{
Env env{*this, envconfig(), withoutFix, nullptr, beast::Severity::Disabled};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badSendMax = badMPTAmount(issue, bad);
auto const checkSeq = env.seq(alice);
auto tx = withNonCanonicalMPTAmount(
env.jt(check::create(alice, bob, STAmount{issue, std::uint64_t{10}})),
sfSendMax,
badSendMax,
alice);
tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
env.submit(tx);
env.close();
auto const checkKeylet = keylet::check(alice.id(), checkSeq);
BEAST_EXPECT((env.le(checkKeylet) != nullptr) == !bad.negative);
if (!bad.negative)
{
env.enableFeature(fixCleanup3_2_0);
env.close();
auto const cashAmount = STAmount{sfAmount, issue, std::uint64_t{1}, 0, false};
env(env.jt(check::cash(bob, checkKeylet.key, cashAmount)), Ter{tefBAD_LEDGER});
env.close();
BEAST_EXPECT(env.le(checkKeylet) != nullptr);
}
}
{
Env env{*this, withoutFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const sendMax = STAmount{sfSendMax, issue, std::uint64_t{10}, 0, false};
auto const checkSeq = env.seq(alice);
env(env.jt(check::create(alice, bob, sendMax)), Ter{tesSUCCESS});
env.close();
auto const badCashAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
check::cash(
bob,
keylet::check(alice.id(), checkSeq).key,
STAmount{issue, std::uint64_t{1}})),
sfAmount,
badCashAmount,
bob);
expectRoundTripBadMPT(tx, sfAmount, bad);
tx.ter = bad.holderSourcePreFixTer;
env.submit(tx);
env.close();
BEAST_EXPECT(env.le(keylet::check(alice.id(), checkSeq)) != nullptr);
BEAST_EXPECT(
(env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
}
{
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const sendMax = STAmount{sfSendMax, issue, std::uint64_t{10}, 0, false};
auto const checkSeq = env.seq(alice);
env(env.jt(check::create(alice, bob, sendMax)), Ter{tesSUCCESS});
env.close();
auto const badCashAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
check::cash(
bob,
keylet::check(alice.id(), checkSeq).key,
STAmount{issue, std::uint64_t{1}})),
sfAmount,
badCashAmount,
bob);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
testcase("fixCleanup3_2_0 rejects non-canonical MPT Escrow amounts");
{
Env env{*this, withoutFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const escrowSeq = env.seq(alice);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
escrow::create(alice, bob, STAmount{issue, std::uint64_t{1}}),
escrow::kFinishTime(env.now() + 1s)),
sfAmount,
badAmount,
alice);
tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tecINSUFFICIENT_FUNDS};
env.submit(tx);
env.close();
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), escrowSeq)) == nullptr);
}
{
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
escrow::create(alice, bob, STAmount{issue, std::uint64_t{1}}),
escrow::kFinishTime(env.now() + 1s)),
sfAmount,
badAmount,
alice);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
testcase("fixCleanup3_2_0 rejects non-canonical MPT Clawback amounts");
{
Env env{*this, withoutFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(claw(gw, STAmount{issue, std::uint64_t{1}}, bob)),
sfAmount,
badAmount,
gw);
expectRoundTripBadMPT(tx, sfAmount, bad);
tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
env.submit(tx);
env.close();
MPTValue const bobAfter = bad.negative ? MPTValue{10'000} : MPTValue{0};
MPTValue const gwAfter = bad.negative ? MPTValue{-20'000} : MPTValue{-10'000};
BEAST_EXPECT(
(env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(bob, issue).value() == STAmount{MPTAmount{bobAfter}, issue}));
BEAST_EXPECT(
(env.balance(gw, issue).value() == STAmount{MPTAmount{gwAfter}, issue}));
}
{
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(claw(gw, STAmount{issue, std::uint64_t{1}}, bob)),
sfAmount,
badAmount,
gw);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
env.close();
BEAST_EXPECT(
(env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
BEAST_EXPECT(
(env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
}
testcase("featureMPTokensV2 disabled rejects MPT OfferCreate amounts");
{
Env env{*this, withoutFixAndV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badTakerPays = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(offer(alice, STAmount{issue, std::uint64_t{1}}, XRP(10))),
sfTakerPays,
badTakerPays,
alice);
expectRoundTripBadMPT(tx, sfTakerPays, bad);
tx.ter = temDISABLED;
env.submit(tx);
}
{
Env env{*this, withFixAndWithoutV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badTakerPays = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(offer(alice, STAmount{issue, std::uint64_t{1}}, XRP(10))),
sfTakerPays,
badTakerPays,
alice);
tx.ter = temDISABLED;
env.submit(tx);
}
{
// sfTakerPays is MPT: both amendments active. Negative offers
// fail in OfferCreate::preflight() before the universal check;
// positive non-canonical amounts fail in the universal check.
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badTakerPays = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(offer(alice, STAmount{issue, std::uint64_t{1}}, XRP(10))),
sfTakerPays,
badTakerPays,
alice);
tx.ter = TER{temBAD_AMOUNT};
env.submit(tx);
}
{
// sfTakerGets is MPT: both amendments active. Negative offers
// fail in OfferCreate::preflight() before the universal check;
// positive non-canonical amounts fail in the universal check.
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badTakerGets = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(offer(alice, XRP(10), STAmount{issue, std::uint64_t{1}})),
sfTakerGets,
badTakerGets,
alice);
tx.ter = TER{temBAD_AMOUNT};
env.submit(tx);
}
testcase("featureMPTokensV2 disabled rejects MPT AMMCreate amounts");
{
Env env{*this, withoutFixAndV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
AMM::createJv(alice.id(), STAmount{issue, std::uint64_t{1}}, XRP(1), 0),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
alice);
expectRoundTripBadMPT(tx, sfAmount, bad);
tx.ter = temDISABLED;
env.submit(tx);
}
{
Env env{*this, withFixAndWithoutV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
AMM::createJv(alice.id(), STAmount{issue, std::uint64_t{1}}, XRP(1), 0),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
alice);
tx.ter = temDISABLED;
env.submit(tx);
}
{
// sfAmount is MPT: both amendments active, expect temBAD_AMOUNT
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
AMM::createJv(alice.id(), STAmount{issue, std::uint64_t{1}}, XRP(1), 0),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
alice);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
testcase("featureMPTokensV2 disabled rejects MPT AMMDeposit amounts");
{
Env env{*this, withoutFixAndV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
AMM::depositJv(
{.account = alice,
.asset1In = STAmount{issue, std::uint64_t{1}},
.assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
alice);
expectRoundTripBadMPT(tx, sfAmount, bad);
tx.ter = temDISABLED;
env.submit(tx);
}
{
Env env{*this, withFixAndWithoutV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
AMM::depositJv(
{.account = alice,
.asset1In = STAmount{issue, std::uint64_t{1}},
.assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
alice);
tx.ter = temDISABLED;
env.submit(tx);
}
{
// sfAmount is MPT: both amendments active, expect temBAD_AMOUNT
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
AMM::depositJv(
{.account = alice,
.asset1In = STAmount{issue, std::uint64_t{1}},
.assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
alice);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
testcase("featureMPTokensV2 disabled rejects MPT AMMWithdraw amounts");
{
Env env{*this, withoutFixAndV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
AMM::withdrawJv(
{.account = alice,
.asset1Out = STAmount{issue, std::uint64_t{1}},
.assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
alice);
expectRoundTripBadMPT(tx, sfAmount, bad);
tx.ter = temDISABLED;
env.submit(tx);
}
{
Env env{*this, withFixAndWithoutV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
AMM::withdrawJv(
{.account = alice,
.asset1Out = STAmount{issue, std::uint64_t{1}},
.assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
alice);
tx.ter = temDISABLED;
env.submit(tx);
}
{
// sfAmount is MPT: both amendments active, expect temBAD_AMOUNT
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
AMM::withdrawJv(
{.account = alice,
.asset1Out = STAmount{issue, std::uint64_t{1}},
.assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
alice);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
testcase("featureMPTokensV2 disabled rejects MPT AMMClawback amounts");
{
Env env{*this, withoutFixAndV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
amm::ammClawback(
gw,
alice,
Asset{issue},
Asset{xrpIssue()},
std::make_optional(STAmount{issue, std::uint64_t{1}})),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
gw);
expectRoundTripBadMPT(tx, sfAmount, bad);
tx.ter = temDISABLED;
env.submit(tx);
}
{
Env env{*this, withFixAndWithoutV2};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
amm::ammClawback(
gw,
alice,
Asset{issue},
Asset{xrpIssue()},
std::make_optional(STAmount{issue, std::uint64_t{1}})),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
gw);
tx.ter = temDISABLED;
env.submit(tx);
}
{
// sfAmount is MPT: both amendments active, expect temBAD_AMOUNT
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
auto tx = withNonCanonicalMPTAmount(
env.jt(
amm::ammClawback(
gw,
alice,
Asset{issue},
Asset{xrpIssue()},
std::make_optional(STAmount{issue, std::uint64_t{1}})),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
gw);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
testcase("fixCleanup3_2_0 rejects non-canonical MPT VaultClawback amounts");
{
Env env{*this, withFix};
env.fund(XRP(100'000), alice, bob, gw);
env.close();
auto const issue = makeIssue(env);
auto const badAmount = badMPTAmount(issue, bad);
uint256 const fakeVaultId = keylet::vault(gw.id(), 1).key;
auto tx = withNonCanonicalMPTAmount(
env.jt(
Vault::clawback(
{.issuer = gw,
.id = fakeVaultId,
.holder = alice,
.amount = STAmount{issue, std::uint64_t{1}}}),
Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
sfAmount,
badAmount,
gw);
tx.ter = temBAD_AMOUNT;
env.submit(tx);
}
}
}
void
testTxJsonMetaFields(FeatureBitset features)
{
@@ -6947,7 +7812,7 @@ public:
// Test MPT Amount is invalid in Tx, which don't support MPT
testMPTInvalidInTx(all);
testNonCanonicalMPTAmountCleanup(all);
// Test parsed MPTokenIssuanceID in API response metadata
testTxJsonMetaFields(all);

View File

@@ -973,7 +973,7 @@ public:
// Offers with negative amounts
{
env(offer(alice, -usd(1'000), XRP(1'000)), Ter(temBAD_OFFER));
env(offer(alice, -usd(1'000), XRP(1'000)), Ter(temBAD_AMOUNT));
env.require(Owners(alice, 1), offers(alice, 0));
}