mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 11:37:07 +00:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cba6772e5 | ||
|
|
6b7151f7c2 | ||
|
|
8ffdfc5953 | ||
|
|
1c8fc3c806 | ||
|
|
c7e3dae68a | ||
|
|
e46cb6bd78 | ||
|
|
5c0c358ea6 | ||
|
|
6b71fb3f48 | ||
|
|
5ec090b258 | ||
|
|
35890680bf | ||
|
|
d3e22a1251 | ||
|
|
0f262a56cf | ||
|
|
392e69ae67 | ||
|
|
080830c4ae | ||
|
|
940dcef212 | ||
|
|
62bdfc6bf6 | ||
|
|
e85f010ef0 | ||
|
|
af17ab2642 | ||
|
|
70fa0d7f79 | ||
|
|
a2ab6c79ac | ||
|
|
5439264bdb | ||
|
|
62921eb913 | ||
|
|
50cacddcf8 | ||
|
|
a839b8b92a | ||
|
|
1a2c0e7d32 | ||
|
|
77df21b485 | ||
|
|
63f270f00e | ||
|
|
0a896b7957 | ||
|
|
12eb050bf8 | ||
|
|
d26d93130e | ||
|
|
a91e8ddad7 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -399,7 +399,8 @@ using InvariantChecks = std::tuple<
|
||||
ValidLoanBroker,
|
||||
ValidLoan,
|
||||
ValidVault,
|
||||
ValidMPTPayment>;
|
||||
ValidMPTPayment,
|
||||
ValidMPTTransfer>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user