Compare commits

...
Author SHA1 Message Date
Gregory Tsipenyuk 6cba6772e5 Merge branch 'develop' into gregtatcam/mpt/assorted-fixes-dev 2026-05-15 13:03:33 -04:00
Gregory Tsipenyuk 6b7151f7c2 Merge branch 'develop' into gregtatcam/mpt/assorted-fixes-dev 2026-05-15 11:37:34 -04:00
Gregory Tsipenyuk 8ffdfc5953 Fix clang-tidy errors 2026-05-03 17:15:39 -04:00
Gregory Tsipenyuk 1c8fc3c806 Fix clang-format and clang-tidy 2026-05-03 10:42:18 -04:00
Gregory Tsipenyuk c7e3dae68a Merge branch 'develop' into gregtatcam/mpt/assorted-fixes-dev 2026-05-03 10:39:50 -04:00
Gregory Tsipenyuk e46cb6bd78 Fix clang-tidy errors 2026-04-29 13:14:05 -04:00
Gregory Tsipenyuk 5c0c358ea6 Merge branch 'develop' into gregtatcam/mpt/assorted-fixes-dev 2026-04-29 09:31:37 -04:00
Gregory Tsipenyuk 6b71fb3f48 Merge branch 'develop' into gregtatcam/mpt/assorted-fixes-dev 2026-04-28 10:34:33 -04:00
Gregory Tsipenyuk 5ec090b258 Merge branch 'develop' into gregtatcam/mpt/assorted-fixes-dev 2026-04-22 06:35:21 -04:00
Gregory Tsipenyuk 35890680bf Merge branch 'develop' into gregtatcam/mpt/assorted-fixes-dev 2026-04-21 11:12:32 -04:00
Gregory Tsipenyuk d3e22a1251 Add testAutoCreateReserve() unit-test to verify MPT/IOU consistent in not checking the reserves when creating MPToken or Trustline on partial offer crossing. 2026-04-21 11:07:20 -04:00
Gregory Tsipenyuk 0f262a56cf Add comments 2026-04-20 07:51:51 -04:00
Gregory Tsipenyuk 392e69ae67 Fix clang-tidy errors. 2026-04-19 15:14:34 -04:00
Gregory Tsipenyuk 080830c4ae Fix clang-tidy errors. 2026-04-19 14:27:17 -04:00
Gregory Tsipenyuk 940dcef212 Merge branch 'develop' into gregtatcam/mpt/assorted-fixes-dev 2026-04-18 10:36:05 -04:00
Gregory Tsipenyuk 62bdfc6bf6 Add MPT to ValidAMM invariant. Add ValidAMM invariant unit-tests. 2026-04-18 10:29:51 -04:00
Gregory Tsipenyuk e85f010ef0 Fix AMMClawback return code on AllowTrustLineClawback and NoFreeze check. 2026-04-15 15:49:55 -04:00
Gregory Tsipenyuk af17ab2642 Merge branch 'develop' into gregtatcam/mpt/assorted-fixes-dev 2026-04-15 14:46:56 -04:00
Gregory Tsipenyuk 70fa0d7f79 Refactor requireAuth handling in ValidMPTTransfer.
* Combine frozen and reqAuth.
* Check senders and receivers for reqAuth.
2026-04-14 09:16:48 -04:00
Gregory Tsipenyuk a2ab6c79ac Add requireAuth invariant 2026-04-13 15:22:49 -04:00
Gregory Tsipenyuk 5439264bdb Fix MPTInvariant refactoring 2026-04-13 13:39:20 -04:00
Gregory TsipenyukandShawn Xie 62921eb913 Update src/libxrpl/tx/invariants/MPTInvariant.cpp
Co-authored-by: Shawn Xie <[email protected]>
2026-04-13 13:29:04 -04:00
Gregory Tsipenyuk 50cacddcf8 Add CheckCash can't transfer failure unit-test. 2026-04-10 12:25:01 -04:00
Gregory Tsipenyuk a839b8b92a Add ValidMPTTransfer invariant unit-tests. 2026-04-10 11:49:43 -04:00
Gregory Tsipenyuk 1a2c0e7d32 Improve ValidMPTTransfer::finalize() 2026-04-10 09:20:56 -04:00
Gregory Tsipenyuk 77df21b485 Address AI review 2026-04-09 17:25:17 -04:00
Gregory Tsipenyukandxrplf-ai-reviewer[bot] 63f270f00e Update src/libxrpl/tx/invariants/MPTInvariant.cpp
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-04-09 14:35:38 -04:00
Gregory Tsipenyuk 0a896b7957 Fix double adjustOwnerCount() in AMMWithdraw. 2026-04-09 13:06:28 -04:00
Gregory Tsipenyuk 12eb050bf8 Change tecFROZEN to tecLOCKED for locked MPT.
Replace checkMPTTxAllowed() with canMPTTradeAndTransfer().
2026-04-09 11:54:15 -04:00
Gregory Tsipenyuk d26d93130e Replace canTrade with canTransfer in CheckCreate/Cash (Check is not DEX tx). 2026-04-09 11:52:55 -04:00
Gregory Tsipenyuk a91e8ddad7 Add ValidMPTTransfer invariant. 2026-04-09 11:52:38 -04:00
21 changed files with 781 additions and 256 deletions

No files matched your search

