From dcd2ff0b5f2019c40c3f38640e751426df97d3c8 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Sat, 23 May 2026 02:40:26 -0400 Subject: [PATCH] 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> --- include/xrpl/protocol/STAmount.h | 17 +- include/xrpl/tx/Transactor.h | 12 + include/xrpl/tx/invariants/InvariantCheck.h | 16 + .../xrpl/tx/transactors/escrow/EscrowCreate.h | 3 + src/libxrpl/protocol/STAmount.cpp | 46 + src/libxrpl/tx/Transactor.cpp | 10 + src/libxrpl/tx/invariants/InvariantCheck.cpp | 31 + .../tx/transactors/check/CheckCash.cpp | 5 + .../tx/transactors/escrow/EscrowCreate.cpp | 12 +- src/test/app/Invariants_test.cpp | 57 ++ src/test/app/MPToken_test.cpp | 867 +++++++++++++++++- src/test/app/OfferMPT_test.cpp | 2 +- 12 files changed, 1074 insertions(+), 4 deletions(-) diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index c576c0da31..bf3e25eedb 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -3,11 +3,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -593,12 +595,25 @@ STAmount::value() const noexcept return *this; } -inline bool +[[nodiscard]] inline bool isLegalNet(STAmount const& value) { return !value.native() || (value.mantissa() <= STAmount::kMaxNativeN); } +[[nodiscard]] inline bool +isLegalMPT(STAmount const& value) +{ + return !value.holds() || + (!value.negative() && value.exponent() == 0 && value.mantissa() <= kMaxMpTokenAmount); +} + +/* Check recursively if an object has invalid MPTAmount or XRPAmount in STAmount field. + * Calls isLegalNet() and isLegalMPT(). + */ +[[nodiscard]] bool +hasInvalidAmount(STBase const& field, beast::Journal j); + //------------------------------------------------------------------------------ // // Operators diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index 61d943c4d5..1440a5097f 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -398,6 +398,15 @@ private: static NotTEC preflight2(PreflightContext const& ctx); + /** Universal validations + - Valid MPTAmount and XRPAmount + + Do not try to call preflightUniversal from preflight() in derived classes. See + the description of invokePreflight for details. + */ + static NotTEC + preflightUniversal(PreflightContext const& ctx); + /** Check transaction-specific invariants only. * * Walks every modified ledger entry via visitInvariantEntry, then @@ -463,6 +472,9 @@ Transactor::invokePreflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx, T::getFlagsMask(ctx))) return ret; + if (auto const ret = preflightUniversal(ctx)) + return ret; + if (auto const ret = T::preflight(ctx)) return ret; diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index 5996039bf0..d4c0154269 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -373,6 +373,21 @@ public: finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); }; +/** Verify that MPT/XRP STAmounts are canonical in any ledger entries left after the + * transaction applies. + */ +class ValidAmounts +{ + std::vector> afterEntries_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + [[nodiscard]] bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; +}; + // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -402,6 +417,7 @@ using InvariantChecks = std::tuple< ValidLoan, ValidVault, ValidMPTPayment, + ValidAmounts, ValidMPTTransfer>; /** diff --git a/include/xrpl/tx/transactors/escrow/EscrowCreate.h b/include/xrpl/tx/transactors/escrow/EscrowCreate.h index 8682ed7369..2e9da89896 100644 --- a/include/xrpl/tx/transactors/escrow/EscrowCreate.h +++ b/include/xrpl/tx/transactors/escrow/EscrowCreate.h @@ -16,6 +16,9 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index c40beabf12..1ba9cd042f 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -19,8 +19,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -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(&field)) + return !isLegalMPT(*amount) || !isLegalNet(*amount); + + if (auto const object = dynamic_cast(&field)) + return hasInvalidAmount(*object, depth + 1, j); + + if (auto const array = dynamic_cast(&field)) + return hasInvalidAmount(*array, depth + 1, j); + + return false; +} + +bool +hasInvalidAmount(STBase const& field, beast::Journal j) +{ + return hasInvalidAmount(field, 0, j); +} + //------------------------------------------------------------------------------ // // Arithmetic diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 97f2cabff2..28fa059902 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include // 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) diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 3e51b6f877..0154dca747 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -1080,4 +1080,35 @@ NoModifiedUnmodifiableFields::finalize( return true; } +void +ValidAmounts::visitEntry( + bool isDelete, + std::shared_ptr const&, + std::shared_ptr 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 diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index cfc133b501..af903ff177 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -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."; diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 0b1db125f6..4de302db3e 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -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()) + return ctx.rules.enabled(featureMPTokensV1); + return true; +} + template static NotTEC escrowCreatePreflightHelper(PreflightContext const& ctx); @@ -103,7 +113,7 @@ template <> NotTEC escrowCreatePreflightHelper(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]; diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 6fd1904992..cc16948553 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -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(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(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"}}, diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 6906c4aba0..3d6cff0885 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -25,11 +25,13 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -49,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -57,13 +60,17 @@ #include #include +#include #include #include #include +#include +#include #include #include #include #include +#include #include #include #include @@ -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::min(); + MPTValue const mptMax = std::numeric_limits::max(); + std::uint64_t const u64Max = std::numeric_limits::max(); + std::uint64_t const firstInvalidMPTMantissa = static_cast(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 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(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()); + 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()); + 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(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(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(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(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(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(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(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(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(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(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(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(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(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); diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index 3f88e57bc7..e9366f7c32 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -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)); }