Merge remote-tracking branch 'xrplf/develop' into sponsor

This commit is contained in:
tequ
2026-04-08 11:47:16 +09:00
741 changed files with 7547 additions and 6324 deletions

View File

@@ -6,6 +6,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -827,7 +828,7 @@ class MPToken_test : public beast::unit_test::suite
env.fund(XRP(1'000), alice);
env.fund(XRP(1'000), bob);
STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
STAmount const mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
env(pay(alice, bob, mpt), ter(temDISABLED));
}
@@ -841,7 +842,7 @@ class MPToken_test : public beast::unit_test::suite
env.fund(XRP(1'000), alice);
env.fund(XRP(1'000), carol);
STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
STAmount const mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
Json::Value jv;
jv[jss::secret] = alice.name();
@@ -1457,7 +1458,7 @@ class MPToken_test : public beast::unit_test::suite
{
Env env{*this, features};
env.fund(XRP(1'000), alice, bob);
STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
STAmount const mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
Json::Value jv;
jv[jss::secret] = alice.name();
jv[jss::tx_json] = pay(alice, bob, mpt);
@@ -1805,7 +1806,7 @@ class MPToken_test : public beast::unit_test::suite
Account const alice("alice");
auto const USD = alice["USD"];
Account const carol("carol");
MPTIssue issue(makeMptID(1, alice));
MPTIssue const issue(makeMptID(1, alice));
STAmount mpt{issue, UINT64_C(100)};
auto const jvb = bridge(alice, USD, alice, USD);
for (auto const& feature : {features, features - featureMPTokensV1})
@@ -2885,7 +2886,7 @@ class MPToken_test : public beast::unit_test::suite
mptAlice.create(
{.metadata = "test", .ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata});
std::vector<std::string> metadatas = {
std::vector<std::string> const metadatas = {
"mutate metadata",
"mutate metadata 2",
"mutate metadata 3",
@@ -3281,6 +3282,123 @@ class MPToken_test : public beast::unit_test::suite
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
}
void
testMultiSendMaximumAmount(FeatureBitset features)
{
// Verify that directSendNoLimitMultiMPT correctly enforces MaximumAmount
// when the issuer sends to multiple receivers. Pre-fixSecurity3_1_3,
// a stale view.read() snapshot caused per-iteration checks to miss
// aggregate overflows. Post-fix, a running total is used instead.
testcase("Multi-send MaximumAmount enforcement");
using namespace test::jtx;
Account const issuer("issuer");
Account const alice("alice");
Account const bob("bob");
std::uint64_t constexpr maxAmt = 150;
Env env{*this, features};
MPTTester mptt(env, issuer, {.holders = {alice, bob}});
mptt.create({.maxAmt = maxAmt, .ownerCount = 1, .flags = tfMPTCanTransfer});
mptt.authorize({.account = alice});
mptt.authorize({.account = bob});
Asset const asset{MPTIssue{mptt.issuanceID()}};
// Each test case creates a fresh ApplyView and calls
// accountSendMulti from the issuer to the given receivers.
auto const runTest = [&](MultiplePaymentDestinations const& receivers,
TER expectedTer,
std::optional<std::uint64_t> expectedOutstanding,
std::string const& label) {
ApplyViewImpl av(&*env.current(), tapNONE);
auto const ter =
accountSendMulti(av, issuer.id(), asset, receivers, env.app().getJournal("View"), {});
BEAST_EXPECTS(ter == expectedTer, label);
// Only verify OutstandingAmount on success — on error the
// view may contain partial state and must be discarded.
if (expectedOutstanding)
{
auto const sle = av.peek(keylet::mptIssuance(mptt.issuanceID()));
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECTS(sle->getFieldU64(sfOutstandingAmount) == *expectedOutstanding, label);
}
};
using R = MultiplePaymentDestinations;
// Post-amendment: aggregate check with running total
runTest(
R{{alice.id(), 100}, {bob.id(), 100}},
tecPATH_DRY,
std::nullopt,
"aggregate exceeds max");
runTest(R{{alice.id(), 75}, {bob.id(), 75}}, tesSUCCESS, maxAmt, "aggregate at boundary");
runTest(R{{alice.id(), 50}, {bob.id(), 50}}, tesSUCCESS, 100, "aggregate within limit");
runTest(
R{{alice.id(), 150}, {bob.id(), 0}},
tesSUCCESS,
maxAmt,
"one receiver at max, other zero");
runTest(
R{{alice.id(), 151}, {bob.id(), 0}},
tecPATH_DRY,
std::nullopt,
"one receiver exceeds max, other zero");
// Issue 50 tokens so outstandingAmount is nonzero, then verify
// the third condition: outstandingAmount > maximumAmount - sendAmount - totalSendAmount
mptt.pay(issuer, alice, 50);
env.close();
// maxAmt=150, outstanding=50, so 100 more available
runTest(
R{{alice.id(), 50}, {bob.id(), 50}},
tesSUCCESS,
maxAmt,
"nonzero outstanding, aggregate at boundary");
runTest(
R{{alice.id(), 50}, {bob.id(), 51}},
tecPATH_DRY,
std::nullopt,
"nonzero outstanding, aggregate exceeds max");
runTest(
R{{alice.id(), 100}, {bob.id(), 0}},
tesSUCCESS,
maxAmt,
"nonzero outstanding, single send at remaining capacity");
runTest(
R{{alice.id(), 101}, {bob.id(), 0}},
tecPATH_DRY,
std::nullopt,
"nonzero outstanding, single send exceeds remaining capacity");
// Pre-amendment: the stale per-iteration check allows each
// individual send (100 <= 150) even though the aggregate (200)
// exceeds MaximumAmount. Preserved for ledger replay.
{
// KNOWN BUG (pre-fixSecurity3_1_3): preserved for ledger replay only
env.disableFeature(fixSecurity3_1_3);
runTest(
R{{alice.id(), 100}, {bob.id(), 100}},
tesSUCCESS,
250,
"pre-amendment allows over-send");
env.enableFeature(fixSecurity3_1_3);
}
}
public:
void
run() override
@@ -3288,6 +3406,7 @@ public:
using namespace test::jtx;
FeatureBitset const all{testable_amendments()};
testMultiSendMaximumAmount(all);
// MPTokenIssuanceCreate
testCreateValidation(all - featureSingleAssetVault);
testCreateValidation(all - featurePermissionedDomains);