mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-01 03:21:04 +00:00
Merge remote-tracking branch 'xrplf/develop' into sponsor
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user