+9 -13
View File
@@ -121,6 +121,15 @@ canTransfer(
[[nodiscard]] TER
canTrade(ReadView const& view, Asset const& asset);
/** Convenience to combine canTrade/Transfer. Returns tesSUCCESS if Asset is Issue.
*/
[[nodiscard]] TER
canMPTTradeAndTransfer(
ReadView const& v,
Asset const& asset,
AccountID const& from,
AccountID const& to);
//------------------------------------------------------------------------------
//
// Empty holding operations (MPT-specific)
@@ -227,17 +236,4 @@ issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
void
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
//------------------------------------------------------------------------------
//
// MPT DEX
//
//------------------------------------------------------------------------------
/* Return true if a transaction is allowed for the specified MPT/account. The
* function checks MPTokenIssuance and MPToken objects flags to determine if the
* transaction is allowed.
*/
TER
checkMPTTxAllowed(ReadView const& v, TxType tx, Asset const& asset, AccountID const& accountID);
} // namespace xrpl
@@ -54,9 +54,15 @@ enum class AuthType { StrongAuth, WeakAuth, Legacy };
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, Asset const& asset);
[[nodiscard]] TER
checkGlobalFrozen(ReadView const& view, Asset const& asset);
[[nodiscard]] bool
isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
[[nodiscard]] TER
checkIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
/**
* isFrozen check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
+2 -1
View File
@@ -399,7 +399,8 @@ using InvariantChecks = std::tuple<
ValidLoanBroker,
ValidLoan,
ValidVault,
ValidMPTPayment>;
ValidMPTPayment,
ValidMPTTransfer>;
/**
* @brief get a tuple of all invariant checks
+38
View File
@@ -56,4 +56,42 @@ public:
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
class ValidMPTTransfer
{
struct Value
{
std::optional<std::uint64_t> amtBefore;
std::optional<std::uint64_t> amtAfter;
};
// MPTID: {holder: Value}
hash_map<uint192, hash_map<AccountID, Value>> amount_;
// Deleted MPToken
// MPToken key: true if MPTAuthorized is set
hash_map<uint256, bool> deletedAuthorized_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
private:
/**
* @brief Check whether a holder is authorized to send or receive an MPToken.
*
* Deleted MPToken SLEs are no longer present in the view by the time
* finalize() runs, so their authorization state is captured during
* visitEntry() and stored in deletedAuthorized_. For deleted MPTokens,
* returns true if reqAuth is false or lsfMPTAuthorized was set at deletion.
* For existing MPTokens, returns the result of requireAuth()
*/
[[nodiscard]] bool
isAuthorized(
ReadView const& view,
MPTID const& mptid,
AccountID const& holder,
bool requireAuth) const;
};
} // namespace xrpl
+16 -62
View File
@@ -25,7 +25,6 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
@@ -524,6 +523,22 @@ canTrade(ReadView const& view, Asset const& asset)
});
}
TER
canMPTTradeAndTransfer(
ReadView const& view,
Asset const& asset,
AccountID const& from,
AccountID const& to)
{
if (!asset.holds<MPTIssue>())
return tesSUCCESS;
if (auto const ter = canTrade(view, asset); !isTesSuccess(ter))
return ter;
return canTransfer(view, asset, from, to);
}
TER
lockEscrowMPT(ApplyView& view, AccountID const& sender, STAmount const& amount, beast::Journal j)
{
@@ -891,65 +906,4 @@ issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amo
view.issuerSelfDebitHookMPT(issue, amount, available);
}
static TER
checkMPTAllowed(ReadView const& view, TxType txType, Asset const& asset, AccountID const& accountID)
{
if (!asset.holds<MPTIssue>())
return tesSUCCESS;
auto const& issuanceID = asset.get<MPTIssue>().getMptID();
auto const validTx = txType == ttAMM_CREATE || txType == ttAMM_DEPOSIT ||
txType == ttAMM_WITHDRAW || txType == ttOFFER_CREATE || txType == ttCHECK_CREATE ||
txType == ttCHECK_CASH || txType == ttPAYMENT;
XRPL_ASSERT(validTx, "xrpl::checkMPTAllowed : all MPT tx or DEX");
if (!validTx)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const& issuer = asset.getIssuer();
if (!view.exists(keylet::account(issuer)))
return tecNO_ISSUER; // LCOV_EXCL_LINE
auto const issuanceKey = keylet::mptIssuance(issuanceID);
auto const issuanceSle = view.read(issuanceKey);
if (!issuanceSle)
return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE
auto const flags = issuanceSle->getFlags();
if ((flags & lsfMPTLocked) != 0u)
return tecLOCKED; // LCOV_EXCL_LINE
// Offer crossing and Payment
if ((flags & lsfMPTCanTrade) == 0)
return tecNO_PERMISSION;
if (accountID != issuer)
{
if ((flags & lsfMPTCanTransfer) == 0)
return tecNO_PERMISSION;
auto const mptSle = view.read(keylet::mptoken(issuanceKey.key, accountID));
// Allow to succeed since some tx create MPToken if it doesn't exist.
// Tx's have their own check for missing MPToken.
if (!mptSle)
return tesSUCCESS;
if (mptSle->isFlag(lsfMPTLocked))
return tecLOCKED;
}
return tesSUCCESS;
}
TER
checkMPTTxAllowed(
ReadView const& view,
TxType txType,
Asset const& asset,
AccountID const& accountID)
{
// use isDEXAllowed for payment/offer crossing
XRPL_ASSERT(txType != ttPAYMENT, "xrpl::checkMPTTxAllowed : not payment");
return checkMPTAllowed(view, txType, asset, accountID);
}
} // namespace xrpl
@@ -55,6 +55,14 @@ isGlobalFrozen(ReadView const& view, Asset const& asset)
[&](MPTIssue const& issue) { return isGlobalFrozen(view, issue); });
}
TER
checkGlobalFrozen(ReadView const& view, Asset const& asset)
{
if (isGlobalFrozen(view, asset))
return asset.holds<MPTIssue>() ? tecLOCKED : tecFROZEN;
return tesSUCCESS;
}
bool
isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset)
{
@@ -62,6 +70,14 @@ isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const&
[&](auto const& issue) { return isIndividualFrozen(view, account, issue); }, asset.value());
}
TER
checkIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset)
{
if (isIndividualFrozen(view, account, asset))
return asset.holds<MPTIssue>() ? tecLOCKED : tecFROZEN;
return tesSUCCESS;
}
bool
isFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth)
{
+17 -16
View File
@@ -45,7 +45,8 @@ ValidAMM::visitEntry(
// AMM pool changed
else if (
(type == ltRIPPLE_STATE && after->isFlag(lsfAMMNode)) ||
(type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)))
(type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)) ||
(type == ltMPTOKEN && after->isFlag(lsfMPTAMM)))
{
ammPoolChanged_ = true;
}
@@ -85,9 +86,9 @@ ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const
{
// LPTokens and the pool can not change on vote
// LCOV_EXCL_START
JLOG(j.error()) << "AMMVote invariant failed: " << lptAMMBalanceBefore_.value_or(STAmount{})
<< " " << lptAMMBalanceAfter_.value_or(STAmount{}) << " "
<< ammPoolChanged_;
JLOG(j.error()) << "Invariant failed: AMMVote failed, "
<< lptAMMBalanceBefore_.value_or(STAmount{}) << " "
<< lptAMMBalanceAfter_.value_or(STAmount{}) << " " << ammPoolChanged_;
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -103,7 +104,7 @@ ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
{
// The pool can not change on bid
// LCOV_EXCL_START
JLOG(j.error()) << "AMMBid invariant failed: pool changed";
JLOG(j.error()) << "Invariant failed: AMMBid failed, pool changed";
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -114,7 +115,7 @@ ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
(*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::kZero))
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ << " "
JLOG(j.error()) << "Invariant failed: AMMBid failed, " << *lptAMMBalanceBefore_ << " "
<< *lptAMMBalanceAfter_;
if (enforce)
return false;
@@ -134,7 +135,7 @@ ValidAMM::finalizeCreate(
if (!ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMCreate invariant failed: AMM object is not created";
JLOG(j.error()) << "Invariant failed: AMMCreate failed, AMM object is not created";
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -157,8 +158,8 @@ ValidAMM::finalizeCreate(
if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) ||
ammLPTokens(amount, amount2, lptAMMBalanceAfter_->get<Issue>()) != *lptAMMBalanceAfter_)
{
JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " "
<< *lptAMMBalanceAfter_;
JLOG(j.error()) << "Invariant failed: AMMCreate failed, " << amount << " " << amount2
<< " " << *lptAMMBalanceAfter_;
if (enforce)
return false;
}
@@ -176,7 +177,7 @@ ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const
// LCOV_EXCL_START
std::string const msg = (isTesSuccess(res)) ? "AMM object is not deleted on tesSUCCESS"
: "AMM object is changed on tecINCOMPLETE";
JLOG(j.error()) << "AMMDelete invariant failed: " << msg;
JLOG(j.error()) << "Invariant failed: AMMDelete failed, " << msg;
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -191,7 +192,7 @@ ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const
if (ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMM swap invariant failed: AMM object changed";
JLOG(j.error()) << "Invariant failed: AMM swap failed, AMM object changed";
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -232,10 +233,10 @@ ValidAMM::generalInvariant(
};
if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck()))
{
JLOG(j.error()) << "AMM " << tx.getTxnType()
<< " invariant failed: " << tx.getHash(HashPrefix::TransactionId) << " "
<< ammPoolChanged_ << " " << amount << " " << amount2 << " "
<< poolProductMean << " " << lptAMMBalanceAfter_->getText() << " "
JLOG(j.error()) << "Invariant failed: AMM " << tx.getTxnType() << " "
<< tx.getHash(HashPrefix::TransactionId) << " " << ammPoolChanged_ << " "
<< amount << " " << amount2 << " " << poolProductMean << " "
<< lptAMMBalanceAfter_->getText() << " "
<< ((*lptAMMBalanceAfter_ == beast::kZero)
? Number{1}
: ((*lptAMMBalanceAfter_ - poolProductMean) / poolProductMean));
@@ -256,7 +257,7 @@ ValidAMM::finalizeDeposit(
if (!ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted";
JLOG(j.error()) << "Invariant failed: AMMDeposit failed, AMM object is deleted";
if (enforce)
return false;
// LCOV_EXCL_STOP
+139
View File
@@ -5,6 +5,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
@@ -15,6 +16,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
@@ -384,4 +386,141 @@ ValidMPTPayment::finalize(
return true;
}
void
ValidMPTTransfer::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
// Record the before/after MPTAmount for each (issuanceID, account) pair
// so finalize() can determine whether a transfer actually occurred.
auto update = [&](SLE const& sle, bool isBefore) {
if (sle.getType() == ltMPTOKEN)
{
auto const issuanceID = sle[sfMPTokenIssuanceID];
auto const account = sle[sfAccount];
auto const amount = sle[sfMPTAmount];
if (isBefore)
{
amount_[issuanceID][account].amtBefore = amount;
}
else
{
amount_[issuanceID][account].amtAfter = amount;
}
if (isDelete && isBefore)
{
deletedAuthorized_[sle.key()] = sle.isFlag(lsfMPTAuthorized);
}
}
};
if (before)
update(*before, true);
if (after)
update(*after, false);
}
bool
ValidMPTTransfer::isAuthorized(
ReadView const& view,
MPTID const& mptid,
AccountID const& holder,
bool reqAuth) const
{
auto const key = keylet::mptoken(mptid, holder);
auto const it = deletedAuthorized_.find(key.key);
if (it != deletedAuthorized_.end())
return !reqAuth || it->second;
return isTesSuccess(requireAuth(view, MPTIssue{mptid}, holder));
}
bool
ValidMPTTransfer::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (hasPrivilege(tx, OverrideFreeze))
return true;
// DEX transactions (AMM[Create,Deposit,Withdraw], cross-currency payments, offer creates) are
// subject to the MPTCanTrade flag in addition to the standard transfer rules.
// A payment is only DEX if it is a cross-currency payment.
auto const isDEX = [&tx] {
auto const txnType = tx.getTxnType();
if (txnType == ttPAYMENT)
{
// A payment is cross-currency (and thus DEX) only if SendMax is present
// and its asset differs from the destination asset.
auto const amount = tx[sfAmount];
return tx[~sfSendMax].value_or(amount).asset() != amount.asset();
}
return txnType == ttAMM_CREATE || txnType == ttAMM_DEPOSIT || txnType == ttAMM_WITHDRAW ||
txnType == ttOFFER_CREATE;
}();
// Only enforce once MPTokensV2 is enabled to preserve consensus with non-V2 nodes.
// Log invariant failure error even if MPTokensV2 is disabled.
auto const enforce = !view.rules().enabled(featureMPTokensV2);
for (auto const& [mptID, values] : amount_)
{
std::uint16_t senders = 0;
std::uint16_t receivers = 0;
bool invalidTransfer = false;
auto const sleIssuance = view.read(keylet::mptIssuance(mptID));
if (!sleIssuance)
{
continue;
}
auto const canTransfer = sleIssuance->isFlag(lsfMPTCanTransfer);
auto const canTrade = sleIssuance->isFlag(lsfMPTCanTrade);
auto const reqAuth = sleIssuance->isFlag(lsfMPTRequireAuth);
for (auto const& [account, value] : values)
{
// Classify each account as a sender or receiver based on whether their MPTAmount
// decreased or increased. Count new MPToken holders (no amtBefore) as receivers.
// Skip deleted MPToken holders (amtAfter is nullopt); deletion requires zero balance.
if (value.amtAfter.has_value() && value.amtBefore.value_or(0) != *value.amtAfter)
{
if (!value.amtBefore.has_value() || *value.amtAfter > *value.amtBefore)
{
++receivers;
}
else
{
++senders;
}
// Check once: if any involved account is frozen, the whole
// issuance transfer is considered frozen. Only need to check for
// frozen if there is a transfer of funds.
if (!invalidTransfer &&
(isFrozen(view, account, MPTIssue{mptID}) ||
!isAuthorized(view, mptID, account, reqAuth)))
{
invalidTransfer = true;
}
}
}
// A transfer between holders has occurred (senders > 0 && receivers > 0).
// Fail if the issuance is frozen, does not permit transfers, or — for
// DEX transactions — does not permit trading.
if ((invalidTransfer || !canTransfer || (isDEX && !canTrade)) && senders > 0 &&
receivers > 0)
{
JLOG(j.fatal()) << "Invariant failed: invalid MPToken transfer between holders";
return enforce;
}
}
return true;
}
} // namespace xrpl
+3
View File
@@ -395,6 +395,9 @@ MPTEndpointPaymentStep::check(StrandContext const& ctx, std::shared_ptr<const SL
TER
MPTEndpointOfferCrossingStep::check(StrandContext const& ctx, std::shared_ptr<const SLE> const&)
{
// The standard checks are all we can do because any remaining checks
// require the existence of a MPToken. Offer crossing does not
// require a pre-existing MPToken.
return tesSUCCESS;
}
@@ -264,9 +264,10 @@ CheckCash::preclaim(PreclaimContext const& ctx)
return tecLOCKED;
}
if (auto const err = canTrade(ctx.view, value.asset()); !isTesSuccess(err))
if (auto const err = canTransfer(ctx.view, issue, srcId, dstId);
!isTesSuccess(err))
{
JLOG(ctx.j.warn()) << "MPT DEX is not allowed.";
JLOG(ctx.j.warn()) << "MPT transfer is disabled.";
return err;
}
@@ -111,10 +111,10 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
{
// The currency may not be globally frozen
AccountID const& issuerId{sendMax.getIssuer()};
if (isGlobalFrozen(ctx.view, sendMax.asset()))
if (auto const ter = checkGlobalFrozen(ctx.view, sendMax.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.warn()) << "Creating a check for frozen asset";
return sendMax.asset().holds<MPTIssue>() ? tecLOCKED : tecFROZEN;
JLOG(ctx.j.warn()) << "Creating a check for frozen or locked asset";
return ter;
}
auto const err = sendMax.asset().visit(
[&](Issue const& issue) -> std::optional<TER> {
@@ -153,9 +153,21 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
},
[&](MPTIssue const& issue) -> std::optional<TER> {
if (srcId != issuerId && isFrozen(ctx.view, srcId, issue))
{
JLOG(ctx.j.warn()) << "Creating a check for locked MPT.";
return tecLOCKED;
}
if (dstId != issuerId && isFrozen(ctx.view, dstId, issue))
{
JLOG(ctx.j.warn()) << "Creating a check for locked MPT.";
return tecLOCKED;
}
if (auto const ter = canTransfer(ctx.view, issue, srcId, dstId);
!isTesSuccess(ter))
{
JLOG(ctx.j.warn()) << "MPT transfer is disabled.";
return ter;
}
return std::nullopt;
});
@@ -169,7 +181,7 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
return tecEXPIRED;
}
return canTrade(ctx.view, ctx.tx[sfSendMax].asset());
return tesSUCCESS;
}
TER
+11 -7
View File
@@ -26,7 +26,6 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
@@ -120,11 +119,16 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
}
// Globally or individually frozen
if (isFrozen(ctx.view, accountID, amount.asset()) ||
isFrozen(ctx.view, accountID, amount2.asset()))
if (auto const ter = checkFrozen(ctx.view, accountID, amount.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "AMM Instance: involves frozen asset.";
return tecFROZEN;
JLOG(ctx.j.debug()) << "AMM Instance: involves frozen or locked asset.";
return ter;
}
if (auto const ter = checkFrozen(ctx.view, accountID, amount2.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "AMM Instance: involves frozen or locked asset.";
return ter;
}
auto noDefaultRipple = [](ReadView const& view, Asset const& asset) {
@@ -191,10 +195,10 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
return terADDRESS_COLLISION;
}
if (auto const ter = checkMPTTxAllowed(ctx.view, ttAMM_CREATE, amount.asset(), accountID);
if (auto const ter = canMPTTradeAndTransfer(ctx.view, amount.asset(), accountID, accountID);
!isTesSuccess(ter))
return ter;
if (auto const ter = checkMPTTxAllowed(ctx.view, ttAMM_CREATE, amount2.asset(), accountID);
if (auto const ter = canMPTTradeAndTransfer(ctx.view, amount2.asset(), accountID, accountID);
!isTesSuccess(ter))
return ter;
+15 -14
View File
@@ -21,7 +21,6 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
@@ -267,12 +266,12 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
return ter;
}
if (isFrozen(ctx.view, accountID, asset))
if (auto const ter = checkFrozen(ctx.view, accountID, asset); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "AMM Deposit: account or currency is frozen, "
JLOG(ctx.j.debug()) << "AMM Deposit: account or currency is frozen or locked, "
<< to_string(accountID) << " " << to_string(asset);
return tecFROZEN;
return ter;
}
return tesSUCCESS;
@@ -304,18 +303,20 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
// LCOV_EXCL_STOP
}
// AMM account or currency frozen
if (isFrozen(ctx.view, ammAccountID, amount->asset()))
if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
!isTesSuccess(ter))
{
JLOG(ctx.j.debug())
<< "AMM Deposit: AMM account or currency is frozen, " << to_string(accountID);
return tecFROZEN;
JLOG(ctx.j.debug()) << "AMM Deposit: AMM account or currency is frozen or locked, "
<< to_string(accountID);
return ter;
}
// Account frozen
if (isIndividualFrozen(ctx.view, accountID, amount->asset()))
if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
!isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "AMM Deposit: account is frozen, " << to_string(accountID)
<< " " << to_string(amount->asset());
return tecFROZEN;
JLOG(ctx.j.debug()) << "AMM Deposit: account is frozen or locked, "
<< to_string(accountID) << " " << to_string(amount->asset());
return ter;
}
if (checkBalance)
{
@@ -368,10 +369,10 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
}
}
if (auto const ter = checkMPTTxAllowed(ctx.view, ttAMM_DEPOSIT, ctx.tx[sfAsset], accountID);
if (auto const ter = canMPTTradeAndTransfer(ctx.view, ctx.tx[sfAsset], accountID, accountID);
!isTesSuccess(ter))
return ter;
if (auto const ter = checkMPTTxAllowed(ctx.view, ttAMM_DEPOSIT, ctx.tx[sfAsset2], accountID);
if (auto const ter = canMPTTradeAndTransfer(ctx.view, ctx.tx[sfAsset2], accountID, accountID);
!isTesSuccess(ter))
return ter;
+11 -15
View File
@@ -7,7 +7,6 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -26,7 +25,6 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
@@ -242,22 +240,24 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx)
return ter;
}
// AMM account or currency frozen
if (isFrozen(ctx.view, ammAccountID, amount->asset()))
if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
!isTesSuccess(ter))
{
JLOG(ctx.j.debug())
<< "AMM Withdraw: AMM account or currency is frozen, " << to_string(accountID);
return tecFROZEN;
JLOG(ctx.j.debug()) << "AMM Withdraw: AMM account or currency is frozen or locked, "
<< to_string(accountID);
return ter;
}
// Account frozen
if (isIndividualFrozen(ctx.view, accountID, amount->asset()))
if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
!isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: account is frozen, " << to_string(accountID)
<< " " << to_string(amount->asset());
return tecFROZEN;
JLOG(ctx.j.debug()) << "AMM Withdraw: account is frozen or locked, "
<< to_string(accountID) << " " << to_string(amount->asset());
return ter;
}
if (auto const ter =
checkMPTTxAllowed(ctx.view, ttAMM_WITHDRAW, amount->asset(), accountID);
canMPTTradeAndTransfer(ctx.view, amount->asset(), accountID, accountID);
!isTesSuccess(ter))
return ter;
}
@@ -635,10 +635,6 @@ AMMWithdraw::withdraw(
auto const balanceAdj = isIssue ? std::max(priorBalance, balance.xrp()) : priorBalance;
if (balanceAdj < reserve)
return tecINSUFFICIENT_RESERVE;
// Update owner count.
if (!isIssue)
adjustOwnerCount(view, sleAccount, 1, journal);
}
return tesSUCCESS;
};
+14 -4
View File
@@ -181,11 +181,15 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
auto viewJ = ctx.registry.get().getJournal("View");
if (isGlobalFrozen(ctx.view, saTakerPays.asset()) ||
isGlobalFrozen(ctx.view, saTakerGets.asset()))
if (auto const ter = checkGlobalFrozen(ctx.view, saTakerPays.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "Offer involves frozen asset";
return tecFROZEN;
return ter;
}
if (auto const ter = checkGlobalFrozen(ctx.view, saTakerGets.asset()); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "Offer involves frozen asset";
return ter;
}
// Allow unfunded MPT for issuer (OutstandingAmount >= MaximumAmount)
@@ -320,7 +324,13 @@ OfferCreate::checkAcceptAsset(
[&](MPTIssue const& issue) -> TER {
// WeakAuth - don't check if MPToken exists since it's created
// if needed.
return requireAuth(view, issue, id, AuthType::WeakAuth);
if (auto const ter = requireAuth(view, issue, id, AuthType::WeakAuth);
!isTesSuccess(ter))
{
return ter;
}
return checkFrozen(view, id, issue);
});
}
+2 -4
View File
@@ -3114,10 +3114,8 @@ private:
btc.set({.holder = bob, .flags = tfMPTLock});
{
// different from IOU. The offer is created but not crossed.
env(offer(bob, btc(5), XRP(25)));
env(offer(bob, btc(5), XRP(25)), Ter(tecLOCKED));
env.close();
BEAST_EXPECT(expectOffers(env, bob, 1, {{{btc(5), XRP(25)}}}));
BEAST_EXPECT(ammAlice.expectBalances(XRP(500), btc(105), ammAlice.tokens()));
}
@@ -3220,7 +3218,7 @@ private:
btc.set({.flags = tfMPTLock});
// assets can't be bought on the market
AMM const ammA3(env, a3, btc(1), XRP(1), Ter(tecFROZEN));
AMM const ammA3(env, a3, btc(1), XRP(1), Ter(tecLOCKED));
// direct issues can be sent
env(pay(g1, a2, btc(1)));
+38 -39
View File
@@ -144,7 +144,7 @@ private:
.pay = 30'000,
.flags = tfMPTCanLock | kMptDexFlags});
usd.set({.flags = tfMPTLock});
AMM const ammAliceFail(env, alice_, XRP(10'000), usd(10'000), Ter(tecFROZEN));
AMM const ammAliceFail(env, alice_, XRP(10'000), usd(10'000), Ter(tecLOCKED));
usd.set({.flags = tfMPTUnlock});
AMM const ammAlice(env, alice_, XRP(10'000), usd(10'000));
}
@@ -348,7 +348,7 @@ private:
.pay = 30'000,
.flags = tfMPTCanLock | kMptDexFlags});
btc.set({.flags = tfMPTLock});
AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecFROZEN));
AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecLOCKED));
BEAST_EXPECT(!ammAlice.ammExists());
}
@@ -365,7 +365,7 @@ private:
btc.set({.holder = alice_, .flags = tfMPTLock});
// alice's token is locked
AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecFROZEN));
AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecLOCKED));
BEAST_EXPECT(!ammAlice.ammExists());
// bob can create
@@ -693,15 +693,15 @@ private:
btc.set({.flags = tfMPTLock});
ammAlice.deposit(
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.deposit(
carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.deposit(
carol_, USD(100), btc(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, USD(100), btc(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
}
// Individually lock MPT or freeze IOU (AMM) with IOU/MPT AMM
@@ -722,20 +722,19 @@ private:
// Carol can not deposit locked mpt
ammAlice.deposit(
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
if (!features[featureAMMClawback])
{
ammAlice.deposit(
carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.deposit(carol_, USD(100), std::nullopt, std::nullopt, std::nullopt);
}
else
{
// Carol can not deposit non-forzen token either
// Carol can not deposit non-frozen token either
ammAlice.deposit(
carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
}
// Alice can deposit because she's not individually locked
@@ -778,9 +777,9 @@ private:
ammAlice.deposit(carol_, USD(100), std::nullopt, std::nullopt, std::nullopt);
// Can not deposit locked token
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.deposit(
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
// Unlock AMM MPT
btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock});
@@ -812,10 +811,10 @@ private:
// Carol's BTC is locked
btc.set({.holder = carol_, .flags = tfMPTLock});
ammAlice.deposit(
carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.deposit(
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
// Unlock carol's BTC
btc.set({.holder = carol_, .flags = tfMPTUnlock});
@@ -831,9 +830,9 @@ private:
ammAlice.deposit(carol_, usd(100), std::nullopt, std::nullopt, std::nullopt);
// Can not deposit locked token BTC
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.deposit(
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
// Unlock AMM MPT BTC
btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock});
@@ -848,9 +847,9 @@ private:
ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt);
// Can not deposit locked token USD
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.deposit(
carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
// Unlock AMM MPT USD
usd.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock});
@@ -904,7 +903,7 @@ private:
AMM amm(env, gw, XRP(10'000), btc(10'000));
amm.deposit({.account = alice, .asset1In = btc(10), .err = Ter(tecNO_PERMISSION)});
amm.deposit({.account = alice, .asset1In = btc(10), .err = Ter(tecNO_AUTH)});
}
// Insufficient XRP balance
@@ -2262,7 +2261,7 @@ private:
.account = alice_,
.asset1Out = btc(100),
.assets = {{XRP, btc}},
.err = Ter(tecNO_PERMISSION)});
.err = Ter(tecNO_AUTH)});
}
// Globally locked MPT
@@ -2283,8 +2282,8 @@ private:
btc.set({.flags = tfMPTLock});
ammAlice.withdraw(
alice_, MPT(ammAlice[1])(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
alice_, MPT(ammAlice[1])(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
// can single withdraw the other asset
ammAlice.withdraw({.account = alice_, .asset1Out = XRP(100)});
@@ -2312,8 +2311,8 @@ private:
// Alice's BTC is locked
btc.set({.holder = alice_, .flags = tfMPTLock});
ammAlice.withdraw(alice_, 1000, std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.withdraw(alice_, 1000, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
// can withdraw the other asset
ammAlice.withdraw(alice_, usd(100), std::nullopt, std::nullopt);
@@ -2341,8 +2340,8 @@ private:
// Alice's BTC is locked
btc.set({.holder = alice_, .flags = tfMPTLock});
ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
// can still single withdraw the unlocked other asset
ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt);
@@ -2361,8 +2360,8 @@ private:
ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt);
// Can not withdraw locked token BTC
ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
// Unlock AMM MPT
btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock});
@@ -6886,33 +6885,33 @@ private:
cb(amm, btc);
};
// Deposit two assets, one of which is frozen,
// then we should get tecFROZEN error.
// Deposit two assets, one of which is locked,
// then we should get tecLOCKED error.
{
Env env(*this);
testAMMDeposit(env, [&](AMM& amm, MPTTester& btc) {
amm.deposit(alice_, btc(100), XRP(100), std::nullopt, tfTwoAsset, Ter(tecFROZEN));
amm.deposit(alice_, btc(100), XRP(100), std::nullopt, tfTwoAsset, Ter(tecLOCKED));
});
}
// Deposit one asset, which is the frozen token,
// then we should get tecFROZEN error.
// Deposit one asset, which is the frozen locked,
// then we should get tecLOCKED error.
{
Env env(*this);
testAMMDeposit(env, [&](AMM& amm, MPTTester& btc) {
amm.deposit(
alice_, btc(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecFROZEN));
alice_, btc(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecLOCKED));
});
}
// Deposit one asset which is not the frozen token,
// but the other asset is frozen. We should get tecFROZEN error
// but the other asset is frozen. We should get tecLOCKED error
// when feature AMMClawback is enabled.
{
Env env(*this);
testAMMDeposit(env, [&](AMM& amm, MPTTester& btc) {
amm.deposit(
alice_, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecFROZEN));
alice_, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecLOCKED));
});
}
}
+4 -4
View File
@@ -1860,10 +1860,10 @@ class CheckMPT_test : public beast::unit_test::Suite
// Use offers to automatically create MPT.
MPT const oF4 = gw1["OF4"];
gw1.set(oF4, tfMPTLock);
env(offer(gw1, XRP(92), oF4(92)), Ter(tecFROZEN));
env(offer(gw1, XRP(92), oF4(92)), Ter(tecLOCKED));
env.close();
BEAST_EXPECT(env.le(keylet::mptoken(oF4, alice)) == nullptr);
env(offer(alice, oF4(92), XRP(92)), Ter(tecFROZEN));
env(offer(alice, oF4(92), XRP(92)), Ter(tecLOCKED));
env.close();
// No one's owner count should have changed.
@@ -1951,10 +1951,10 @@ class CheckMPT_test : public beast::unit_test::Suite
// Use offers to automatically create MPT.
MPT const oF4 = gw1["OF4"];
gw1.set(oF4, tfMPTLock);
env(offer(alice, XRP(91), oF4(91)), Ter(tecFROZEN));
env(offer(alice, XRP(91), oF4(91)), Ter(tecLOCKED));
env.close();
BEAST_EXPECT(env.le(keylet::mptoken(oF4, alice)) == nullptr);
env(offer(bob, oF4(91), XRP(91)), Ter(tecFROZEN));
env(offer(bob, oF4(91), XRP(91)), Ter(tecLOCKED));
env.close();
// No one's owner count should have changed.
+182
View File
@@ -4103,6 +4103,187 @@ class Invariants_test : public beast::unit_test::Suite
return true;
});
}
// Invalid transfer
std::array<std::pair<TxType, bool>, 3> const invalidTransferTests = {
std::make_pair(ttAMM_WITHDRAW, false),
std::make_pair(ttPAYMENT, false),
std::make_pair(ttPAYMENT, true)};
for (auto const enabled : {true, false})
{
for (auto const& [tx, crossCurrencyPayment] : invalidTransferTests)
{
for (auto const flag :
{static_cast<std::uint32_t>(lsfMPTLocked),
~lsfMPTCanTransfer,
~lsfMPTCanTrade,
0u})
{
MPTID id{};
auto const isSuccess = !enabled || flag == 0 ||
(tx == ttPAYMENT && !crossCurrencyPayment && (flag == ~lsfMPTCanTrade));
std::pair<TER, TER> const error = isSuccess
? std::make_pair(TER(tesSUCCESS), TER(tesSUCCESS))
: std::make_pair(TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED));
doInvariantCheck(
{{isSuccess ? "" : "invalid MPToken transfer between holders"}},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto update = [&](AccountID const& a, std::uint64_t v) {
auto sle = ac.view().peek(keylet::mptoken(id, a));
if (!sle)
return false;
sle->at(sfMPTAmount) = v;
ac.view().update(sle);
return true;
};
auto issuanceSle = ac.view().peek(keylet::mptIssuance(id));
if (!issuanceSle)
return false;
auto const flags = issuanceSle->at(sfFlags);
if (flag == lsfMPTLocked)
{
issuanceSle->at(sfFlags) = flags | lsfMPTLocked;
}
else if (flag != 0u)
{
issuanceSle->at(sfFlags) = flags & flag;
}
issuanceSle->at(sfOutstandingAmount) = 200;
ac.view().update(issuanceSle);
return update(a1, 101) && update(a2, 99);
},
XRPAmount{},
STTx{
tx,
[&](STObject& tx) {
if (crossCurrencyPayment)
{
tx.setFieldAmount(
sfSendMax, STAmount(MPTAmount{100}, MPTIssue{id}));
}
}},
{error.first, error.second},
[&](Account const& a1, Account const& a2, Env& env) {
Account const gw("gw");
env.fund(XRP(1'000), gw);
MPTTester const usd(
{.env = env, .issuer = gw, .holders = {a1, a2}, .pay = 100});
id = usd.issuanceID();
if (!enabled)
{
env.disableFeature(featureMPTokensV2);
}
return true;
});
}
}
}
}
void
testAMM()
{
testcase << "AMM";
using namespace jtx;
MPTID mptID{};
uint256 ammID{};
AccountID ammAccountID{};
Account const gw{"gw"};
Issue lptIssue{};
PrettyAsset poolAsset{xrpIssue()};
auto deleteAMMAccount = [&](ApplyContext& ac, bool) {
auto sle = ac.view().peek(keylet::account(ammAccountID));
if (!sle)
return false;
ac.view().erase(sle);
return true;
};
auto updateLPTokensBalance = [&](ApplyContext& ac, std::int64_t amount) {
auto sle = ac.view().peek(keylet::amm(ammID));
if (!sle)
return false;
sle->setFieldAmount(sfLPTokenBalance, STAmount{lptIssue, amount});
ac.view().update(sle);
return true;
};
auto updateLPTokensBadAmount = [&](ApplyContext& ac, bool) {
return updateLPTokensBalance(ac, -1);
};
auto updateLPTokensBadBalance = [&](ApplyContext& ac, bool) {
return updateLPTokensBalance(ac, 200'000'000);
};
auto updateAMM = [&](ApplyContext& ac, bool) { return updateLPTokensBalance(ac, 10); };
auto updateAMMPool = [&](ApplyContext& ac, bool isMPT) {
if (isMPT)
{
auto sle = ac.view().peek(keylet::mptoken(mptID, ammAccountID));
if (!sle)
return false;
sle->setFieldU64(sfMPTAmount, 1);
ac.view().update(sle);
return true;
}
auto sle = ac.view().peek(keylet::account(ammAccountID));
if (!sle)
return false;
sle->setFieldAmount(sfBalance, XRP(1));
ac.view().update(sle);
return true;
};
auto test = [&](auto const txType,
auto&& update,
bool isMPT,
TER error = tecINVARIANT_FAILED) {
doInvariantCheck(
{{"AMM"}},
[&](Account const&, Account const&, ApplyContext& ac) { return update(ac, isMPT); },
XRPAmount{},
STTx{txType, [&](STObject& tx) {}},
{tecINVARIANT_FAILED, error},
[&](Account const&, Account const&, Env& env) {
env.fund(XRP(1'000), gw);
poolAsset = [&]() -> PrettyAsset {
if (isMPT)
{
MPT const mpt = MPTTester({.env = env, .issuer = gw});
mptID = mpt.issuanceID;
return mpt;
}
return gw["USD"];
}();
AMM const amm(env, gw, XRP(100), poolAsset(100));
ammAccountID = amm.ammAccount();
ammID = amm.ammID();
lptIssue = amm.lptIssue();
return true;
});
};
for (bool const isMPT : {false, true})
{
auto const error = isMPT ? TER(tecINVARIANT_FAILED) : TER(tefINVARIANT_FAILED);
for (auto txType : {ttAMM_CREATE, ttAMM_DEPOSIT, ttAMM_CLAWBACK, ttAMM_WITHDRAW})
{
test(txType, deleteAMMAccount, isMPT, tefINVARIANT_FAILED);
test(txType, updateLPTokensBadAmount, isMPT);
test(txType, updateLPTokensBadBalance, isMPT);
}
for (auto txType : {ttAMM_BID, ttAMM_VOTE})
{
test(txType, updateAMMPool, isMPT, error);
test(txType, updateLPTokensBadAmount, isMPT);
test(txType, updateLPTokensBadBalance, isMPT);
}
for (auto txType : {ttAMM_DELETE, ttCHECK_CASH, ttOFFER_CREATE, ttPAYMENT})
{
test(txType, updateAMM, isMPT);
}
}
}
// Test the invariant overwrite fix for both pre- and post-amendment
@@ -4389,6 +4570,7 @@ public:
testInvariantOverwrite(defaultAmendments());
testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3);
testVaultComputeCoarsestScale();
testAMM();
}
};
+143 -71
View File
@@ -33,6 +33,7 @@
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ApplyViewImpl.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
@@ -3481,10 +3482,10 @@ class MPToken_test : public beast::unit_test::Suite
auto const [errBuy, errSell] = [&]() -> std::pair<TER, TER> {
// Global lock
if (lockMPTIssue)
return std::make_pair(tecFROZEN, tecFROZEN);
return std::make_pair(tecLOCKED, tecLOCKED);
// Local lock
if (lockMPToken)
return std::make_pair(tesSUCCESS, error(tecUNFUNDED_OFFER));
return std::make_pair(error(tecLOCKED), error(tecUNFUNDED_OFFER));
// MPToken doesn't exist
if (requireAuth)
return std::make_pair(error(tecNO_AUTH), error(tecUNFUNDED_OFFER));
@@ -3582,10 +3583,9 @@ class MPToken_test : public beast::unit_test::Suite
else
{
auto const err = flag == tfMPTLock ? Ter(tecUNFUNDED_OFFER) : Ter(tesSUCCESS);
auto const err1 = flag == tfMPTLock ? Ter(tecLOCKED) : Ter(tesSUCCESS);
env(offer(alice, eth(1), btc(1)), err);
// Offer created by not crossed
env(offer(carol, btc(1), eth(1)));
BEAST_EXPECT(expectOffers(env, carol, 1, {{btc(1), eth(1)}}));
env(offer(carol, btc(1), eth(1)), err1);
}
};
@@ -5932,7 +5932,7 @@ class MPToken_test : public beast::unit_test::Suite
env.close();
}
// MPTCanTransfer disabled
// MPTCanTransfer is disabled
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
@@ -5973,49 +5973,50 @@ class MPToken_test : public beast::unit_test::Suite
BEAST_EXPECT(env.balance(alice, mpt) == mpt(0));
BEAST_EXPECT(env.balance(gw, mpt) == mpt(0));
// neither src nor dst is issuer, can still create
// neither src nor dst is issuer, can't create
checkId = keylet::check(alice, env.seq(alice)).key;
env(check::create(alice, carol, mpt(100)), Ter(tecNO_AUTH));
env.close();
// can create now
mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTransfer});
checkId = keylet::check(alice, env.seq(alice)).key;
env(check::create(alice, carol, mpt(100)));
env.close();
// can't cash
env(check::cash(carol, checkId, mpt(10)), Ter(tecPATH_PARTIAL));
env.close();
// can cash now
mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTransfer});
env(pay(gw, alice, mpt(10)));
env.close();
// can't cash
mpt.set({.account = gw, .mutableFlags = tmfMPTClearCanTransfer});
env.close();
env(check::cash(carol, checkId, mpt(10)), Ter(tecNO_AUTH));
env.close();
// can cash
mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTransfer});
env(check::cash(carol, checkId, mpt(10)));
env.close();
}
// MPTCanTrade disabled
// MPTCanTrade is disabled
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
env.close();
MPTTester mpt(
MPT const mpt = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.flags = tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateCanTrade});
.pay = 10,
.flags = tfMPTCanTransfer});
uint256 checkId{keylet::check(gw, env.seq(gw)).key};
uint256 const checkId{keylet::check(alice, env.seq(alice)).key};
// can't create
env(check::create(gw, alice, mpt(100)), Ter(tecNO_PERMISSION));
// can create
env(check::create(alice, carol, mpt(100)));
env.close();
mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTrade});
// can't cash
checkId = keylet::check(gw, env.seq(gw)).key;
env(check::create(gw, carol, mpt(100)));
env.close();
mpt.set({.account = gw, .mutableFlags = tmfMPTClearCanTrade});
env(check::cash(carol, checkId, mpt(10)), Ter(tecNO_PERMISSION));
// can cash
env(check::cash(carol, checkId, mpt(10)));
env.close();
}
@@ -6070,33 +6071,6 @@ class MPToken_test : public beast::unit_test::Suite
env.close();
}
// MPTCanTransfer is not set and the account is not the issuer of MPT
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
auto eur = MPTTester(
{.env = env, .issuer = gw, .holders = {alice, carol}, .flags = tfMPTCanTrade});
uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
// alice can create
env(check::create(alice, carol, eur(1)));
env.close();
// carol can't cash
env(check::cash(carol, chkId, eur(1)), Ter(tecPATH_PARTIAL));
env.close();
// if issuer creates a check then carol can cash since
// it's a transfer from the issuer
uint256 const chkId1{getCheckIndex(gw, env.seq(gw))};
// alice can't create since CanTransfer is not set
env(check::create(gw, carol, eur(1)));
env.close();
env(check::cash(carol, chkId1, eur(1)));
env.close();
}
// Can create check if src/dst don't own MPT
{
Env env{*this, features};
@@ -6257,8 +6231,7 @@ class MPToken_test : public beast::unit_test::Suite
AMM amm(env, gw, btc(100), usd(100));
env.close();
// alice can't deposit since MPTCanTransfer is not set
amm.deposit(
DepositArg{.account = alice, .tokens = 1'000, .err = Ter(tecNO_PERMISSION)});
amm.deposit(DepositArg{.account = alice, .tokens = 1'000, .err = Ter(tecNO_AUTH)});
env.close();
// can't clawback since alice is not an LP
@@ -6491,8 +6464,8 @@ class MPToken_test : public beast::unit_test::Suite
// alice and issuer can't create
usd.set({.flags = tfMPTLock});
createFail(alice, tecFROZEN);
createFail(gw, tecFROZEN);
createFail(alice, tecLOCKED);
createFail(gw, tecLOCKED);
// MPTRequireAuth is set
@@ -6512,7 +6485,7 @@ class MPToken_test : public beast::unit_test::Suite
usd.set({.mutableFlags = tmfMPTClearRequireAuth});
usd.set({.mutableFlags = tmfMPTClearCanTransfer});
// alice can't create
createFail(alice, tecNO_PERMISSION);
createFail(alice, tecNO_AUTH);
// issuer can create
createDeleteAMM(gw);
usd.set({.mutableFlags = tmfMPTSetCanTransfer});
@@ -6558,12 +6531,12 @@ class MPToken_test : public beast::unit_test::Suite
{.account = account,
.asset1In = usd(1),
.asset2In = eur(1),
.err = Ter(tecFROZEN)});
.err = Ter(tecLOCKED)});
amm.deposit(
{.account = account,
.asset1In = eur(1),
.assets = std::make_pair(eur, usd),
.err = Ter(tecFROZEN)});
.err = Ter(tecLOCKED)});
}
usd.set({.flags = tfMPTUnlock});
@@ -6598,15 +6571,12 @@ class MPToken_test : public beast::unit_test::Suite
usd.set({.mutableFlags = tmfMPTClearCanTransfer});
// carol can't deposit
amm.deposit(
{.account = carol,
.asset1In = usd(1),
.asset2In = eur(1),
.err = Ter(tecNO_PERMISSION)});
{.account = carol, .asset1In = usd(1), .asset2In = eur(1), .err = Ter(tecNO_AUTH)});
amm.deposit(
{.account = carol,
.asset1In = eur(1),
.assets = std::make_pair(eur, usd),
.err = Ter(tecNO_PERMISSION)});
.err = Ter(tecNO_AUTH)});
// issuer can deposit
amm.deposit({.account = gw, .tokens = 1'000});
// carol can deposit
@@ -6648,8 +6618,8 @@ class MPToken_test : public beast::unit_test::Suite
{.account = account,
.asset1Out = usd(1),
.asset2Out = eur(1),
.err = Ter(tecFROZEN)});
amm.withdraw({.account = account, .tokens = 1'000, .err = Ter(tecFROZEN)});
.err = Ter(tecLOCKED)});
amm.withdraw({.account = account, .tokens = 1'000, .err = Ter(tecLOCKED)});
// can single withdraw another asset
amm.withdraw(
{.account = account, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)});
@@ -6675,7 +6645,7 @@ class MPToken_test : public beast::unit_test::Suite
usd.authorize({.account = gw, .holder = carol});
amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)});
// MPTCanTransfer is set
// MPTCanTransfer is not set
usd.set({.mutableFlags = tmfMPTClearRequireAuth});
usd.set({.mutableFlags = tmfMPTClearCanTransfer});
@@ -6684,7 +6654,7 @@ class MPToken_test : public beast::unit_test::Suite
{.account = carol,
.asset1Out = usd(1),
.asset2Out = eur(1),
.err = Ter(tecNO_PERMISSION)});
.err = Ter(tecNO_AUTH)});
// can withdraw another asset
amm.withdraw(
{.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)});
@@ -6695,6 +6665,9 @@ class MPToken_test : public beast::unit_test::Suite
amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)});
usd.set({.mutableFlags = tmfMPTSetCanTransfer});
// MPTCanTrade is not set
usd.set({.mutableFlags = tmfMPTClearCanTrade});
amm.withdraw({.account = gw, .tokens = 1'000, .err = Ter(tecNO_PERMISSION)});
amm.withdraw({.account = carol, .tokens = 1'000, .err = Ter(tecNO_PERMISSION)});
@@ -6716,6 +6689,99 @@ class MPToken_test : public beast::unit_test::Suite
}
}
void
testFixDoubleOwnerCount(FeatureBitset all)
{
testcase("Fix Double adjustOwnerCount in AMMWithdraw");
using namespace jtx;
// Carol deposits XRP into an XRP/MPT pool, then withdraws MPT.
// Carol has no MPToken before the withdrawal. If the bug exists,
// her ownerCount will be inflated by +1 extra.
Account const gw{"gw"};
Account const alice{"alice"};
Account const carol{"carol"};
Env env(*this, all);
env.fund(XRP(30'000), gw, alice, carol);
env.close();
// Create MPT with DEX flags. Only alice is a holder initially.
MPT const btc = MPTTester(
{.env = env, .issuer = gw, .holders = {alice}, .pay = 20'000, .flags = kMPT_DEX_FLAGS});
// Alice creates XRP/MPT AMM pool
AMM amm(env, alice, XRP(10'000), btc(10'000));
// Carol deposits XRP (single asset) into the pool.
// Carol gets LP tokens but does NOT have an MPToken yet.
auto const carolOwnersBefore = ownerCount(env, carol);
amm.deposit(carol, XRP(1'000), std::nullopt, std::nullopt, tfSingleAsset);
auto const carolOwnersAfterDeposit = ownerCount(env, carol);
// Carol should have +1 for LP token trustline
BEAST_EXPECT(carolOwnersAfterDeposit == carolOwnersBefore + 1);
auto const carolOwnersBeforeWithdraw = ownerCount(env, carol);
// Carol withdraws single MPT asset. She doesn't have an MPToken,
// so one must be created. Bug: ownerCount incremented twice.
amm.withdraw({.account = carol, .asset1Out = btc(100), .flags = tfSingleAsset});
auto const carolOwnersAfterWithdraw = ownerCount(env, carol);
// Expected: +1 for the new MPToken (so total increase = 1)
BEAST_EXPECT(carolOwnersAfterWithdraw == carolOwnersBeforeWithdraw + 1);
}
void
testTradeAndTransfer()
{
using namespace jtx;
testcase("Trade and Transfer");
// Verify canMPTTradeAndTransfer validates the flags when from == to and from != to
Account const gw{"gw"};
Account const alice{"alice"};
Account const carol{"carol"};
Env env(*this);
env.fund(XRP(1'000), gw, alice, carol);
MPTTester mpt(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.pay = 100,
.flags = kMPT_DEX_FLAGS,
.mutableFlags = tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanTrade});
// Both flags are enabled
BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw)));
BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice)));
BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice)));
BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol)));
// MPTCanTrade is disabled
mpt.set({.mutableFlags = tmfMPTClearCanTrade});
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw) == tecNO_PERMISSION);
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice) == tecNO_PERMISSION);
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == tecNO_PERMISSION);
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == tecNO_PERMISSION);
// MPTCanTransfer is disabled
mpt.set({.mutableFlags = tmfMPTSetCanTrade});
mpt.set({.mutableFlags = tmfMPTClearCanTransfer});
BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw)));
BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice)));
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == tecNO_AUTH);
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == tecNO_AUTH);
// Both flags are disabled
mpt.set({.mutableFlags = tmfMPTClearCanTrade});
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw) == tecNO_PERMISSION);
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice) == tecNO_PERMISSION);
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == tecNO_PERMISSION);
BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == tecNO_PERMISSION);
}
public:
void
run() override
@@ -6822,6 +6888,12 @@ public:
// Test AMM
testBasicAMM(all);
// Test Trade/Transfer
testTradeAndTransfer();
// Fixes
testFixDoubleOwnerCount(all);
}
};
+96
View File
@@ -4707,6 +4707,101 @@ public:
}
}
void
testAutoCreateReserve(FeatureBitset features)
{
// When an offer on the book is partially crossed, the payment engine
// auto-creates a new ledger object (MPToken or IOU trustline) for the
// offer owner to hold the incoming asset. This happens inside
// BookStep::forEachOffer (MPT: checkCreateMPT) and BookStep::consumeOffer
// (IOU: directSendNoFeeIOU -> trustCreate) without a reserve sufficiency
// check. The offer owner can therefore end up with more objects than
// their XRP balance can reserve for, consistent with IOU behavior.
testcase("Auto-Create Object Without Reserve Check During Partial Crossing");
using namespace jtx;
auto const gw = Account{"gateway"};
auto const alice = Account{"alice"};
auto const carol = Account{"carol"};
auto const bob = Account{"bob"};
auto test = [&](auto&& getToken, auto&& execTx) {
// MPT/IOU: carol's existing offer buys MPT/IOU by selling XRP.
// carol has no MPToken/Trustline for this issuance. When alice partially crosses
// carol's offer, an MPToken/Trustline is auto-created for carol without checking
// that she can afford the extra reserve slot.
Env env{*this, features};
auto const f = env.current()->fees().base;
auto const r = reserve(env, 0);
auto const inc = reserve(env, 1) - r;
env.fund(XRP(10'000), gw, alice, bob);
// getToken:
// - Create MPT with CanTransfer + CanTrade; authorize alice as holder.
// - Create IOU trustline
auto const token = getToken(env);
// carol: reserve(0) + 1 increment + fee covers placing one offer.
// After the offer tx she has exactly reserve(1) + XRP(30).
// XRP(30) < inc (50 XRP), so receiving a second object will put her
// below reserve(2).
if (BEAST_EXPECT(inc > XRP(30)))
env.fund(r + inc + f + XRP(30), carol);
// carol's offer goes on the book (no counterpart yet).
// TakerPays=Token(30): carol will receive Token when crossed.
// TakerGets=XRP(30): carol will give XRP when crossed.
env(offer(carol, token(30), XRP(30)));
env.require(Owners(carol, 1));
// Execute offer create or cross-currency payment
// alice partially crosses carol's offer.
// alice sends Token(15) to carol and receives XRP(15).
// Token:
// - MPT: checkCreateMPT auto-creates an MPToken for carol (no reserve check).
// - IOU: directSendNoFeeIOU auto-creates an Trustline for carol (no reserve check).
execTx(env, token);
// Carol now owns 2 objects (remaining offer + new MPToken) even
// though her XRP balance is only reserve(1) + XRP(15), which is
// below reserve(2) = reserve(1) + inc.
auto const carolBalance = r + inc + XRP(15);
env.require(Owners(carol, 2), Balance(carol, token(15)), Balance(carol, carolBalance));
BEAST_EXPECT(carolBalance < r + 2 * inc); // below reserve(2)
};
std::function<PrettyAsset(Env&)> const getIOU = [&](Env& env) -> PrettyAsset {
env.trust(gw["USD"](1'000), alice);
env(pay(gw, alice, gw["USD"](100)));
return gw["USD"];
};
std::function<PrettyAsset(Env&)> const getMPT = [&](Env& env) -> PrettyAsset {
MPT const mpT1 = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 100});
return mpT1;
};
for (auto&& getToken : {getIOU, getMPT})
{
test(getToken, [&](Env& env, PrettyAsset const& token) {
// alice partially crosses carol's offer.
// alice sends Token(15) to carol and receives XRP(15).
// Token is MPT: checkCreateMPT auto-creates an MPToken for carol (no reserve
// check). Token is IOU: directSendNoFeeIOU auto-creates a trustline for carol (no
// reserve check).
env(offer(alice, XRP(15), token(15)));
});
test(getToken, [&](Env& env, PrettyAsset const& token) {
// Similar to above but with cross-currency payment.
env(pay(alice, bob, XRP(15)),
Sendmax(token(15)),
Path(~XRP),
Txflags(tfNoRippleDirect | tfPartialPayment));
});
}
}
void
testAll(FeatureBitset features)
{
@@ -4763,6 +4858,7 @@ public:
testRmSmallIncreasedQOffersMPT(features);
testFillOrKill(features);
testTickSize(features);
testAutoCreateReserve(features);
}
void