59 auto const nonCanonicalMPTAmount = [&](
SField const& field) {
68 auto const negativeMPTAmount = [&](
SField const& field) {
71 auto const nonCanonicalMPTPayment = [&]() {
73 tx.setFieldAmount(sfAmount, nonCanonicalMPTAmount(sfAmount));
82 nonCanonicalMPTPayment(),
86 {{
"ledger entry contains non-canonical MPT or XRP amount"}},
94 sleNew->setAccountID(sfAccount, a1.
id());
95 sleNew->setAccountID(sfDestination, a2.id());
96 sleNew->setFieldAmount(sfSendMax, nonCanonicalMPTAmount(sfSendMax));
97 ac.view().insert(sleNew);
102 {{
"ledger entry contains non-canonical MPT or XRP amount"}},
110 sleNew->setAccountID(sfAccount, a1.
id());
111 sleNew->setAccountID(sfDestination, a2.id());
112 sleNew->setFieldAmount(sfSendMax, negativeMPTAmount(sfSendMax));
113 ac.view().insert(sleNew);
119 {{
"OutstandingAmount overflow"}},
128 sleNew->setFieldU64(sfOutstandingAmount, 110);
129 sleNew->setFieldU64(sfMaximumAmount, 100);
130 ac.view().insert(sleNew);
136 {{
"invalid OutstandingAmount balance"}},
145 sleNew->setFieldU64(sfOutstandingAmount, 100);
146 sleNew->setFieldU64(sfMaximumAmount, 100);
147 ac.view().insert(sleNew);
150 sleNew->setFieldU64(sfMPTAmount, 90);
151 ac.view().insert(sleNew);
162 return update(
id, ac, a1);
169 env.fund(
XRP(1'000), gw);
171 {.env = env, .issuer = gw, .holders = {a1}, .pay = 100, .maxAmt = 100});
177 "invalid OutstandingAmount balance",
182 sle->setFieldU64(sfMPTAmount, 101);
191 sle->setFieldU64(sfOutstandingAmount, 101);
206 env.fund(
XRP(1'000), gw);
208 {.env = env, .issuer = gw, .holders = {a1, a2}, .pay = 50, .maxAmt = 1'000});
218 if (!sleIss || !sleTok)
220 (*sleIss)[sfOutstandingAmount] = (*sleIss)[sfOutstandingAmount] + 10;
221 (*sleTok)[sfMPTAmount] = (*sleTok)[sfMPTAmount] + 10;
234 if (!sleIss || !sleA || !sleB)
236 (*sleIss)[sfFlags] = (*sleIss)[sfFlags] | lsfMPTCanTransfer;
237 (*sleA)[sfMPTAmount] = (*sleA)[sfMPTAmount] - 10;
238 (*sleB)[sfMPTAmount] = (*sleB)[sfMPTAmount] + 10;
255 {{
"OutstandingAmount balance changed on failure"}},
265 {{
"OutstandingAmount balance changed on failure"}},
275 {{
"MPToken balance changed on failure"}},
285 {{
"MPToken balance changed on failure"}},
297 {{
"OutstandingAmount balance changed on failure"}},
307 {{
"MPToken balance changed on failure"}},
323 if (!sleTok || (*sleTok)[sfMPTAmount] < 10)
326 (*sleTok)[sfMPTAmount] = (*sleTok)[sfMPTAmount] - 10;
327 sleTok->setFieldU64(sfLockedAmount, 10);
334 {{
"MPToken balance changed on failure"}},
345 {{
"MPToken balance changed on failure"}},
363 env.fund(
XRP(1'000), gw);
364 MPTTester const mpt({.env = env, .issuer = gw, .holders = {a1, a2}, .maxAmt = 100});
370 if (!sleTok || (*sleTok)[sfMPTAmount] != 0)
378 {{
"MPToken deleted on failure"}},
392 Account const issuer{
"issuer"};
393 Account const holder{
"holder"};
395 env.
fund(
XRP(1'000), issuer, holder, other);
396 auto const usd = issuer[
"USD"];
397 env.
trust(usd(100), holder);
398 env(
pay(issuer, holder, usd(100)));
405 {{
"Invariant failed: trustline clawback balance change is invalid"}},
413 if (holder.
id() > issuer.
id())
415 sle->setFieldAmount(sfBalance, balance);
423 tx[sfAccount] = issuer.
id();
432 Account const issuer{
"issuer"};
433 Account const holder{
"holder"};
435 env.
fund(
XRP(1'000), issuer, holder, other);
436 auto const usd = issuer[
"USD"];
437 env.
trust(usd(100), holder);
438 env(
pay(issuer, holder, usd(100)));
459 tx[sfAccount] = issuer.
id();
467 Env env(*
this,
all_ - featureMPTokensV2);
468 Account const issuer{
"issuer"};
469 Account const holder{
"holder"};
471 env.
fund(
XRP(1'000), issuer, holder, other);
472 auto const usd = issuer[
"USD"];
473 env.
trust(usd(100), holder);
474 env(
pay(issuer, holder, usd(100)));
481 {{
"Invariant failed: trustline clawback balance change is invalid"}},
489 if (holder.
id() > issuer.
id())
491 sle->setFieldAmount(sfBalance, balance);
499 tx[sfAccount] = issuer.
id();
508 Account const issuer{
"issuer"};
509 Account const holder{
"holder"};
511 env.
fund(
XRP(1'000), issuer, holder, other);
513 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
520 {{
"Invariant failed: MPT clawback balance change is invalid"}},
524 if (!sleToken || !sleIssuance)
527 sleToken->setFieldU64(sfMPTAmount, 80);
528 sleIssuance->setFieldU64(sfOutstandingAmount, 80);
537 tx[sfAccount] = issuer.
id();
538 tx[sfHolder] = holder.
id();
547 Account const issuer{
"issuer"};
548 Account const holder{
"holder"};
550 env.
fund(
XRP(1'000), issuer, holder, other);
551 auto const usd = issuer[
"USD"];
552 env.
trust(usd(100), holder);
553 env(
pay(issuer, holder, usd(100)));
555 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
562 {{
"Invariant failed: trustline and MPToken both changed"}},
568 if (!sleLine || !sleToken || !sleIssuance)
572 if (holder.
id() > issuer.
id())
574 sleLine->setFieldAmount(sfBalance, balance);
575 sleToken->setFieldU64(sfMPTAmount, 90);
576 sleIssuance->setFieldU64(sfOutstandingAmount, 90);
586 tx[sfAccount] = issuer.
id();
587 tx[sfHolder] = holder.
id();
598 Account const issuer{
"issuer"};
599 Account const holder{
"holder"};
601 env.
fund(
XRP(1'000), issuer, holder, other);
602 auto const usd = issuer[
"USD"];
603 auto const eur = issuer[
"EUR"];
604 env.
trust(eur(100), holder);
605 env(
pay(issuer, holder, eur(100)));
612 {{
"Invariant failed: trustline clawback changed the wrong line"}},
619 if (holder.
id() > issuer.
id())
621 sle->setFieldAmount(sfBalance, balance);
629 tx[sfAccount] = issuer.
id();
638 Account const issuer{
"issuer"};
639 Account const holder{
"holder"};
641 env.
fund(
XRP(1'000), issuer, holder, other);
642 auto const usd = issuer[
"USD"];
643 env.
trust(usd(100), holder);
644 env(
pay(issuer, holder, usd(100)));
651 {{
"Invariant failed: trustline or MPT balance is negative"}},
659 if (holder.
id() < issuer.
id())
661 sle->setFieldAmount(sfBalance, balance);
669 tx[sfAccount] = issuer.
id();
679 Account const issuer{
"issuer"};
680 Account const holder{
"holder"};
682 env.
fund(
XRP(1'000), issuer, holder, other);
683 auto const usd = issuer[
"USD"];
685 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
692 {{
"Invariant failed: trustline clawback changed the wrong line"}},
696 if (!sleToken || !sleIssuance)
698 sleToken->setFieldU64(sfMPTAmount, 90);
699 sleIssuance->setFieldU64(sfOutstandingAmount, 90);
708 tx[sfAccount] = issuer.
id();
717 Account const issuer{
"issuer"};
718 Account const holder{
"holder"};
720 env.
fund(
XRP(1'000), issuer, holder, other);
721 auto const usd = issuer[
"USD"];
722 env.
trust(usd(100), holder);
723 env(
pay(issuer, holder, usd(100)));
730 {{
"Invariant failed: trustline clawback amount is invalid"}},
737 if (holder.
id() > issuer.
id())
739 sle->setFieldAmount(sfBalance, balance);
747 tx[sfAccount] = issuer.
id();
756 Account const issuer{
"issuer"};
757 Account const holder{
"holder"};
759 env.
fund(
XRP(1'000), issuer, holder, other);
761 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
768 {{
"Invariant failed: MPT clawback missing holder"}},
772 if (!sleToken || !sleIssuance)
774 sleToken->setFieldU64(sfMPTAmount, 90);
775 sleIssuance->setFieldU64(sfOutstandingAmount, 90);
784 tx[sfAccount] = issuer.
id();
793 Account const issuer{
"issuer"};
794 Account const holder{
"holder"};
796 env.
fund(
XRP(1'000), issuer, holder, other);
798 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
805 {{
"Invariant failed: MPT clawback token is missing"}},
809 if (!sleToken || !sleIssuance)
812 sleIssuance->setFieldU64(sfOutstandingAmount, 0);
821 tx[sfAccount] = issuer.
id();
822 tx[sfHolder] = holder.
id();
831 Account const issuer{
"issuer"};
832 Account const holder{
"holder"};
834 env.
fund(
XRP(1'000), issuer, holder, other);
838 .holders = {holder, other},
847 {{
"Invariant failed: MPT clawback changed the wrong token"}},
851 if (!sleToken || !sleIssuance)
853 sleToken->setFieldU64(sfMPTAmount, 90);
854 sleIssuance->setFieldU64(sfOutstandingAmount, 190);
863 tx[sfAccount] = issuer.
id();
864 tx[sfHolder] = holder.
id();
873 Account const issuer{
"issuer"};
874 Account const holder{
"holder"};
876 env.
fund(
XRP(1'000), issuer, holder, other);
878 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
885 {{
"Invariant failed: MPT clawback amount is invalid"}},
889 if (!sleToken || !sleIssuance)
891 sleToken->setFieldU64(sfMPTAmount, 90);
892 sleIssuance->setFieldU64(sfOutstandingAmount, 90);
901 tx[sfAccount] = issuer.
id();
902 tx[sfHolder] = holder.
id();
914 for (
auto const& [tx, nTokens] :
tests)
917 {{
std::string(
"MPToken created for the MPT issuer")}},
923 auto seq = sle->getFieldU32(sfSequence);
924 for (
int i = 0; i < nTokens; ++i)
943 for (
auto const& tx : {ttAMM_WITHDRAW, ttAMM_CLAWBACK})
948 {{
"MPT authorize succeeded but created/deleted bad number of mptokens"}},
950 for (
auto const& a : {a1, a2, a3})
964 env.fund(
XRP(1'000), gw, a3);
965 MPTTester const mpt({.env = env, .issuer = gw, .holders = {a1, a2, a3}});
976 auto const insertHolderTokens =
981 auto seq = sle->getFieldU32(sfSequence);
982 for (
int i = 0; i < n; ++i)
987 (*sleNew)[sfAccount] = holder.id();
988 (*sleNew)[sfMPTokenIssuanceID] = mpt.
getMptID();
989 ac.view().insert(sleNew);
995 {{ttLOAN_SET, 3}, {ttVAULT_WITHDRAW, 2}}};
996 for (
auto const& [txnType, nTokens] : createOverCap)
999 {{
"MPT authorize succeeded but created/deleted bad number mptokens"}},
1001 return insertHolderTokens(a1, a2, ac, nTokens);
1009 auto const precloseTwoHolders = [&id](
Account const& a1,
Account const& a2,
Env& env) {
1011 env.fund(
XRP(1'000), gw);
1012 MPTTester const mpt({.env = env, .issuer = gw, .holders = {a1, a2}});
1017 {{ttLOAN_SET, 1}, {ttVAULT_WITHDRAW, 2}}};
1018 for (
auto const& [txnType, nTokens] : deleteOverCap)
1021 {{
"MPT authorize succeeded but created/deleted bad number mptokens"}},
1024 for (
int i = 0; i < nTokens; ++i)
1029 ac.view().erase(sle);
1036 precloseTwoHolders);
1044 {{
"sfReferenceHolding set on a new MPTokenIssuance by a "
1045 "non-VaultCreate transaction"}},
1052 sleNew->setFieldH256(sfReferenceHolding,
UInt256{1});
1067 {{
"sfReferenceHolding was modified on an existing "
1068 "MPTokenIssuance"}},
1077 sleIssuance->setFieldH256(sfReferenceHolding,
UInt256{2});
1085 Account const issuer{
"issuer"};
1086 env.fund(
XRP(10'000), issuer);
1089 mptt.
create({.flags = tfMPTCanTransfer | tfMPTCanLock});
1094 Vault const vault{env};
1095 auto [tx,
keylet] = vault.create({.owner = a1, .asset = asset});
1109 for (
auto const flag : std::views::values(getMPTokenIssuanceFlags()))
1111 allFlags &= ~lsfMPTLocked;
1115 Account const issuer{
"issuer"};
1116 env.fund(
XRP(10'000), issuer);
1119 mptt.
create({.flags = allFlags});
1126 for (
auto const flag : std::views::values(getMPTokenIssuanceFlags()))
1128 if (flag == lsfMPTLocked)
1130 Precheck const clearFlag = [&, flag](
1135 sleIssuance->setFieldU32(sfFlags, sleIssuance->getFlags() & ~flag);
1150 {{
"immutable MPTokenIssuance flag cleared"}},
1165 {{
"vault pseudo-account holding deleted by a "
1166 "non-VaultDelete transaction"}},
1171 auto const sleIssuance =
1173 if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
1186 Account const issuer{
"issuer"};
1187 env.fund(
XRP(10'000), issuer);
1190 mptt.
create({.flags = tfMPTCanTransfer | tfMPTCanLock});
1195 Vault const vault{env};
1196 auto [tx,
keylet] = vault.create({.owner = a1, .asset = asset});
1210 FeatureBitset const gatesEnabled{featureMPTokensV2, fixCleanup3_4_0};
1211 for (
auto const gates :
1213 gatesEnabled - featureMPTokensV2,
1214 gatesEnabled - fixCleanup3_4_0,
1217 for (
auto const& [tx, crossCurrencyPayment] : invalidTransferTests)
1219 for (
auto const flag :
1232 auto const isSuccess = !gates.any() || flag == 0 ||
1233 (tx == ttPAYMENT && !crossCurrencyPayment && (flag == ~lsfMPTCanTrade)) ||
1234 (tx == ttAMM_WITHDRAW &&
1235 (flag == ~lsfMPTCanTrade || flag == ~lsfMPTCanTransfer));
1240 {{isSuccess ?
"" :
"invalid MPToken transfer between holders"}},
1246 sle->at(sfMPTAmount) = v;
1253 if (flag == lsfMPTLocked)
1255 issuanceSle->at(sfFlags) = issuanceSle->at(sfFlags) | lsfMPTLocked;
1257 issuanceSle->at(sfOutstandingAmount) = 200;
1259 return update(a1, 101) && update(a2, 99);
1265 if (crossCurrencyPayment)
1274 env.fund(
XRP(1'000), gw);
1278 .holders = {a1, a2},
1280 .flags = createFlags});
1285 if (!gates[featureMPTokensV2])
1286 env.disableFeature(featureMPTokensV2);
1287 if (!gates[fixCleanup3_4_0])
1288 env.disableFeature(fixCleanup3_4_0);
1300 MPTTester mpt(env, a1, {.holders = {a2}, .fund =
false});
1301 mpt.
create({.flags = tfMPTCanTransfer});
1302 orphanID = mpt.issuanceID();
1305 mpt.authorize({.account = a2});
1312 {{
"orphaned MPToken balance changed"}},
1315 if (!sleTok || (*sleTok)[sfMPTAmount] != 0)
1317 (*sleTok)[sfMPTAmount] = (*sleTok)[sfMPTAmount] + 10;
1342 {{
"MPToken deleted on failure"}},
1367 auto const setupVault = [&](
Account const& a1,
1370 env.fund(
XRP(1'000), gw);
1371 env.trust(gw[
"IOU"](10'000), a1);
1372 env.trust(gw[
"IOU"](10'000), a2);
1374 env(
pay(gw, a1, gw[
"IOU"](500)));
1375 env(
pay(gw, a2, gw[
"IOU"](500)));
1378 Vault const vault{env};
1379 auto [
createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw[
"IOU"]});
1383 {.depositor = a1, .id = vaultKeylet.key, .amount = gw[
"IOU"](100)}));
1385 {.depositor = a2, .id = vaultKeylet.key, .amount = gw[
"IOU"](100)}));
1388 return {env.le(vaultKeylet)->at(sfShareMPTID), env.le(vaultKeylet)->at(sfAccount)};
1392 auto const precheck =
1398 (*sle1)[sfMPTAmount] -= 10;
1399 (*sle2)[sfMPTAmount] += 10;
1407 auto const preclose = [&](
Account const& a1,
Account const& a2,
Env& env) ->
bool {
1408 auto [sid, vid] = setupVault(a1, a2, env);
1410 env(
trust(gw, gw[
"IOU"](0),
Account{
"vaultPseudo", vid}, tfSetFreeze));
1417 {{
"invalid MPToken transfer between holders"}},
1427 auto const preclose = [&](
Account const& a1,
Account const& a2,
Env& env) ->
bool {
1428 auto [sid, vid] = setupVault(a1, a2, env);
1430 env(
trust(gw, gw[
"IOU"](0), a2, tfSetFreeze));
1437 {{
"invalid MPToken transfer between holders"}},
1451 testcase <<
"ValidConfidentialMPToken";
1457 auto const precloseConfidential =
1459 MPTTester mpt(env, a1, {.holders = {a2}, .fund =
false});
1460 mpt.
create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
1461 mptID = mpt.issuanceID();
1463 mpt.authorize({.account = a2});
1464 mpt.pay(a1, a2, 100);
1466 mpt.generateKeyPair(a1);
1467 mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
1469 mpt.generateKeyPair(a2);
1473 .holderPubKey = mpt.getPubKey(a2),
1480 {
"MPToken deleted with encrypted fields while COA > 0"},
1486 ac.view().erase(sleToken);
1492 precloseConfidential);
1496 {
"MPToken encrypted field existence inconsistency"},
1502 sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
1503 ac.view().update(sleToken);
1509 precloseConfidential);
1512 {
"MPToken encrypted field existence inconsistency"},
1517 sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
1518 sleToken->makeFieldAbsent(sfConfidentialBalanceInbox);
1519 sleToken->makeFieldAbsent(sfConfidentialBalanceSpending);
1520 sleToken->setFieldVL(sfAuditorEncryptedBalance,
Blob{0x00});
1521 ac.view().update(sleToken);
1527 precloseConfidential);
1530 auto const precloseNoPrivacy = [&mptID](
1532 MPTTester mpt(env, a1, {.holders = {a2}, .fund =
false});
1534 mpt.
create({.flags = tfMPTCanTransfer});
1535 mptID = mpt.issuanceID();
1536 mpt.authorize({.account = a2});
1537 mpt.pay(a1, a2, 100);
1542 {
"MPToken has encrypted fields but Issuance does not have "
1543 "lsfMPTCanHoldConfidentialBalance "
1551 sleToken->setFieldVL(sfConfidentialBalanceInbox,
Blob{0x00});
1552 sleToken->setFieldVL(sfConfidentialBalanceSpending,
Blob{0x00});
1553 sleToken->setFieldVL(sfIssuerEncryptedBalance,
Blob{0x00});
1554 ac.view().update(sleToken);
1564 {
"Confidential outstanding amount exceeds total outstanding amount"},
1570 sleIssuance->setFieldU64(sfConfidentialOutstandingAmount, 200);
1571 ac.view().update(sleIssuance);
1577 precloseConfidential);
1581 {
"Token conservation violation for MPT"},
1587 sleIssuance->setFieldU64(
1588 sfConfidentialOutstandingAmount,
1589 sleIssuance->getFieldU64(sfConfidentialOutstandingAmount) - 10);
1590 ac.view().update(sleIssuance);
1597 precloseConfidential);
1601 {
"Invariant failed: OutstandingAmount changed "
1602 "by confidential transaction that should not "
1603 "modify it for MPT"},
1608 sleIssuance->setFieldU64(
1609 sfOutstandingAmount, sleIssuance->getFieldU64(sfOutstandingAmount) + 1);
1610 ac.view().update(sleIssuance);
1616 precloseConfidential);
1621 {
"Invariant failed: MPTAmount changed by confidential "
1622 "transaction that should not modify this field."},
1627 sleToken->setFieldU64(sfMPTAmount, sleToken->getFieldU64(sfMPTAmount) + 1);
1628 ac.view().update(sleToken);
1636 precloseConfidential);
1640 {
"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending "
1643 Blob const kChangedConfidentialSpending = {0xBA, 0xDD};
1647 sleToken->setFieldVL(sfConfidentialBalanceSpending, kChangedConfidentialSpending);
1650 ac.view().update(sleToken);
1656 precloseConfidential);
1659 auto const precloseOrphan = [&mptID](
1661 MPTTester mpt(env, a1, {.holders = {a2}, .fund =
false});
1662 mpt.
create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
1663 mptID = mpt.issuanceID();
1664 mpt.authorize({.account = a2});
1667 mpt.generateKeyPair(a1);
1668 mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
1669 mpt.generateKeyPair(a2);
1673 .holderPubKey = mpt.getPubKey(a2),
1688 ac.view().erase(sleToken);