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

@@ -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));
}