Compare commits

...

12 Commits

Author SHA1 Message Date
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 Tsipenyuk
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
Gregory Tsipenyuk
56c9d1d497 fix: Add description for terLOCKED error (#6811) 2026-04-08 20:56:19 +00:00
yinyiqian1
d52dd29d20 fix: Address AI reviewer comments for Permission Delegation (#6675) 2026-04-08 20:22:19 +00:00
Mayukha Vadari
7793b5f10b refactor: Combine AMMHelpers and AMMUtils (#6733) 2026-04-08 17:38:33 +00:00
Gregory Tsipenyuk
dfcad69155 feat: Add MPT support to DEX (#5285) 2026-04-08 16:17:37 +00:00
Pratik Mankawde
6d1a5be8d2 fix: Handle WSClient write failure when server closes WebSocket (#6671)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 19:15:40 +00:00
227 changed files with 35599 additions and 4731 deletions

View File

@@ -44,6 +44,10 @@ suggestWords:
words:
- abempty
- AMMID
- AMMMPT
- AMMMPToken
- AMMMPTokens
- AMMXRP
- amt
- amts
- asnode
@@ -96,6 +100,7 @@ words:
- distro
- doxyfile
- dxrpl
- enabled
- endmacro
- exceptioned
- Falco
@@ -148,6 +153,8 @@ words:
- ltype
- mcmodel
- MEMORYSTATUSEX
- MPTAMM
- MPTDEX
- Merkle
- Metafuncton
- misprediction
@@ -157,6 +164,7 @@ words:
- mptid
- mptissuance
- mptissuanceid
- mptissue
- mptoken
- mptokenid
- mptokenissuance

View File

@@ -213,11 +213,60 @@ public:
// Called when a credit is made to an account
// This is required to support PaymentSandbox
virtual void
creditHook(
creditHookIOU(
AccountID const& from,
AccountID const& to,
STAmount const& amount,
STAmount const& preCreditBalance)
{
XRPL_ASSERT(amount.holds<Issue>(), "creditHookIOU: amount is for Issue");
}
virtual void
creditHookMPT(
AccountID const& from,
AccountID const& to,
STAmount const& amount,
std::uint64_t preCreditBalanceHolder,
std::int64_t preCreditBalanceIssuer)
{
XRPL_ASSERT(amount.holds<MPTIssue>(), "creditHookMPT: amount is for MPTIssue");
}
/** Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
* Unlike IOU, MPT doesn't have bi-directional relationship with an issuer,
* where a trustline limits an amount that can be issued to a holder.
* Consequently, the credit step (last MPTEndpointStep or
* BookStep buying MPT) might temporarily overflow OutstandingAmount.
* Limiting of a step's output amount in this case is delegated to
* the next step (in rev order). The next step always redeems when a holder
* account sells MPT (first MPTEndpointStep or BookStep selling MPT).
* In this case the holder account is only limited by the step's output
* and it's available funds since it's transferring the funds from one
* account to another account and doesn't change OutstandingAmount.
* This doesn't apply to an offer owned by an issuer.
* In this case the issuer sells or self debits and is increasing
* OutstandingAmount. Ability to issue is limited by the issuer
* originally available funds less already self sold MPT amounts (MPT sell
* offer).
* Consider an example:
* - GW creates MPT(USD) with 1,000USD MaximumAmount.
* - GW pays 950USD to A1.
* - A1 creates an offer 100XRP(buy)/100USD(sell).
* - GW creates an offer 100XRP(buy)/100USD(sell).
* - A2 pays 200USD to A3 with sendMax of 200XRP.
* Since the payment engine executes payments in reverse,
* OutstandingAmount overflows in MPTEndpointStep: 950 + 200 = 1,150USD.
* BookStep first consumes A1 offer. This reduces OutstandingAmount
* by 100USD: 1,150 - 100 = 1,050USD. GW offer can only be partially
* consumed because the initial available amount is 50USD = 1,000 - 950.
* BookStep limits it's output to 150USD. This in turn limits A3's send
* amount to 150XRP: A1 buys 100XRP and sells 100USD to A3. This doesn't
* change OutstandingAmount. GW buys 50XRP and sells 50USD to A3. This
* changes OutstandingAmount to 1,000USD.
*/
virtual void
issuerSelfDebitHookMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance)
{
}

View File

@@ -3,8 +3,8 @@
#include <xrpl/ledger/AcceptedLedgerTx.h>
#include <xrpl/ledger/BookListeners.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MultiApiJson.h>
#include <xrpl/protocol/UintTypes.h>
@@ -53,30 +53,30 @@ public:
issue. This is useful for pathfinding to find all possible next hops
from a given currency.
@param issue The issue to search for
@param asset The asset to search for
@param domain Optional domain restriction for the order book
@return Vector of books that want this issue
*/
virtual std::vector<Book>
getBooksByTakerPays(Issue const& issue, std::optional<Domain> const& domain = std::nullopt) = 0;
getBooksByTakerPays(Asset const& asset, std::optional<Domain> const& domain = std::nullopt) = 0;
/** Get the count of order books that want a specific issue.
@param issue The issue to search for
@param asset The asset to search for
@param domain Optional domain restriction for the order book
@return Number of books that want this issue
*/
virtual int
getBookSize(Issue const& issue, std::optional<Domain> const& domain = std::nullopt) = 0;
getBookSize(Asset const& asset, std::optional<Domain> const& domain = std::nullopt) = 0;
/** Check if an order book to XRP exists for the given issue.
@param issue The issue to check
@param asset The asset to check
@param domain Optional domain restriction for the order book
@return true if a book from this issue to XRP exists
*/
virtual bool
isBookToXRP(Issue const& issue, std::optional<Domain> const& domain = std::nullopt) = 0;
isBookToXRP(Asset const& asset, std::optional<Domain> const& domain = std::nullopt) = 0;
/**
* Process a transaction for order book tracking.

View File

@@ -15,10 +15,55 @@ namespace detail {
// into the PaymentSandbox class itself
class DeferredCredits
{
public:
struct Adjustment
private:
using KeyIOU = std::tuple<AccountID, AccountID, Currency>;
struct ValueIOU
{
Adjustment(STAmount const& d, STAmount const& c, STAmount const& b)
explicit ValueIOU() = default;
STAmount lowAcctCredits;
STAmount highAcctCredits;
STAmount lowAcctOrigBalance;
};
struct HolderValueMPT
{
HolderValueMPT() = default;
// Debit to issuer
std::uint64_t debit = 0;
std::uint64_t origBalance = 0;
};
struct IssuerValueMPT
{
IssuerValueMPT() = default;
std::map<AccountID, HolderValueMPT> holders;
// Credit to holder
std::uint64_t credit = 0;
// OutstandingAmount might overflow when MPTs are credited to a holder.
// Consider A1 paying 100MPT to A2 and A1 already having maximum MPTs.
// Since the payment engine executes a payment in revers, A2 is
// credited first and OutstandingAmount is going to be equal
// to MaximumAmount + 100MPT. In the next step A1 redeems 100MPT
// to the issuer and OutstandingAmount balances out.
std::int64_t origBalance = 0;
// Self debit on offer selling MPT. Since the payment engine executes
// a payment in reverse, a crediting/buying step may overflow
// OutstandingAmount. A sell MPT offer owned by a holder can redeem any
// amount up to the offer's amount and holder's available funds,
// balancing out OutstandingAmount. But if the offer's owner is issuer
// then it issues more MPT. In this case the available amount to issue
// is the initial issuer's available amount less all offer sell amounts
// by the issuer. This is self-debit, where the offer's owner,
// issuer in this case, debits to self.
std::uint64_t selfDebit = 0;
};
using AdjustmentMPT = IssuerValueMPT;
public:
struct AdjustmentIOU
{
AdjustmentIOU(STAmount const& d, STAmount const& c, STAmount const& b)
: debits(d), credits(c), origBalance(b)
{
}
@@ -29,16 +74,30 @@ public:
// Get the adjustments for the balance between main and other.
// Returns the debits, credits and the original balance
std::optional<Adjustment>
adjustments(AccountID const& main, AccountID const& other, Currency const& currency) const;
std::optional<AdjustmentIOU>
adjustmentsIOU(AccountID const& main, AccountID const& other, Currency const& currency) const;
std::optional<AdjustmentMPT>
adjustmentsMPT(MPTID const& mptID) const;
void
credit(
creditIOU(
AccountID const& sender,
AccountID const& receiver,
STAmount const& amount,
STAmount const& preCreditSenderBalance);
void
creditMPT(
AccountID const& sender,
AccountID const& receiver,
STAmount const& amount,
std::uint64_t preCreditBalanceHolder,
std::int64_t preCreditBalanceIssuer);
void
issuerSelfDebitMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance);
void
ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_t next);
@@ -52,21 +111,11 @@ public:
apply(DeferredCredits& to);
private:
// lowAccount, highAccount
using Key = std::tuple<AccountID, AccountID, Currency>;
struct Value
{
explicit Value() = default;
static KeyIOU
makeKeyIOU(AccountID const& a1, AccountID const& a2, Currency const& currency);
STAmount lowAcctCredits;
STAmount highAcctCredits;
STAmount lowAcctOrigBalance;
};
static Key
makeKey(AccountID const& a1, AccountID const& a2, Currency const& c);
std::map<Key, Value> credits_;
std::map<KeyIOU, ValueIOU> creditsIOU_;
std::map<MPTID, IssuerValueMPT> creditsMPT_;
std::map<AccountID, std::uint32_t> ownerCounts_;
};
@@ -131,16 +180,35 @@ public:
/** @} */
STAmount
balanceHook(AccountID const& account, AccountID const& issuer, STAmount const& amount)
balanceHookIOU(AccountID const& account, AccountID const& issuer, STAmount const& amount)
const override;
STAmount
balanceHookMPT(AccountID const& account, MPTIssue const& issue, std::int64_t amount)
const override;
STAmount
balanceHookSelfIssueMPT(MPTIssue const& issue, std::int64_t amount) const override;
void
creditHook(
creditHookIOU(
AccountID const& from,
AccountID const& to,
STAmount const& amount,
STAmount const& preCreditBalance) override;
void
creditHookMPT(
AccountID const& from,
AccountID const& to,
STAmount const& amount,
std::uint64_t preCreditBalanceHolder,
std::int64_t preCreditBalanceIssuer) override;
void
issuerSelfDebitHookMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance)
override;
void
adjustOwnerCountHook(AccountID const& account, std::uint32_t cur, std::uint32_t next) override;

View File

@@ -148,15 +148,35 @@ public:
// Accounts in a payment are not allowed to use assets acquired during that
// payment. The PaymentSandbox tracks the debits, credits, and owner count
// changes that accounts make during a payment. `balanceHook` adjusts
// changes that accounts make during a payment. `balanceHookIOU` adjusts
// balances so newly acquired assets are not counted toward the balance.
// This is required to support PaymentSandbox.
virtual STAmount
balanceHook(AccountID const& account, AccountID const& issuer, STAmount const& amount) const
balanceHookIOU(AccountID const& account, AccountID const& issuer, STAmount const& amount) const
{
XRPL_ASSERT(amount.holds<Issue>(), "balanceHookIOU: amount is for Issue");
return amount;
}
// balanceHookMPT adjusts balances so newly acquired assets are not counted
// toward the balance.
virtual STAmount
balanceHookMPT(AccountID const& account, MPTIssue const& issue, std::int64_t amount) const
{
return STAmount{issue, amount};
}
// An offer owned by an issuer and selling MPT is limited by the issuer's
// funds available to issue, which are originally available funds less
// already self sold MPT amounts (MPT sell offer). This hook is used
// by issuerFundsToSelfIssue() function.
virtual STAmount
balanceHookSelfIssueMPT(MPTIssue const& issue, std::int64_t amount) const
{
return STAmount{issue, amount};
}
// Accounts in a payment are not allowed to use assets acquired during that
// payment. The PaymentSandbox tracks the debits, credits, and owner count
// changes that accounts make during a payment. `ownerCountHook` adjusts the

View File

@@ -63,8 +63,8 @@ isVaultPseudoAccountFrozen(
isLPTokenFrozen(
ReadView const& view,
AccountID const& account,
Issue const& asset,
Issue const& asset2);
Asset const& asset,
Asset const& asset2);
// Return the list of enabled amendments
[[nodiscard]] std::set<uint256>

View File

@@ -1,8 +1,13 @@
#pragma once
#include <xrpl/basics/Expected.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/AmountConversions.h>
#include <xrpl/protocol/Feature.h>
@@ -11,6 +16,7 @@
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
namespace xrpl {
@@ -36,7 +42,7 @@ enum class IsDeposit : bool { No = false, Yes = true };
* @return LP Tokens as IOU
*/
STAmount
ammLPTokens(STAmount const& asset1, STAmount const& asset2, Issue const& lptIssue);
ammLPTokens(STAmount const& asset1, STAmount const& asset2, Asset const& lptIssue);
/** Calculate LP Tokens given asset's deposit amount.
* @param asset1Balance current AMM asset1 balance
@@ -124,7 +130,8 @@ withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Nu
template <typename Amt>
requires(
std::is_same_v<Amt, STAmount> || std::is_same_v<Amt, IOUAmount> ||
std::is_same_v<Amt, XRPAmount> || std::is_same_v<Amt, Number>)
std::is_same_v<Amt, XRPAmount> || std::is_same_v<Amt, MPTAmount> ||
std::is_same_v<Amt, Number>)
bool
withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist)
{
@@ -195,7 +202,7 @@ getAMMOfferStartWithTakerGets(
// Round downward to minimize the offer and to maximize the quality.
// This has the most impact when takerGets is XRP.
auto const takerGets =
toAmount<TOut>(getIssue(pool.out), nTakerGetsProposed, Number::downward);
toAmount<TOut>(getAsset(pool.out), nTakerGetsProposed, Number::downward);
return TAmounts<TIn, TOut>{swapAssetOut(pool, takerGets, tfee), takerGets};
};
@@ -262,7 +269,7 @@ getAMMOfferStartWithTakerPays(
// Round downward to minimize the offer and to maximize the quality.
// This has the most impact when takerPays is XRP.
auto const takerPays =
toAmount<TIn>(getIssue(pool.in), nTakerPaysProposed, Number::downward);
toAmount<TIn>(getAsset(pool.in), nTakerPaysProposed, Number::downward);
return TAmounts<TIn, TOut>{takerPays, swapAssetIn(pool, takerPays, tfee)};
};
@@ -331,7 +338,7 @@ changeSpotPriceQuality(
<< " " << to_string(pool.out) << " " << quality << " " << tfee;
return std::nullopt;
}
auto const takerPays = toAmount<TIn>(getIssue(pool.in), nTakerPays, Number::upward);
auto const takerPays = toAmount<TIn>(getAsset(pool.in), nTakerPays, Number::upward);
// should not fail
if (auto amounts = TAmounts<TIn, TOut>{takerPays, swapAssetIn(pool, takerPays, tfee)};
Quality{amounts} < quality &&
@@ -360,7 +367,7 @@ changeSpotPriceQuality(
// Generate the offer starting with XRP side. Return seated offer amounts
// if the offer can be generated, otherwise nullopt.
auto amounts = [&]() {
if (isXRP(getIssue(pool.out)))
if (isXRP(getAsset(pool.out)))
return getAMMOfferStartWithTakerGets(pool, quality, tfee);
return getAMMOfferStartWithTakerPays(pool, quality, tfee);
}();
@@ -445,7 +452,7 @@ swapAssetIn(TAmounts<TIn, TOut> const& pool, TIn const& assetIn, std::uint16_t t
auto const denom = pool.in + assetIn * (1 - fee);
if (denom.signum() <= 0)
return toAmount<TOut>(getIssue(pool.out), 0);
return toAmount<TOut>(getAsset(pool.out), 0);
Number::setround(Number::upward);
auto const ratio = numerator / denom;
@@ -454,14 +461,14 @@ swapAssetIn(TAmounts<TIn, TOut> const& pool, TIn const& assetIn, std::uint16_t t
auto const swapOut = pool.out - ratio;
if (swapOut.signum() < 0)
return toAmount<TOut>(getIssue(pool.out), 0);
return toAmount<TOut>(getAsset(pool.out), 0);
return toAmount<TOut>(getIssue(pool.out), swapOut, Number::downward);
return toAmount<TOut>(getAsset(pool.out), swapOut, Number::downward);
}
else
{
return toAmount<TOut>(
getIssue(pool.out),
getAsset(pool.out),
pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
Number::downward);
}
@@ -508,7 +515,7 @@ swapAssetOut(TAmounts<TIn, TOut> const& pool, TOut const& assetOut, std::uint16_
auto const denom = pool.out - assetOut;
if (denom.signum() <= 0)
{
return toMaxAmount<TIn>(getIssue(pool.in));
return toMaxAmount<TIn>(getAsset(pool.in));
}
Number::setround(Number::upward);
@@ -522,14 +529,14 @@ swapAssetOut(TAmounts<TIn, TOut> const& pool, TOut const& assetOut, std::uint16_
Number::setround(Number::upward);
auto const swapIn = numerator2 / feeMult;
if (swapIn.signum() < 0)
return toAmount<TIn>(getIssue(pool.in), 0);
return toAmount<TIn>(getAsset(pool.in), 0);
return toAmount<TIn>(getIssue(pool.in), swapIn, Number::upward);
return toAmount<TIn>(getAsset(pool.in), swapIn, Number::upward);
}
else
{
return toAmount<TIn>(
getIssue(pool.in),
getAsset(pool.in),
((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee),
Number::upward);
}
@@ -616,9 +623,9 @@ getRoundedAsset(Rules const& rules, STAmount const& balance, A const& frac, IsDe
if (!rules.enabled(fixAMMv1_3))
{
if constexpr (std::is_same_v<A, STAmount>)
return multiply(balance, frac, balance.issue());
return multiply(balance, frac, balance.asset());
else
return toSTAmount(balance.issue(), balance * frac);
return toSTAmount(balance.asset(), balance * frac);
}
auto const rm = detail::getAssetRounding(isDeposit);
return multiply(balance, frac, rm);
@@ -712,4 +719,94 @@ adjustFracByTokens(
STAmount const& tokens,
Number const& frac);
/** Get AMM pool balances.
*/
std::pair<STAmount, STAmount>
ammPoolHolds(
ReadView const& view,
AccountID const& ammAccountID,
Asset const& asset1,
Asset const& asset2,
FreezeHandling freezeHandling,
AuthHandling authHandling,
beast::Journal const j);
/** Get AMM pool and LP token balances. If both optIssue are
* provided then they are used as the AMM token pair issues.
* Otherwise the missing issues are fetched from ammSle.
*/
Expected<std::tuple<STAmount, STAmount, STAmount>, TER>
ammHolds(
ReadView const& view,
SLE const& ammSle,
std::optional<Asset> const& optAsset1,
std::optional<Asset> const& optAsset2,
FreezeHandling freezeHandling,
AuthHandling authHandling,
beast::Journal const j);
/** Get the balance of LP tokens.
*/
STAmount
ammLPHolds(
ReadView const& view,
Asset const& asset1,
Asset const& asset2,
AccountID const& ammAccount,
AccountID const& lpAccount,
beast::Journal const j);
STAmount
ammLPHolds(
ReadView const& view,
SLE const& ammSle,
AccountID const& lpAccount,
beast::Journal const j);
/** Get AMM trading fee for the given account. The fee is discounted
* if the account is the auction slot owner or one of the slot's authorized
* accounts.
*/
std::uint16_t
getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account);
/** Returns total amount held by AMM for the given token.
*/
STAmount
ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const& asset);
/** Delete trustlines to AMM. If all trustlines are deleted then
* AMM object and account are deleted. Otherwise tecINCOMPLETE is returned.
*/
TER
deleteAMMAccount(Sandbox& view, Asset const& asset, Asset const& asset2, beast::Journal j);
/** Initialize Auction and Voting slots and set the trading/discounted fee.
*/
void
initializeFeeAuctionVote(
ApplyView& view,
std::shared_ptr<SLE>& ammSle,
AccountID const& account,
Asset const& lptAsset,
std::uint16_t tfee);
/** Return true if the Liquidity Provider is the only AMM provider, false
* otherwise. Return tecINTERNAL if encountered an unexpected condition,
* for instance Liquidity Provider has more than one LPToken trustline.
*/
Expected<bool, TER>
isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID const& lpAccount);
/** Due to rounding, the LPTokenBalance of the last LP might
* not match the LP's trustline balance. If it's within the tolerance,
* update LPTokenBalance to match the LP's trustline balance.
*/
Expected<bool, TER>
verifyAndAdjustLPTokenBalance(
Sandbox& sb,
STAmount const& lpTokens,
std::shared_ptr<SLE>& ammSle,
AccountID const& account);
} // namespace xrpl

View File

@@ -1,106 +0,0 @@
#pragma once
#include <xrpl/basics/Expected.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
namespace xrpl {
class ReadView;
class ApplyView;
class Sandbox;
class NetClock;
/** Get AMM pool balances.
*/
std::pair<STAmount, STAmount>
ammPoolHolds(
ReadView const& view,
AccountID const& ammAccountID,
Issue const& issue1,
Issue const& issue2,
FreezeHandling freezeHandling,
beast::Journal const j);
/** Get AMM pool and LP token balances. If both optIssue are
* provided then they are used as the AMM token pair issues.
* Otherwise the missing issues are fetched from ammSle.
*/
Expected<std::tuple<STAmount, STAmount, STAmount>, TER>
ammHolds(
ReadView const& view,
SLE const& ammSle,
std::optional<Issue> const& optIssue1,
std::optional<Issue> const& optIssue2,
FreezeHandling freezeHandling,
beast::Journal const j);
/** Get the balance of LP tokens.
*/
STAmount
ammLPHolds(
ReadView const& view,
Currency const& cur1,
Currency const& cur2,
AccountID const& ammAccount,
AccountID const& lpAccount,
beast::Journal const j);
STAmount
ammLPHolds(
ReadView const& view,
SLE const& ammSle,
AccountID const& lpAccount,
beast::Journal const j);
/** Get AMM trading fee for the given account. The fee is discounted
* if the account is the auction slot owner or one of the slot's authorized
* accounts.
*/
std::uint16_t
getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account);
/** Returns total amount held by AMM for the given token.
*/
STAmount
ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Issue const& issue);
/** Delete trustlines to AMM. If all trustlines are deleted then
* AMM object and account are deleted. Otherwise tecIMPCOMPLETE is returned.
*/
TER
deleteAMMAccount(Sandbox& view, Issue const& asset, Issue const& asset2, beast::Journal j);
/** Initialize Auction and Voting slots and set the trading/discounted fee.
*/
void
initializeFeeAuctionVote(
ApplyView& view,
std::shared_ptr<SLE>& ammSle,
AccountID const& account,
Issue const& lptIssue,
std::uint16_t tfee);
/** Return true if the Liquidity Provider is the only AMM provider, false
* otherwise. Return tecINTERNAL if encountered an unexpected condition,
* for instance Liquidity Provider has more than one LPToken trustline.
*/
Expected<bool, TER>
isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID const& lpAccount);
/** Due to rounding, the LPTokenBalance of the last LP might
* not match the LP's trustline balance. If it's within the tolerance,
* update LPTokenBalance to match the LP's trustline balance.
*/
Expected<bool, TER>
verifyAndAdjustLPTokenBalance(
Sandbox& sb,
STAmount const& lpTokens,
std::shared_ptr<SLE>& ammSle,
AccountID const& account);
} // namespace xrpl

View File

@@ -41,7 +41,8 @@ escrowUnlockApplyHelper<Issue>(
bool createAsset,
beast::Journal journal)
{
Keylet const trustLineKey = keylet::line(receiver, amount.issue());
Issue const& issue = amount.get<Issue>();
Keylet const trustLineKey = keylet::line(receiver, issue);
bool const recvLow = issuer > receiver;
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
@@ -64,9 +65,9 @@ escrowUnlockApplyHelper<Issue>(
return tecNO_LINE_INSUF_RESERVE;
}
Currency const currency = amount.getCurrency();
STAmount initialBalance(amount.issue());
initialBalance.setIssuer(noAccount());
Currency const currency = issue.currency;
STAmount initialBalance(issue);
initialBalance.get<Issue>().account = noAccount();
if (TER const ter = trustCreate(
view, // payment sandbox
@@ -113,7 +114,8 @@ escrowUnlockApplyHelper<Issue>(
if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate)
{
// compute transfer fee, if any
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.issue(), true);
auto const xferFee =
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}

View File

@@ -80,6 +80,7 @@ authorizeMPToken(
* requireAuth check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
* purely defensive, as we currently do not allow such vaults to be created.
* WeakAuth intentionally allows missing MPTokens under MPToken V2.
*/
[[nodiscard]] TER
requireAuth(
@@ -114,6 +115,21 @@ canTransfer(
AccountID const& from,
AccountID const& to);
/** Check if Asset can be traded on DEX. return tecNO_PERMISSION
* if it doesn't and tesSUCCESS otherwise.
*/
[[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)
@@ -164,4 +180,60 @@ createMPToken(
AccountID const& account,
std::uint32_t const flags);
TER
checkCreateMPT(
xrpl::ApplyView& view,
xrpl::MPTIssue const& mptIssue,
xrpl::AccountID const& holder,
beast::Journal j);
//------------------------------------------------------------------------------
//
// MPT Overflow related
//
//------------------------------------------------------------------------------
// MaximumAmount doesn't exceed 2**63-1
std::int64_t
maxMPTAmount(SLE const& sleIssuance);
// OutstandingAmount may overflow and available amount might be negative.
// But available amount is always <= |MaximumAmount - OutstandingAmount|.
std::int64_t
availableMPTAmount(SLE const& sleIssuance);
std::int64_t
availableMPTAmount(ReadView const& view, MPTID const& mptID);
/** Checks for two types of OutstandingAmount overflow during a send operation.
* 1. **Direct directSendNoFee (Overflow: No):** A true overflow check when
* `OutstandingAmount > MaximumAmount`. This threshold is used for direct
* directSendNoFee transactions that bypass the payment engine.
* 2. **accountSend & Payment Engine (Overflow: Yes):** A temporary overflow
* check when `OutstandingAmount > UINT64_MAX`. This higher threshold is used
* for `accountSend` and payments processed via the payment engine.
*/
bool
isMPTOverflow(
std::int64_t sendAmount,
std::uint64_t outstandingAmount,
std::int64_t maximumAmount,
AllowMPTOverflow allowOverflow);
/**
* Determine funds available for an issuer to sell in an issuer owned offer.
* Issuing step, which could be either MPTEndPointStep last step or BookStep's
* TakerPays may overflow OutstandingAmount. Redeeming step, in BookStep's
* TakerGets redeems the offer's owner funds, essentially balancing out
* the overflow, unless the offer's owner is the issuer.
*/
[[nodiscard]] STAmount
issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
/** Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
* See ApplyView::issuerSelfDebitHookMPT().
*/
void
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
} // namespace xrpl

View File

@@ -252,4 +252,14 @@ deleteAMMTrustLine(
std::optional<AccountID> const& ammAccountID,
beast::Journal j);
/** Delete AMMs MPToken. The passed `sle` must be obtained from a prior
* call to view.peek().
*/
[[nodiscard]] TER
deleteAMMMPToken(
ApplyView& view,
std::shared_ptr<SLE> sleMPT,
AccountID const& ammAccountID,
beast::Journal j);
} // namespace xrpl

View File

@@ -31,6 +31,9 @@ enum SpendableHandling { shSIMPLE_BALANCE, shFULL_BALANCE };
enum class WaiveTransferFee : bool { No = false, Yes };
/** Controls whether accountSend is allowed to overflow OutstandingAmount **/
enum class AllowMPTOverflow : bool { No = false, Yes };
/* Check if MPToken (for MPT) or trust line (for IOU) exists:
* - StrongAuth - before checking if authorization is required
* - WeakAuth
@@ -51,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
@@ -176,6 +185,16 @@ accountFunds(
FreezeHandling freezeHandling,
beast::Journal j);
// Overload with AuthHandling to support IOU and MPT.
[[nodiscard]] STAmount
accountFunds(
ReadView const& view,
AccountID const& id,
STAmount const& saDefault,
FreezeHandling freezeHandling,
AuthHandling authHandling,
beast::Journal j);
/** Returns the transfer fee as Rate based on the type of token
* @param view The ledger view
* @param amount The amount to transfer
@@ -257,7 +276,8 @@ accountSend(
AccountID const& to,
STAmount const& saAmount,
beast::Journal j,
WaiveTransferFee waiveFee = WaiveTransferFee::No);
WaiveTransferFee waiveFee = WaiveTransferFee::No,
AllowMPTOverflow allowOverflow = AllowMPTOverflow::No);
using MultiplePaymentDestinations = std::vector<std::pair<AccountID, Number>>;
/** Like accountSend, except one account is sending multiple payments (with the

View File

@@ -2,7 +2,7 @@
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
@@ -31,12 +31,12 @@ class Rules;
/** Calculate Liquidity Provider Token (LPT) Currency.
*/
Currency
ammLPTCurrency(Currency const& cur1, Currency const& cur2);
ammLPTCurrency(Asset const& asset1, Asset const& asset2);
/** Calculate LPT Issue from AMM asset pair.
*/
Issue
ammLPTIssue(Currency const& cur1, Currency const& cur2, AccountID const& ammAccountID);
ammLPTIssue(Asset const& asset1, Asset const& asset2, AccountID const& ammAccountID);
/** Validate the amount.
* If validZero is false and amount is beast::zero then invalid amount.
@@ -46,19 +46,19 @@ ammLPTIssue(Currency const& cur1, Currency const& cur2, AccountID const& ammAcco
NotTEC
invalidAMMAmount(
STAmount const& amount,
std::optional<std::pair<Issue, Issue>> const& pair = std::nullopt,
std::optional<std::pair<Asset, Asset>> const& pair = std::nullopt,
bool validZero = false);
NotTEC
invalidAMMAsset(
Issue const& issue,
std::optional<std::pair<Issue, Issue>> const& pair = std::nullopt);
Asset const& asset,
std::optional<std::pair<Asset, Asset>> const& pair = std::nullopt);
NotTEC
invalidAMMAssetPair(
Issue const& issue1,
Issue const& issue2,
std::optional<std::pair<Issue, Issue>> const& pair = std::nullopt);
Asset const& asset1,
Asset const& asset2,
std::optional<std::pair<Asset, Asset>> const& pair = std::nullopt);
/** Get time slot of the auction slot.
*/

View File

@@ -1,6 +1,7 @@
#pragma once
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/XRPAmount.h>
@@ -9,11 +10,12 @@
namespace xrpl {
inline STAmount
toSTAmount(IOUAmount const& iou, Issue const& iss)
toSTAmount(IOUAmount const& iou, Asset const& asset)
{
XRPL_ASSERT(asset.holds<Issue>(), "xrpl::toSTAmount : is Issue");
bool const isNeg = iou.signum() < 0;
std::uint64_t const umant = isNeg ? -iou.mantissa() : iou.mantissa();
return STAmount(iss, umant, iou.exponent(), isNeg, STAmount::unchecked());
return STAmount(asset, umant, iou.exponent(), isNeg, STAmount::unchecked());
}
inline STAmount
@@ -31,12 +33,25 @@ toSTAmount(XRPAmount const& xrp)
}
inline STAmount
toSTAmount(XRPAmount const& xrp, Issue const& iss)
toSTAmount(XRPAmount const& xrp, Asset const& asset)
{
XRPL_ASSERT(isXRP(iss.account) && isXRP(iss.currency), "xrpl::toSTAmount : is XRP");
XRPL_ASSERT(isXRP(asset), "xrpl::toSTAmount : is XRP");
return toSTAmount(xrp);
}
inline STAmount
toSTAmount(MPTAmount const& mpt)
{
return STAmount(mpt, noMPT());
}
inline STAmount
toSTAmount(MPTAmount const& mpt, Asset const& asset)
{
XRPL_ASSERT(asset.holds<MPTIssue>(), "xrpl::toSTAmount : is MPT");
return STAmount(mpt, asset.get<MPTIssue>());
}
template <class T>
T
toAmount(STAmount const& amt) = delete;
@@ -76,6 +91,21 @@ toAmount<XRPAmount>(STAmount const& amt)
return XRPAmount(sMant);
}
template <>
inline MPTAmount
toAmount<MPTAmount>(STAmount const& amt)
{
XRPL_ASSERT(
amt.holds<MPTIssue>() && amt.mantissa() <= maxMPTokenAmount && amt.exponent() == 0,
"xrpl::toAmount<MPTAmount> : maximum mantissa");
if (amt.mantissa() > maxMPTokenAmount || amt.exponent() != 0)
Throw<std::runtime_error>("toAmount<MPTAmount>: invalid mantissa or exponent");
bool const isNeg = amt.negative();
std::int64_t const sMant = isNeg ? -std::int64_t(amt.mantissa()) : amt.mantissa();
return MPTAmount(sMant);
}
template <class T>
T
toAmount(IOUAmount const& amt) = delete;
@@ -98,23 +128,36 @@ toAmount<XRPAmount>(XRPAmount const& amt)
return amt;
}
template <class T>
T
toAmount(MPTAmount const& amt) = delete;
template <>
inline MPTAmount
toAmount<MPTAmount>(MPTAmount const& amt)
{
return amt;
}
template <typename T>
T
toAmount(Issue const& issue, Number const& n, Number::rounding_mode mode = Number::getround())
toAmount(Asset const& asset, Number const& n, Number::rounding_mode mode = Number::getround())
{
saveNumberRoundMode const rm(Number::getround());
if (isXRP(issue))
if (isXRP(asset))
Number::setround(mode);
if constexpr (std::is_same_v<IOUAmount, T>)
return IOUAmount(n);
else if constexpr (std::is_same_v<XRPAmount, T>)
return XRPAmount(static_cast<std::int64_t>(n));
else if constexpr (std::is_same_v<MPTAmount, T>)
return MPTAmount(static_cast<std::int64_t>(n));
else if constexpr (std::is_same_v<STAmount, T>)
{
if (isXRP(issue))
return STAmount(issue, static_cast<std::int64_t>(n));
return STAmount(issue, n);
if (isXRP(asset))
return STAmount(asset, static_cast<std::int64_t>(n));
return STAmount(asset, n);
}
else
{
@@ -125,17 +168,23 @@ toAmount(Issue const& issue, Number const& n, Number::rounding_mode mode = Numbe
template <typename T>
T
toMaxAmount(Issue const& issue)
toMaxAmount(Asset const& asset)
{
if constexpr (std::is_same_v<IOUAmount, T>)
return IOUAmount(STAmount::cMaxValue, STAmount::cMaxOffset);
else if constexpr (std::is_same_v<XRPAmount, T>)
return XRPAmount(static_cast<std::int64_t>(STAmount::cMaxNativeN));
else if constexpr (std::is_same_v<MPTAmount, T>)
return MPTAmount(maxMPTokenAmount);
else if constexpr (std::is_same_v<STAmount, T>)
{
if (isXRP(issue))
return STAmount(issue, static_cast<std::int64_t>(STAmount::cMaxNativeN));
return STAmount(issue, STAmount::cMaxValue, STAmount::cMaxOffset);
return asset.visit(
[](Issue const& issue) {
if (isXRP(issue))
return STAmount(issue, static_cast<std::int64_t>(STAmount::cMaxNativeN));
return STAmount(issue, STAmount::cMaxValue, STAmount::cMaxOffset);
},
[](MPTIssue const& issue) { return STAmount(issue, maxMPTokenAmount); });
}
else
{
@@ -145,21 +194,23 @@ toMaxAmount(Issue const& issue)
}
inline STAmount
toSTAmount(Issue const& issue, Number const& n, Number::rounding_mode mode = Number::getround())
toSTAmount(Asset const& asset, Number const& n, Number::rounding_mode mode = Number::getround())
{
return toAmount<STAmount>(issue, n, mode);
return toAmount<STAmount>(asset, n, mode);
}
template <typename T>
Issue
getIssue(T const& amt)
Asset
getAsset(T const& amt)
{
if constexpr (std::is_same_v<IOUAmount, T>)
return noIssue();
else if constexpr (std::is_same_v<XRPAmount, T>)
return xrpIssue();
else if constexpr (std::is_same_v<MPTAmount, T>)
return noMPT();
else if constexpr (std::is_same_v<STAmount, T>)
return amt.issue();
return amt.asset();
else
{
constexpr bool alwaysFalse = !std::is_same_v<T, T>;
@@ -175,6 +226,8 @@ get(STAmount const& a)
return a.iou();
else if constexpr (std::is_same_v<XRPAmount, T>)
return a.xrp();
else if constexpr (std::is_same_v<MPTAmount, T>)
return a.mpt();
else if constexpr (std::is_same_v<STAmount, T>)
return a;
else

View File

@@ -2,20 +2,37 @@
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rules.h>
namespace xrpl {
class Asset;
class STAmount;
template <typename TIss>
concept ValidIssueType = std::is_same_v<TIss, Issue> || std::is_same_v<TIss, MPTIssue>;
template <typename T>
requires(
std::is_same_v<T, XRPAmount> || std::is_same_v<T, IOUAmount> ||
std::is_same_v<T, MPTAmount>)
struct AmountType
{
using amount_type = T;
};
template <typename A>
concept AssetType = std::is_convertible_v<A, Asset> || std::is_convertible_v<A, Issue> ||
std::is_convertible_v<A, MPTIssue> || std::is_convertible_v<A, MPTID>;
/* Used to check for an asset with either badCurrency()
* or MPT with 0 account.
*/
struct BadAsset
{
};
inline BadAsset const&
badAsset()
{
static BadAsset const a;
return a;
}
/* Asset is an abstraction of three different issue types: XRP, IOU, MPT.
* For historical reasons, two issue types XRP and IOU are wrapped in Issue
@@ -26,6 +43,9 @@ class Asset
{
public:
using value_type = std::variant<Issue, MPTIssue>;
using token_type = std::variant<Currency, MPTID>;
using AmtType =
std::variant<AmountType<XRPAmount>, AmountType<IOUAmount>, AmountType<MPTAmount>>;
private:
value_type issue_;
@@ -69,36 +89,42 @@ public:
constexpr value_type const&
value() const;
constexpr token_type
token() const;
void
setJson(Json::Value& jv) const;
STAmount
operator()(Number const&) const;
bool
constexpr AmtType
getAmountType() const;
// Custom, generic visit implementation
template <typename... Visitors>
constexpr auto
visit(Visitors&&... visitors) const -> decltype(auto)
{
// Simple delegation to the reusable utility, passing the internal
// variant data.
return detail::visit(issue_, std::forward<Visitors>(visitors)...);
}
constexpr bool
native() const
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
return issue.native();
if constexpr (std::is_same_v<TIss, MPTIssue>)
return false;
},
issue_);
return visit(
[&](Issue const& issue) { return issue.native(); },
[&](MPTIssue const&) { return false; });
}
bool
integral() const
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
return issue.native();
if constexpr (std::is_same_v<TIss, MPTIssue>)
return true;
},
issue_);
return visit(
[&](Issue const& issue) { return issue.native(); },
[&](MPTIssue const&) { return true; });
}
friend constexpr bool
@@ -110,6 +136,10 @@ public:
friend constexpr bool
operator==(Currency const& lhs, Asset const& rhs);
// rhs is either badCurrency() or MPT issuer is 0
friend constexpr bool
operator==(BadAsset const& lhs, Asset const& rhs);
/** Return true if both assets refer to the same currency (regardless of
* issuer) or MPT issuance. Otherwise return false.
*/
@@ -117,6 +147,12 @@ public:
equalTokens(Asset const& lhs, Asset const& rhs);
};
template <ValidIssueType TIss>
constexpr bool is_issue_v = std::is_same_v<TIss, Issue>;
template <ValidIssueType TIss>
constexpr bool is_mptissue_v = std::is_same_v<TIss, MPTIssue>;
inline Json::Value
to_json(Asset const& asset)
{
@@ -156,6 +192,29 @@ Asset::value() const
return issue_;
}
constexpr Asset::token_type
Asset::token() const
{
return visit(
[&](Issue const& issue) -> Asset::token_type { return issue.currency; },
[&](MPTIssue const& issue) -> Asset::token_type { return issue.getMptID(); });
}
constexpr Asset::AmtType
Asset::getAmountType() const
{
return visit(
[&](Issue const& issue) -> Asset::AmtType {
constexpr AmountType<XRPAmount> xrp;
constexpr AmountType<IOUAmount> iou;
return native() ? AmtType(xrp) : AmtType(iou);
},
[&](MPTIssue const& issue) -> Asset::AmtType {
constexpr AmountType<MPTAmount> mpt;
return AmtType(mpt);
});
}
constexpr bool
operator==(Asset const& lhs, Asset const& rhs)
{
@@ -177,7 +236,7 @@ operator<=>(Asset const& lhs, Asset const& rhs)
[]<ValidIssueType TLhs, ValidIssueType TRhs>(TLhs const& lhs_, TRhs const& rhs_) {
if constexpr (std::is_same_v<TLhs, TRhs>)
return std::weak_ordering(lhs_ <=> rhs_);
else if constexpr (std::is_same_v<TLhs, Issue> && std::is_same_v<TRhs, MPTIssue>)
else if constexpr (is_issue_v<TLhs> && is_mptissue_v<TRhs>)
return std::weak_ordering::greater;
else
return std::weak_ordering::less;
@@ -189,7 +248,17 @@ operator<=>(Asset const& lhs, Asset const& rhs)
constexpr bool
operator==(Currency const& lhs, Asset const& rhs)
{
return rhs.holds<Issue>() && rhs.get<Issue>().currency == lhs;
return rhs.visit(
[&](Issue const& issue) { return issue.currency == lhs; },
[](MPTIssue const& issue) { return false; });
}
constexpr bool
operator==(BadAsset const&, Asset const& rhs)
{
return rhs.visit(
[](Issue const& issue) -> bool { return badCurrency() == issue.currency; },
[](MPTIssue const& issue) -> bool { return issue.getIssuer() == xrpAccount(); });
}
constexpr bool
@@ -223,4 +292,36 @@ validJSONAsset(Json::Value const& jv);
Asset
assetFromJson(Json::Value const& jv);
Json::Value
to_json(Asset const& asset);
inline bool
isConsistent(Asset const& asset)
{
return asset.visit(
[](Issue const& issue) { return isConsistent(issue); },
[](MPTIssue const&) { return true; });
}
inline bool
validAsset(Asset const& asset)
{
return asset.visit(
[](Issue const& issue) { return isConsistent(issue) && issue.currency != badCurrency(); },
[](MPTIssue const& issue) { return issue.getIssuer() != xrpAccount(); });
}
template <class Hasher>
void
hash_append(Hasher& h, Asset const& r)
{
using beast::hash_append;
r.visit(
[&](Issue const& issue) { hash_append(h, issue); },
[&](MPTIssue const& issue) { hash_append(h, issue); });
}
std::ostream&
operator<<(std::ostream& os, Asset const& x);
} // namespace xrpl

View File

@@ -2,7 +2,7 @@
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Asset.h>
#include <boost/utility/base_from_member.hpp>
@@ -15,15 +15,15 @@ namespace xrpl {
class Book final : public CountedObject<Book>
{
public:
Issue in;
Issue out;
Asset in;
Asset out;
std::optional<uint256> domain;
Book()
{
}
Book(Issue const& in_, Issue const& out_, std::optional<uint256> const& domain_)
Book(Asset const& in_, Asset const& out_, std::optional<uint256> const& domain_)
: in(in_), out(out_), domain(domain_)
{
}
@@ -112,16 +112,67 @@ public:
}
};
template <>
struct hash<xrpl::MPTIssue> : private boost::base_from_member<std::hash<xrpl::MPTID>, 0>
{
private:
using id_hash_type = boost::base_from_member<std::hash<xrpl::MPTID>, 0>;
public:
explicit hash() = default;
using value_type = std::size_t;
using argument_type = xrpl::MPTIssue;
value_type
operator()(argument_type const& value) const
{
value_type const result(id_hash_type::member(value.getMptID()));
return result;
}
};
template <>
struct hash<xrpl::Asset>
{
private:
using value_type = std::size_t;
using argument_type = xrpl::Asset;
using issue_hasher = std::hash<xrpl::Issue>;
using mptissue_hasher = std::hash<xrpl::MPTIssue>;
issue_hasher m_issue_hasher;
mptissue_hasher m_mptissue_hasher;
public:
explicit hash() = default;
value_type
operator()(argument_type const& asset) const
{
return asset.visit(
[&](xrpl::Issue const& issue) {
value_type const result(m_issue_hasher(issue));
return result;
},
[&](xrpl::MPTIssue const& issue) {
value_type const result(m_mptissue_hasher(issue));
return result;
});
}
};
//------------------------------------------------------------------------------
template <>
struct hash<xrpl::Book>
{
private:
using issue_hasher = std::hash<xrpl::Issue>;
using asset_hasher = std::hash<xrpl::Asset>;
using uint256_hasher = xrpl::uint256::hasher;
issue_hasher m_issue_hasher;
asset_hasher m_asset_hasher;
uint256_hasher m_uint256_hasher;
public:
@@ -133,8 +184,8 @@ public:
value_type
operator()(argument_type const& value) const
{
value_type result(m_issue_hasher(value.in));
boost::hash_combine(result, m_issue_hasher(value.out));
value_type result(m_asset_hasher(value.in));
boost::hash_combine(result, m_asset_hasher(value.out));
if (value.domain)
boost::hash_combine(result, m_uint256_hasher(*value.domain));
@@ -159,6 +210,22 @@ struct hash<xrpl::Issue> : std::hash<xrpl::Issue>
// using Base::Base; // inherit ctors
};
template <>
struct hash<xrpl::MPTIssue> : std::hash<xrpl::MPTIssue>
{
explicit hash() = default;
using Base = std::hash<xrpl::MPTIssue>;
};
template <>
struct hash<xrpl::Asset> : std::hash<xrpl::Asset>
{
explicit hash() = default;
using Base = std::hash<xrpl::Asset>;
};
template <>
struct hash<xrpl::Book> : std::hash<xrpl::Book>
{

View File

@@ -0,0 +1,86 @@
#pragma once
#include <xrpl/protocol/UintTypes.h>
#include <type_traits>
namespace xrpl {
class STAmount;
class Asset;
class Issue;
class MPTIssue;
class IOUAmount;
class XRPAmount;
class MPTAmount;
template <typename A>
concept StepAmount =
std::is_same_v<A, XRPAmount> || std::is_same_v<A, IOUAmount> || std::is_same_v<A, MPTAmount>;
template <typename TIss>
concept ValidIssueType = std::is_same_v<TIss, Issue> || std::is_same_v<TIss, MPTIssue>;
template <typename A>
concept AssetType = std::is_convertible_v<A, Asset> || std::is_convertible_v<A, Issue> ||
std::is_convertible_v<A, MPTIssue> || std::is_convertible_v<A, MPTID>;
template <typename T>
concept ValidPathAsset = (std::is_same_v<T, Currency> || std::is_same_v<T, MPTID>);
template <class TTakerPays, class TTakerGets>
concept ValidTaker =
((std::is_same_v<TTakerPays, IOUAmount> || std::is_same_v<TTakerPays, XRPAmount> ||
std::is_same_v<TTakerPays, MPTAmount>) &&
(std::is_same_v<TTakerGets, IOUAmount> || std::is_same_v<TTakerGets, XRPAmount> ||
std::is_same_v<TTakerGets, MPTAmount>) &&
(!std::is_same_v<TTakerPays, XRPAmount> || !std::is_same_v<TTakerGets, XRPAmount>));
namespace detail {
// This template combines multiple callable objects (lambdas) into a single
// object that std::visit can use for overload resolution.
template <typename... Ts>
struct CombineVisitors : Ts...
{
// Bring all operator() overloads from base classes into this scope.
// It's the mechanism that makes the CombineVisitors struct function
// as a single callable object with multiple overloads.
using Ts::operator()...;
// Perfect forwarding constructor to correctly initialize the base class
// lambdas
constexpr CombineVisitors(Ts&&... ts) : Ts(std::forward<Ts>(ts))...
{
}
};
// This function forces function template argument deduction, which is more
// robust than class template argument deduction (CTAD) via the deduction guide.
template <typename... Ts>
constexpr CombineVisitors<std::decay_t<Ts>...>
make_combine_visitors(Ts&&... ts)
{
// std::decay_t<Ts> is used to remove references/constness from the lambda
// types before they are passed as template arguments to the CombineVisitors
// struct.
return CombineVisitors<std::decay_t<Ts>...>{std::forward<Ts>(ts)...};
}
// This function takes ANY variant and ANY number of visitors, and performs the
// visit. It is the reusable core logic.
template <typename Variant, typename... Visitors>
constexpr auto
visit(Variant&& v, Visitors&&... visitors) -> decltype(auto)
{
// Use the function template helper instead of raw CTAD.
auto visitor_set = make_combine_visitors(std::forward<Visitors>(visitors)...);
// Delegate to std::visit, perfectly forwarding the variant and the visitor
// set.
return std::visit(visitor_set, std::forward<Variant>(v));
}
} // namespace detail
} // namespace xrpl

View File

@@ -187,7 +187,8 @@ enum LedgerEntryType : std::uint16_t {
\
LEDGER_OBJECT(MPToken, \
LSF_FLAG2(lsfMPTLocked, 0x00000001) \
LSF_FLAG(lsfMPTAuthorized, 0x00000002)) \
LSF_FLAG(lsfMPTAuthorized, 0x00000002) \
LSF_FLAG(lsfMPTAMM, 0x00000004)) \
\
LEDGER_OBJECT(Credential, \
LSF_FLAG(lsfAccepted, 0x00010000)) \

View File

@@ -22,11 +22,12 @@ public:
using value_type = std::int64_t;
protected:
value_type value_;
value_type value_{};
public:
MPTAmount() = default;
constexpr MPTAmount(MPTAmount const& other) = default;
constexpr MPTAmount(beast::Zero);
constexpr MPTAmount&
operator=(MPTAmount const& other) = default;
@@ -85,6 +86,11 @@ constexpr MPTAmount::MPTAmount(value_type value) : value_(value)
{
}
constexpr MPTAmount::MPTAmount(beast::Zero)
{
*this = beast::zero;
}
constexpr MPTAmount&
MPTAmount::operator=(beast::Zero)
{
@@ -116,6 +122,14 @@ MPTAmount::value() const
return value_;
}
// Output MPTAmount as just the value.
template <class Char, class Traits>
std::basic_ostream<Char, Traits>&
operator<<(std::basic_ostream<Char, Traits>& os, MPTAmount const& q)
{
return os << q.value();
}
inline std::string
to_string(MPTAmount const& amount)
{

View File

@@ -17,7 +17,14 @@ private:
public:
MPTIssue() = default;
explicit MPTIssue(MPTID const& issuanceID);
MPTIssue(MPTID const& issuanceID);
MPTIssue(std::uint32_t sequence, AccountID const& account);
operator MPTID const&() const
{
return mptID_;
}
AccountID const&
getIssuer() const;
@@ -73,6 +80,47 @@ isXRP(MPTID const&)
return false;
}
inline AccountID
getMPTIssuer(MPTID const& mptid)
{
static_assert(sizeof(MPTID) == (sizeof(std::uint32_t) + sizeof(AccountID)));
// Extract the 20 bytes for the AccountID
std::array<std::uint8_t, sizeof(AccountID)> bytes{};
std::copy_n(mptid.data() + sizeof(std::uint32_t), sizeof(AccountID), bytes.begin());
// bit_cast is a "magic" compiler intrinsic that is
// usually optimized away to nothing in the final assembly.
return std::bit_cast<AccountID>(bytes);
}
// Disallow temporary
inline AccountID const&
getMPTIssuer(MPTID const&&) = delete;
inline AccountID const&
getMPTIssuer(MPTID&&) = delete;
inline MPTID
noMPT()
{
static MPTIssue const mpt{0, noAccount()};
return mpt.getMptID();
}
inline MPTID
badMPT()
{
static MPTIssue const mpt{0, xrpAccount()};
return mpt.getMptID();
}
template <class Hasher>
void
hash_append(Hasher& h, MPTIssue const& r)
{
using beast::hash_append;
hash_append(h, r.getMptID());
}
Json::Value
to_json(MPTIssue const& mptIssue);
@@ -82,4 +130,17 @@ to_string(MPTIssue const& mptIssue);
MPTIssue
mptIssueFromJson(Json::Value const& jv);
std::ostream&
operator<<(std::ostream& os, MPTIssue const& x);
} // namespace xrpl
namespace std {
template <>
struct hash<xrpl::MPTID> : xrpl::MPTID::hasher
{
explicit hash() = default;
};
} // namespace std

View File

@@ -0,0 +1,130 @@
#pragma once
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Concepts.h>
namespace xrpl {
/* Represent STPathElement's asset, which can be Currency or MPTID.
*/
class PathAsset
{
private:
std::variant<Currency, MPTID> easset_;
public:
PathAsset() = default;
// Enables comparing Asset and PathAsset
PathAsset(Asset const& asset);
PathAsset(Currency const& currency) : easset_(currency)
{
}
PathAsset(MPTID const& mpt) : easset_(mpt)
{
}
template <ValidPathAsset T>
constexpr bool
holds() const;
constexpr bool
isXRP() const;
template <ValidPathAsset T>
T const&
get() const;
constexpr std::variant<Currency, MPTID> const&
value() const;
// Custom, generic visit implementation
template <typename... Visitors>
constexpr auto
visit(Visitors&&... visitors) const -> decltype(auto)
{
// Simple delegation to the reusable utility, passing the internal
// variant data.
return detail::visit(easset_, std::forward<Visitors>(visitors)...);
}
friend constexpr bool
operator==(PathAsset const& lhs, PathAsset const& rhs);
};
template <ValidPathAsset PA>
constexpr bool is_currency_v = std::is_same_v<PA, Currency>;
template <ValidPathAsset PA>
constexpr bool is_mptid_v = std::is_same_v<PA, MPTID>;
inline PathAsset::PathAsset(Asset const& asset)
{
asset.visit(
[&](Issue const& issue) { easset_ = issue.currency; },
[&](MPTIssue const& issue) { easset_ = issue.getMptID(); });
}
template <ValidPathAsset T>
constexpr bool
PathAsset::holds() const
{
return std::holds_alternative<T>(easset_);
}
template <ValidPathAsset T>
T const&
PathAsset::get() const
{
if (!holds<T>())
Throw<std::runtime_error>("PathAsset doesn't hold requested asset.");
return std::get<T>(easset_);
}
constexpr std::variant<Currency, MPTID> const&
PathAsset::value() const
{
return easset_;
}
constexpr bool
PathAsset::isXRP() const
{
return visit(
[&](Currency const& currency) { return xrpl::isXRP(currency); },
[](MPTID const&) { return false; });
}
constexpr bool
operator==(PathAsset const& lhs, PathAsset const& rhs)
{
return std::visit(
[]<ValidPathAsset TLhs, ValidPathAsset TRhs>(TLhs const& lhs_, TRhs const& rhs_) {
if constexpr (std::is_same_v<TLhs, TRhs>)
return lhs_ == rhs_;
else
return false;
},
lhs.value(),
rhs.value());
}
template <typename Hasher>
void
hash_append(Hasher& h, PathAsset const& pathAsset)
{
std::visit([&]<ValidPathAsset T>(T const& e) { hash_append(h, e); }, pathAsset.value());
}
inline bool
isXRP(PathAsset const& asset)
{
return asset.isXRP();
}
std::string
to_string(PathAsset const& asset);
std::ostream&
operator<<(std::ostream& os, PathAsset const& x);
} // namespace xrpl

View File

@@ -164,12 +164,9 @@ public:
constexpr TIss const&
get() const;
Issue const&
issue() const;
// These three are deprecated
Currency const&
getCurrency() const;
template <ValidIssueType TIss>
TIss&
get();
AccountID const&
getIssuer() const;
@@ -225,9 +222,6 @@ public:
void
clear(Asset const& asset);
void
setIssuer(AccountID const& uIssuer);
/** Set the Issue for this amount. */
void
setIssue(Asset const& asset);
@@ -466,16 +460,11 @@ STAmount::get() const
return mAsset.get<TIss>();
}
inline Issue const&
STAmount::issue() const
template <ValidIssueType TIss>
TIss&
STAmount::get()
{
return get<Issue>();
}
inline Currency const&
STAmount::getCurrency() const
{
return mAsset.get<Issue>().currency;
return mAsset.get<TIss>();
}
inline AccountID const&
@@ -505,11 +494,13 @@ operator bool() const noexcept
inline STAmount::
operator Number() const
{
if (native())
return xrp();
if (mAsset.holds<MPTIssue>())
return mpt();
return iou();
return asset().visit(
[&](Issue const& issue) -> Number {
if (issue.native())
return xrp();
return iou();
},
[&](MPTIssue const&) -> Number { return mpt(); });
}
inline STAmount&
@@ -568,12 +559,6 @@ STAmount::clear(Asset const& asset)
clear();
}
inline void
STAmount::setIssuer(AccountID const& uIssuer)
{
mAsset.get<Issue>().account = uIssuer;
}
inline STAmount const&
STAmount::value() const noexcept
{

View File

@@ -349,6 +349,8 @@ public:
void
setFieldH128(SField const& field, uint128 const&);
void
setFieldH192(SField const& field, uint192 const&);
void
setFieldH256(SField const& field, uint256 const&);
void
setFieldI32(SField const& field, std::int32_t);

View File

@@ -3,6 +3,8 @@
#include <xrpl/basics/CountedObject.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/PathAsset.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/UintTypes.h>
@@ -16,7 +18,7 @@ class STPathElement final : public CountedObject<STPathElement>
{
unsigned int mType;
AccountID mAccountID;
Currency mCurrencyID;
PathAsset mAssetID;
AccountID mIssuerID;
bool is_offer_;
@@ -28,8 +30,10 @@ public:
typeAccount = 0x01, // Rippling through an account (vs taking an offer).
typeCurrency = 0x10, // Currency follows.
typeIssuer = 0x20, // Issuer follows.
typeMPT = 0x40, // MPT follows.
typeBoundary = 0xFF, // Boundary between alternate paths.
typeAll = typeAccount | typeCurrency | typeIssuer,
typeAsset = typeCurrency | typeMPT,
typeAll = typeAccount | typeCurrency | typeIssuer | typeMPT,
// Combination of all types.
};
@@ -40,19 +44,19 @@ public:
STPathElement(
std::optional<AccountID> const& account,
std::optional<Currency> const& currency,
std::optional<PathAsset> const& asset,
std::optional<AccountID> const& issuer);
STPathElement(
AccountID const& account,
Currency const& currency,
PathAsset const& asset,
AccountID const& issuer,
bool forceCurrency = false);
bool forceAsset = false);
STPathElement(
unsigned int uType,
AccountID const& account,
Currency const& currency,
PathAsset const& asset,
AccountID const& issuer);
auto
@@ -70,6 +74,12 @@ public:
bool
hasCurrency() const;
bool
hasMPT() const;
bool
hasAsset() const;
bool
isNone() const;
@@ -78,12 +88,21 @@ public:
AccountID const&
getAccountID() const;
PathAsset const&
getPathAsset() const;
Currency const&
getCurrency() const;
MPTID const&
getMPTID() const;
AccountID const&
getIssuerID() const;
bool
isType(Type const& pe) const;
bool
operator==(STPathElement const& t) const;
@@ -118,7 +137,7 @@ public:
emplace_back(Args&&... args);
bool
hasSeen(AccountID const& account, Currency const& currency, AccountID const& issuer) const;
hasSeen(AccountID const& account, PathAsset const& asset, AccountID const& issuer) const;
Json::Value getJson(JsonOptions) const;
@@ -221,7 +240,7 @@ inline STPathElement::STPathElement() : mType(typeNone), is_offer_(true)
inline STPathElement::STPathElement(
std::optional<AccountID> const& account,
std::optional<Currency> const& currency,
std::optional<PathAsset> const& asset,
std::optional<AccountID> const& issuer)
: mType(typeNone)
{
@@ -238,10 +257,10 @@ inline STPathElement::STPathElement(
mAccountID != noAccount(), "xrpl::STPathElement::STPathElement : account is set");
}
if (currency)
if (asset)
{
mCurrencyID = *currency;
mType |= typeCurrency;
mAssetID = *asset;
mType |= mAssetID.holds<Currency>() ? typeCurrency : typeMPT;
}
if (issuer)
@@ -256,20 +275,20 @@ inline STPathElement::STPathElement(
inline STPathElement::STPathElement(
AccountID const& account,
Currency const& currency,
PathAsset const& asset,
AccountID const& issuer,
bool forceCurrency)
bool forceAsset)
: mType(typeNone)
, mAccountID(account)
, mCurrencyID(currency)
, mAssetID(asset)
, mIssuerID(issuer)
, is_offer_(isXRP(mAccountID))
{
if (!is_offer_)
mType |= typeAccount;
if (forceCurrency || !isXRP(currency))
mType |= typeCurrency;
if (forceAsset || !isXRP(mAssetID))
mType |= asset.holds<Currency>() ? typeCurrency : typeMPT;
if (!isXRP(issuer))
mType |= typeIssuer;
@@ -280,14 +299,19 @@ inline STPathElement::STPathElement(
inline STPathElement::STPathElement(
unsigned int uType,
AccountID const& account,
Currency const& currency,
PathAsset const& asset,
AccountID const& issuer)
: mType(uType)
, mAccountID(account)
, mCurrencyID(currency)
, mAssetID(asset)
, mIssuerID(issuer)
, is_offer_(isXRP(mAccountID))
{
// uType could be assetType; i.e. either Currency or MPTID.
// Get the actual type.
mAssetID.visit(
[&](Currency const&) { mType = mType & (~Type::typeMPT); },
[&](MPTID const&) { mType = mType & (~Type::typeCurrency); });
hash_value_ = get_hash(*this);
}
@@ -309,16 +333,34 @@ STPathElement::isAccount() const
return !isOffer();
}
inline bool
STPathElement::isType(Type const& pe) const
{
return (mType & pe) != 0u;
}
inline bool
STPathElement::hasIssuer() const
{
return getNodeType() & STPathElement::typeIssuer;
return isType(STPathElement::typeIssuer);
}
inline bool
STPathElement::hasCurrency() const
{
return getNodeType() & STPathElement::typeCurrency;
return isType(STPathElement::typeCurrency);
}
inline bool
STPathElement::hasMPT() const
{
return isType(STPathElement::typeMPT);
}
inline bool
STPathElement::hasAsset() const
{
return isType(STPathElement::typeAsset);
}
inline bool
@@ -335,10 +377,22 @@ STPathElement::getAccountID() const
return mAccountID;
}
inline PathAsset const&
STPathElement::getPathAsset() const
{
return mAssetID;
}
inline Currency const&
STPathElement::getCurrency() const
{
return mCurrencyID;
return mAssetID.get<Currency>();
}
inline MPTID const&
STPathElement::getMPTID() const
{
return mAssetID.get<MPTID>();
}
inline AccountID const&
@@ -351,7 +405,7 @@ inline bool
STPathElement::operator==(STPathElement const& t) const
{
return (mType & typeAccount) == (t.mType & typeAccount) && hash_value_ == t.hash_value_ &&
mAccountID == t.mAccountID && mCurrencyID == t.mCurrencyID && mIssuerID == t.mIssuerID;
mAccountID == t.mAccountID && mAssetID == t.mAssetID && mIssuerID == t.mIssuerID;
}
inline bool

View File

@@ -121,6 +121,7 @@ enum TEMcodes : TERUnderlyingType {
temARRAY_TOO_LARGE,
temBAD_TRANSFER_FEE,
temINVALID_INNER_BATCH,
temBAD_MPT,
};
//------------------------------------------------------------------------------
@@ -208,6 +209,7 @@ enum TERcodes : TERUnderlyingType {
terADDRESS_COLLISION, // Failed to allocate AccountID when trying to
// create a pseudo-account
terNO_DELEGATE_PERMISSION, // Delegate does not have permission
terLOCKED, // MPT is locked
};
//------------------------------------------------------------------------------
@@ -342,10 +344,6 @@ enum TECcodes : TERUnderlyingType {
tecLIMIT_EXCEEDED = 195,
tecPSEUDO_ACCOUNT = 196,
tecPRECISION_LOSS = 197,
// DEPRECATED: This error code tecNO_DELEGATE_PERMISSION is reserved for
// backward compatibility with historical data on non-prod networks, can be
// reclaimed after those networks reset.
tecNO_DELEGATE_PERMISSION = 198,
};
//------------------------------------------------------------------------------

View File

@@ -15,6 +15,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(MPTokensV2, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (Security3_1_3, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -161,8 +161,10 @@ LEDGER_ENTRY(ltDIR_NODE, 0x0064, DirectoryNode, directory, ({
{sfOwner, soeOPTIONAL}, // for owner directories
{sfTakerPaysCurrency, soeOPTIONAL}, // order book directories
{sfTakerPaysIssuer, soeOPTIONAL}, // order book directories
{sfTakerPaysMPT, soeOPTIONAL}, // order book directories
{sfTakerGetsCurrency, soeOPTIONAL}, // order book directories
{sfTakerGetsIssuer, soeOPTIONAL}, // order book directories
{sfTakerGetsMPT, soeOPTIONAL}, // order book directories
{sfExchangeRate, soeOPTIONAL}, // order book directories
{sfIndexes, soeREQUIRED},
{sfRootIndex, soeREQUIRED},

View File

@@ -159,6 +159,8 @@ TYPED_SFIELD(sfTakerGetsIssuer, UINT160, 4)
// 192-bit (common)
TYPED_SFIELD(sfMPTokenIssuanceID, UINT192, 1)
TYPED_SFIELD(sfShareMPTID, UINT192, 2)
TYPED_SFIELD(sfTakerPaysMPT, UINT192, 3)
TYPED_SFIELD(sfTakerGetsMPT, UINT192, 4)
// 256-bit (common)
TYPED_SFIELD(sfLedgerHash, UINT256, 1)

View File

@@ -27,7 +27,7 @@
TRANSACTION(ttPAYMENT, 0, Payment,
Delegation::delegable,
uint256{},
createAcct,
createAcct | mayCreateMPT,
({
{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
@@ -129,10 +129,10 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey,
TRANSACTION(ttOFFER_CREATE, 7, OfferCreate,
Delegation::delegable,
uint256{},
noPriv,
mayCreateMPT,
({
{sfTakerPays, soeREQUIRED},
{sfTakerGets, soeREQUIRED},
{sfTakerPays, soeREQUIRED, soeMPTSupported},
{sfTakerGets, soeREQUIRED, soeMPTSupported},
{sfExpiration, soeOPTIONAL},
{sfOfferSequence, soeOPTIONAL},
{sfDomainID, soeOPTIONAL},
@@ -239,7 +239,7 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate,
noPriv,
({
{sfDestination, soeREQUIRED},
{sfSendMax, soeREQUIRED},
{sfSendMax, soeREQUIRED, soeMPTSupported},
{sfExpiration, soeOPTIONAL},
{sfDestinationTag, soeOPTIONAL},
{sfInvoiceID, soeOPTIONAL},
@@ -252,11 +252,11 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate,
TRANSACTION(ttCHECK_CASH, 17, CheckCash,
Delegation::delegable,
uint256{},
noPriv,
mayCreateMPT,
({
{sfCheckID, soeREQUIRED},
{sfAmount, soeOPTIONAL},
{sfDeliverMin, soeOPTIONAL},
{sfAmount, soeOPTIONAL, soeMPTSupported},
{sfDeliverMin, soeOPTIONAL, soeMPTSupported},
}))
/** This transaction type cancels an existing check. */
@@ -409,12 +409,12 @@ TRANSACTION(ttCLAWBACK, 30, Clawback,
TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback,
Delegation::delegable,
featureAMMClawback,
mayDeleteAcct | overrideFreeze,
mayDeleteAcct | overrideFreeze | mayAuthorizeMPT,
({
{sfHolder, soeREQUIRED},
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfAmount, soeOPTIONAL},
{sfAsset, soeREQUIRED, soeMPTSupported},
{sfAsset2, soeREQUIRED, soeMPTSupported},
{sfAmount, soeOPTIONAL, soeMPTSupported},
}))
/** This transaction type creates an AMM instance */
@@ -424,10 +424,10 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback,
TRANSACTION(ttAMM_CREATE, 35, AMMCreate,
Delegation::delegable,
featureAMM,
createPseudoAcct,
createPseudoAcct | mayCreateMPT,
({
{sfAmount, soeREQUIRED},
{sfAmount2, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
{sfAmount2, soeREQUIRED, soeMPTSupported},
{sfTradingFee, soeREQUIRED},
}))
@@ -440,10 +440,10 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit,
featureAMM,
noPriv,
({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfAmount, soeOPTIONAL},
{sfAmount2, soeOPTIONAL},
{sfAsset, soeREQUIRED, soeMPTSupported},
{sfAsset2, soeREQUIRED, soeMPTSupported},
{sfAmount, soeOPTIONAL, soeMPTSupported},
{sfAmount2, soeOPTIONAL, soeMPTSupported},
{sfEPrice, soeOPTIONAL},
{sfLPTokenOut, soeOPTIONAL},
{sfTradingFee, soeOPTIONAL},
@@ -456,12 +456,12 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit,
TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw,
Delegation::delegable,
featureAMM,
mayDeleteAcct,
mayDeleteAcct | mayAuthorizeMPT,
({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfAmount, soeOPTIONAL},
{sfAmount2, soeOPTIONAL},
{sfAsset, soeREQUIRED, soeMPTSupported},
{sfAsset2, soeREQUIRED, soeMPTSupported},
{sfAmount, soeOPTIONAL, soeMPTSupported},
{sfAmount2, soeOPTIONAL, soeMPTSupported},
{sfEPrice, soeOPTIONAL},
{sfLPTokenIn, soeOPTIONAL},
}))
@@ -475,8 +475,8 @@ TRANSACTION(ttAMM_VOTE, 38, AMMVote,
featureAMM,
noPriv,
({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfAsset, soeREQUIRED, soeMPTSupported},
{sfAsset2, soeREQUIRED, soeMPTSupported},
{sfTradingFee, soeREQUIRED},
}))
@@ -489,8 +489,8 @@ TRANSACTION(ttAMM_BID, 39, AMMBid,
featureAMM,
noPriv,
({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfAsset, soeREQUIRED, soeMPTSupported},
{sfAsset2, soeREQUIRED, soeMPTSupported},
{sfBidMin, soeOPTIONAL},
{sfBidMax, soeOPTIONAL},
{sfAuthAccounts, soeOPTIONAL},
@@ -503,10 +503,10 @@ TRANSACTION(ttAMM_BID, 39, AMMBid,
TRANSACTION(ttAMM_DELETE, 40, AMMDelete,
Delegation::delegable,
featureAMM,
mustDeleteAcct,
mustDeleteAcct | mayDeleteMPT,
({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfAsset, soeREQUIRED, soeMPTSupported},
{sfAsset2, soeREQUIRED, soeMPTSupported},
}))
/** This transactions creates a crosschain sequence number */

View File

@@ -401,6 +401,8 @@ JSS(min_ledger); // in: LedgerCleaner
JSS(minimum_fee); // out: TxQ
JSS(minimum_level); // out: TxQ
JSS(missingCommand); // error
JSS(mpt_issuance_id_a); // out: BookChanges
JSS(mpt_issuance_id_b); // out: BookChanges
JSS(name); // out: AmendmentTableImpl, PeerImp
JSS(needed_state_hashes); // out: InboundLedger
JSS(needed_transaction_hashes); // out: InboundLedger

View File

@@ -117,6 +117,30 @@ public:
return this->sle_->isFieldPresent(sfTakerPaysIssuer);
}
/**
* @brief Get sfTakerPaysMPT (soeOPTIONAL)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT192::type::value_type>
getTakerPaysMPT() const
{
if (hasTakerPaysMPT())
return this->sle_->at(sfTakerPaysMPT);
return std::nullopt;
}
/**
* @brief Check if sfTakerPaysMPT is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasTakerPaysMPT() const
{
return this->sle_->isFieldPresent(sfTakerPaysMPT);
}
/**
* @brief Get sfTakerGetsCurrency (soeOPTIONAL)
* @return The field value, or std::nullopt if not present.
@@ -165,6 +189,30 @@ public:
return this->sle_->isFieldPresent(sfTakerGetsIssuer);
}
/**
* @brief Get sfTakerGetsMPT (soeOPTIONAL)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT192::type::value_type>
getTakerGetsMPT() const
{
if (hasTakerGetsMPT())
return this->sle_->at(sfTakerGetsMPT);
return std::nullopt;
}
/**
* @brief Check if sfTakerGetsMPT is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasTakerGetsMPT() const
{
return this->sle_->isFieldPresent(sfTakerGetsMPT);
}
/**
* @brief Get sfExchangeRate (soeOPTIONAL)
* @return The field value, or std::nullopt if not present.
@@ -427,6 +475,17 @@ public:
return *this;
}
/**
* @brief Set sfTakerPaysMPT (soeOPTIONAL)
* @return Reference to this builder for method chaining.
*/
DirectoryNodeBuilder&
setTakerPaysMPT(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfTakerPaysMPT] = value;
return *this;
}
/**
* @brief Set sfTakerGetsCurrency (soeOPTIONAL)
* @return Reference to this builder for method chaining.
@@ -449,6 +508,17 @@ public:
return *this;
}
/**
* @brief Set sfTakerGetsMPT (soeOPTIONAL)
* @return Reference to this builder for method chaining.
*/
DirectoryNodeBuilder&
setTakerGetsMPT(std::decay_t<typename SF_UINT192::type::value_type> const& value)
{
object_[sfTakerGetsMPT] = value;
return *this;
}
/**
* @brief Set sfExchangeRate (soeOPTIONAL)
* @return Reference to this builder for method chaining.

View File

@@ -49,6 +49,7 @@ public:
/**
* @brief Get sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -60,6 +61,7 @@ public:
/**
* @brief Get sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -192,6 +194,7 @@ public:
/**
* @brief Set sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMBidBuilder&
@@ -203,6 +206,7 @@ public:
/**
* @brief Set sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMBidBuilder&

View File

@@ -21,7 +21,7 @@ class AMMClawbackBuilder;
* Type: ttAMM_CLAWBACK (31)
* Delegable: Delegation::delegable
* Amendment: featureAMMClawback
* Privileges: mayDeleteAcct | overrideFreeze
* Privileges: mayDeleteAcct | overrideFreeze | mayAuthorizeMPT
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMClawbackBuilder to construct new transactions.
@@ -60,6 +60,7 @@ public:
/**
* @brief Get sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -71,6 +72,7 @@ public:
/**
* @brief Get sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -82,6 +84,7 @@ public:
/**
* @brief Get sfAmount (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
@@ -166,6 +169,7 @@ public:
/**
* @brief Set sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMClawbackBuilder&
@@ -177,6 +181,7 @@ public:
/**
* @brief Set sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMClawbackBuilder&
@@ -188,6 +193,7 @@ public:
/**
* @brief Set sfAmount (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMClawbackBuilder&

View File

@@ -21,7 +21,7 @@ class AMMCreateBuilder;
* Type: ttAMM_CREATE (35)
* Delegable: Delegation::delegable
* Amendment: featureAMM
* Privileges: createPseudoAcct
* Privileges: createPseudoAcct | mayCreateMPT
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMCreateBuilder to construct new transactions.
@@ -49,6 +49,7 @@ public:
/**
* @brief Get sfAmount (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -60,6 +61,7 @@ public:
/**
* @brief Get sfAmount2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -129,6 +131,7 @@ public:
/**
* @brief Set sfAmount (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMCreateBuilder&
@@ -140,6 +143,7 @@ public:
/**
* @brief Set sfAmount2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMCreateBuilder&

View File

@@ -21,7 +21,7 @@ class AMMDeleteBuilder;
* Type: ttAMM_DELETE (40)
* Delegable: Delegation::delegable
* Amendment: featureAMM
* Privileges: mustDeleteAcct
* Privileges: mustDeleteAcct | mayDeleteMPT
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMDeleteBuilder to construct new transactions.
@@ -49,6 +49,7 @@ public:
/**
* @brief Get sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -60,6 +61,7 @@ public:
/**
* @brief Get sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -116,6 +118,7 @@ public:
/**
* @brief Set sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMDeleteBuilder&
@@ -127,6 +130,7 @@ public:
/**
* @brief Set sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMDeleteBuilder&

View File

@@ -49,6 +49,7 @@ public:
/**
* @brief Get sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -60,6 +61,7 @@ public:
/**
* @brief Get sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -71,6 +73,7 @@ public:
/**
* @brief Get sfAmount (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
@@ -97,6 +100,7 @@ public:
/**
* @brief Get sfAmount2 (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
@@ -246,6 +250,7 @@ public:
/**
* @brief Set sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMDepositBuilder&
@@ -257,6 +262,7 @@ public:
/**
* @brief Set sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMDepositBuilder&
@@ -268,6 +274,7 @@ public:
/**
* @brief Set sfAmount (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMDepositBuilder&
@@ -279,6 +286,7 @@ public:
/**
* @brief Set sfAmount2 (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMDepositBuilder&

View File

@@ -49,6 +49,7 @@ public:
/**
* @brief Get sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -60,6 +61,7 @@ public:
/**
* @brief Get sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -129,6 +131,7 @@ public:
/**
* @brief Set sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMVoteBuilder&
@@ -140,6 +143,7 @@ public:
/**
* @brief Set sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMVoteBuilder&

View File

@@ -21,7 +21,7 @@ class AMMWithdrawBuilder;
* Type: ttAMM_WITHDRAW (37)
* Delegable: Delegation::delegable
* Amendment: featureAMM
* Privileges: mayDeleteAcct
* Privileges: mayDeleteAcct | mayAuthorizeMPT
*
* Immutable wrapper around STTx providing type-safe field access.
* Use AMMWithdrawBuilder to construct new transactions.
@@ -49,6 +49,7 @@ public:
/**
* @brief Get sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -60,6 +61,7 @@ public:
/**
* @brief Get sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -71,6 +73,7 @@ public:
/**
* @brief Get sfAmount (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
@@ -97,6 +100,7 @@ public:
/**
* @brief Get sfAmount2 (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
@@ -220,6 +224,7 @@ public:
/**
* @brief Set sfAsset (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMWithdrawBuilder&
@@ -231,6 +236,7 @@ public:
/**
* @brief Set sfAsset2 (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMWithdrawBuilder&
@@ -242,6 +248,7 @@ public:
/**
* @brief Set sfAmount (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMWithdrawBuilder&
@@ -253,6 +260,7 @@ public:
/**
* @brief Set sfAmount2 (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
AMMWithdrawBuilder&

View File

@@ -21,7 +21,7 @@ class CheckCashBuilder;
* Type: ttCHECK_CASH (17)
* Delegable: Delegation::delegable
* Amendment: uint256{}
* Privileges: noPriv
* Privileges: mayCreateMPT
*
* Immutable wrapper around STTx providing type-safe field access.
* Use CheckCashBuilder to construct new transactions.
@@ -60,6 +60,7 @@ public:
/**
* @brief Get sfAmount (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
@@ -86,6 +87,7 @@ public:
/**
* @brief Get sfDeliverMin (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
@@ -166,6 +168,7 @@ public:
/**
* @brief Set sfAmount (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
CheckCashBuilder&
@@ -177,6 +180,7 @@ public:
/**
* @brief Set sfDeliverMin (soeOPTIONAL)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
CheckCashBuilder&

View File

@@ -60,6 +60,7 @@ public:
/**
* @brief Get sfSendMax (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -205,6 +206,7 @@ public:
/**
* @brief Set sfSendMax (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
CheckCreateBuilder&

View File

@@ -21,7 +21,7 @@ class OfferCreateBuilder;
* Type: ttOFFER_CREATE (7)
* Delegable: Delegation::delegable
* Amendment: uint256{}
* Privileges: noPriv
* Privileges: mayCreateMPT
*
* Immutable wrapper around STTx providing type-safe field access.
* Use OfferCreateBuilder to construct new transactions.
@@ -49,6 +49,7 @@ public:
/**
* @brief Get sfTakerPays (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -60,6 +61,7 @@ public:
/**
* @brief Get sfTakerGets (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -194,6 +196,7 @@ public:
/**
* @brief Set sfTakerPays (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
OfferCreateBuilder&
@@ -205,6 +208,7 @@ public:
/**
* @brief Set sfTakerGets (soeREQUIRED)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
OfferCreateBuilder&

View File

@@ -21,7 +21,7 @@ class PaymentBuilder;
* Type: ttPAYMENT (0)
* Delegable: Delegation::delegable
* Amendment: uint256{}
* Privileges: createAcct
* Privileges: createAcct | mayCreateMPT
*
* Immutable wrapper around STTx providing type-safe field access.
* Use PaymentBuilder to construct new transactions.

View File

@@ -398,7 +398,9 @@ using InvariantChecks = std::tuple<
ValidPseudoAccounts,
ValidLoanBroker,
ValidLoan,
ValidVault>;
ValidVault,
ValidMPTPayment,
ValidMPTTransfer>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -44,6 +44,7 @@ enum Privilege {
mayDeleteMPT = 0x0400, // The transaction MAY delete an MPT object. May not create.
mustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault
mayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault
mayCreateMPT = 0x2000, // The transaction MAY create an MPT object, except for issuer.
};
constexpr Privilege

View File

@@ -28,4 +28,50 @@ public:
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/** Verify:
* - OutstandingAmount <= MaximumAmount for any MPT
* - OutstandingAmount after = OutstandingAmount before +
* sum (MPT after - MPT before) - this is total MPT credit/debit
*/
class ValidMPTPayment
{
enum Order { Before = 0, After = 1 };
struct MPTData
{
std::array<std::int64_t, After + 1> outstanding{};
// sum (MPT after - MPT before)
std::int64_t mptAmount{0};
};
// true if OutstandingAmount > MaximumAmount in after for any MPT
bool overflow_{false};
// mptid:MPTData
hash_map<uint192, MPTData> data_;
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&);
};
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_;
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&);
};
} // namespace xrpl

View File

@@ -4,13 +4,13 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/AMMUtils.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/tx/transactors/dex/AMMContext.h>
namespace xrpl {
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
class AMMOffer;
/** AMMLiquidity class provides AMM offers to BookStep class.
@@ -35,8 +35,8 @@ private:
AMMContext& ammContext_;
AccountID const ammAccountID_;
std::uint32_t const tradingFee_;
Issue const issueIn_;
Issue const issueOut_;
Asset const assetIn_;
Asset const assetOut_;
// Initial AMM pool balances
TAmounts<TIn, TOut> const initialBalances_;
beast::Journal const j_;
@@ -46,8 +46,8 @@ public:
ReadView const& view,
AccountID const& ammAccountID,
std::uint32_t tradingFee,
Issue const& in,
Issue const& out,
Asset const& in,
Asset const& out,
AMMContext& ammContext,
beast::Journal j);
~AMMLiquidity() = default;
@@ -87,16 +87,16 @@ public:
return ammContext_;
}
Issue const&
issueIn() const
Asset const&
assetIn() const
{
return issueIn_;
return assetIn_;
}
Issue const&
issueOut() const
Asset const&
assetOut() const
{
return issueOut_;
return assetOut_;
}
private:

View File

@@ -3,6 +3,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/TER.h>
@@ -16,7 +17,7 @@ class QualityFunction;
* methods for use in generic BookStep methods. AMMOffer amounts
* are changed indirectly in BookStep limiting steps.
*/
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
class AMMOffer
{
private:
@@ -52,8 +53,11 @@ public:
return quality_;
}
Issue const&
issueIn() const;
Asset const&
assetIn() const;
Asset const&
assetOut() const;
AccountID const&
owner() const;
@@ -99,7 +103,8 @@ public:
static TER
send(Args&&... args)
{
return accountSend(std::forward<Args>(args)..., WaiveTransferFee::Yes);
return accountSend(
std::forward<Args>(args)..., WaiveTransferFee::Yes, AllowMPTOverflow::Yes);
}
bool

View File

@@ -3,6 +3,8 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/contract.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
@@ -12,28 +14,15 @@
namespace xrpl {
template <class TIn, class TOut>
class TOfferBase
{
protected:
Issue issIn_;
Issue issOut_;
};
template <>
class TOfferBase<STAmount, STAmount>
{
public:
explicit TOfferBase() = default;
};
template <class TIn = STAmount, class TOut = STAmount>
class TOffer : private TOfferBase<TIn, TOut>
template <StepAmount TIn, StepAmount TOut>
class TOffer
{
private:
SLE::pointer m_entry;
Quality m_quality{};
AccountID m_account;
Asset assetIn_;
Asset assetOut_;
TAmounts<TIn, TOut> m_amounts{};
void
@@ -113,10 +102,10 @@ public:
return m_entry->key();
}
Issue const&
issueIn() const;
Issue const&
issueOut() const;
Asset const&
assetIn() const;
Asset const&
assetOut() const;
TAmounts<TIn, TOut>
limitOut(TAmounts<TIn, TOut> const& offerAmount, TOut const& limit, bool roundUp) const;
@@ -131,8 +120,8 @@ public:
bool
isFunded() const
{
// Offer owner is issuer; they have unlimited funds
return m_account == issueOut().account;
// Offer owner is issuer; they have unlimited funds if IOU
return m_account == assetOut_.getIssuer() && assetOut_.holds<Issue>();
}
static std::pair<std::uint32_t, std::uint32_t>
@@ -167,9 +156,7 @@ public:
}
};
using Offer = TOffer<>;
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
TOffer<TIn, TOut>::TOffer(SLE::pointer const& entry, Quality quality)
: m_entry(entry), m_quality(quality), m_account(m_entry->getAccountID(sfAccount))
{
@@ -177,33 +164,26 @@ TOffer<TIn, TOut>::TOffer(SLE::pointer const& entry, Quality quality)
auto const tg = m_entry->getFieldAmount(sfTakerGets);
m_amounts.in = toAmount<TIn>(tp);
m_amounts.out = toAmount<TOut>(tg);
this->issIn_ = tp.issue();
this->issOut_ = tg.issue();
assetIn_ = tp.asset();
assetOut_ = tg.asset();
}
template <>
inline TOffer<STAmount, STAmount>::TOffer(SLE::pointer const& entry, Quality quality)
: m_entry(entry)
, m_quality(quality)
, m_account(m_entry->getAccountID(sfAccount))
, m_amounts(m_entry->getFieldAmount(sfTakerPays), m_entry->getFieldAmount(sfTakerGets))
{
}
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
void
TOffer<TIn, TOut>::setFieldAmounts()
{
// LCOV_EXCL_START
#ifdef _MSC_VER
UNREACHABLE("xrpl::TOffer::setFieldAmounts : must be specialized");
#else
static_assert(sizeof(TOut) == -1, "Must be specialized");
#endif
// LCOV_EXCL_STOP
if constexpr (std::is_same_v<TIn, XRPAmount>)
m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in));
else
m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, assetIn_));
if constexpr (std::is_same_v<TOut, XRPAmount>)
m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out));
else
m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, assetOut_));
}
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
TAmounts<TIn, TOut>
TOffer<TIn, TOut>::limitOut(TAmounts<TIn, TOut> const& offerAmount, TOut const& limit, bool roundUp)
const
@@ -213,7 +193,7 @@ TOffer<TIn, TOut>::limitOut(TAmounts<TIn, TOut> const& offerAmount, TOut const&
return quality().ceil_out_strict(offerAmount, limit, roundUp);
}
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
TAmounts<TIn, TOut>
TOffer<TIn, TOut>::limitIn(TAmounts<TIn, TOut> const& offerAmount, TIn const& limit, bool roundUp)
const
@@ -228,75 +208,29 @@ TOffer<TIn, TOut>::limitIn(TAmounts<TIn, TOut> const& offerAmount, TIn const& li
return m_quality.ceil_in(offerAmount, limit);
}
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
template <typename... Args>
TER
TOffer<TIn, TOut>::send(Args&&... args)
{
return accountSend(std::forward<Args>(args)...);
return accountSend(std::forward<Args>(args)..., WaiveTransferFee::No, AllowMPTOverflow::Yes);
}
template <>
inline void
TOffer<STAmount, STAmount>::setFieldAmounts()
template <StepAmount TIn, StepAmount TOut>
Asset const&
TOffer<TIn, TOut>::assetIn() const
{
m_entry->setFieldAmount(sfTakerPays, m_amounts.in);
m_entry->setFieldAmount(sfTakerGets, m_amounts.out);
return assetIn_;
}
template <>
inline void
TOffer<IOUAmount, IOUAmount>::setFieldAmounts()
template <StepAmount TIn, StepAmount TOut>
Asset const&
TOffer<TIn, TOut>::assetOut() const
{
m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_));
m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_));
return assetOut_;
}
template <>
inline void
TOffer<IOUAmount, XRPAmount>::setFieldAmounts()
{
m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in, issIn_));
m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out));
}
template <>
inline void
TOffer<XRPAmount, IOUAmount>::setFieldAmounts()
{
m_entry->setFieldAmount(sfTakerPays, toSTAmount(m_amounts.in));
m_entry->setFieldAmount(sfTakerGets, toSTAmount(m_amounts.out, issOut_));
}
template <class TIn, class TOut>
Issue const&
TOffer<TIn, TOut>::issueIn() const
{
return this->issIn_;
}
template <>
inline Issue const&
TOffer<STAmount, STAmount>::issueIn() const
{
return m_amounts.in.issue();
}
template <class TIn, class TOut>
Issue const&
TOffer<TIn, TOut>::issueOut() const
{
return this->issOut_;
}
template <>
inline Issue const&
TOffer<STAmount, STAmount>::issueOut() const
{
return m_amounts.out.issue();
}
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
inline std::ostream&
operator<<(std::ostream& os, TOffer<TIn, TOut> const& offer)
{

View File

@@ -4,6 +4,7 @@
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/tx/paths/BookTip.h>
#include <xrpl/tx/paths/Offer.h>
@@ -11,7 +12,7 @@
namespace xrpl {
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
class TOfferStreamBase
{
public:
@@ -64,6 +65,7 @@ protected:
permRmOffer(uint256 const& offerIndex) = 0;
template <class TTakerPays, class TTakerGets>
requires ValidTaker<TTakerPays, TTakerGets>
bool
shouldRmSmallIncreasedQOffer() const;
@@ -105,33 +107,6 @@ public:
}
};
/** Presents and consumes the offers in an order book.
Two `ApplyView` objects accumulate changes to the ledger. `view`
is applied when the calling transaction succeeds. If the calling
transaction fails, then `view_cancel` is applied.
Certain invalid offers are automatically removed:
- Offers with missing ledger entries
- Offers that expired
- Offers found unfunded:
An offer is found unfunded when the corresponding balance is zero
and the caller has not modified the balance. This is accomplished
by also looking up the balance in the cancel view.
When an offer is removed, it is removed from both views. This grooms the
order book regardless of whether or not the transaction is successful.
*/
class OfferStream : public TOfferStreamBase<STAmount, STAmount>
{
protected:
void
permRmOffer(uint256 const& offerIndex) override;
public:
using TOfferStreamBase<STAmount, STAmount>::TOfferStreamBase;
};
/** Presents and consumes the offers in an order book.
The `view_' ` `ApplyView` accumulates changes to the ledger.
@@ -149,7 +124,7 @@ public:
and the caller has not modified the balance. This is accomplished
by also looking up the balance in the cancel view.
*/
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
class FlowOfferStream : public TOfferStreamBase<TIn, TOut>
{
private:

View File

@@ -1,198 +0,0 @@
#pragma once
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/XRPAmount.h>
#include <optional>
namespace xrpl {
struct AmountSpec
{
explicit AmountSpec() = default;
bool native{};
union
{
XRPAmount xrp;
IOUAmount iou = {};
};
std::optional<AccountID> issuer;
std::optional<Currency> currency;
friend std::ostream&
operator<<(std::ostream& stream, AmountSpec const& amt)
{
if (amt.native)
stream << to_string(amt.xrp);
else
stream << to_string(amt.iou);
if (amt.currency)
stream << "/(" << *amt.currency << ")";
if (amt.issuer)
stream << "/" << *amt.issuer << "";
return stream;
}
};
struct EitherAmount
{
#ifndef NDEBUG
bool native = false;
#endif
union
{
IOUAmount iou = {};
XRPAmount xrp;
};
EitherAmount() = default;
explicit EitherAmount(IOUAmount const& a) : iou(a)
{
}
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
// ignore warning about half of iou amount being uninitialized
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
explicit EitherAmount(XRPAmount const& a) : xrp(a)
{
#ifndef NDEBUG
native = true;
#endif
}
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic pop
#endif
explicit EitherAmount(AmountSpec const& a)
{
#ifndef NDEBUG
native = a.native;
#endif
if (a.native)
xrp = a.xrp;
else
iou = a.iou;
}
#ifndef NDEBUG
friend std::ostream&
operator<<(std::ostream& stream, EitherAmount const& amt)
{
if (amt.native)
stream << to_string(amt.xrp);
else
stream << to_string(amt.iou);
return stream;
}
#endif
};
template <class T>
T&
get(EitherAmount& amt)
{
static_assert(sizeof(T) == -1, "Must used specialized function");
return T(0);
}
template <>
inline IOUAmount&
get<IOUAmount>(EitherAmount& amt)
{
XRPL_ASSERT(!amt.native, "xrpl::get<IOUAmount>(EitherAmount&) : is not XRP");
return amt.iou;
}
template <>
inline XRPAmount&
get<XRPAmount>(EitherAmount& amt)
{
XRPL_ASSERT(amt.native, "xrpl::get<XRPAmount>(EitherAmount&) : is XRP");
return amt.xrp;
}
template <class T>
T const&
get(EitherAmount const& amt)
{
static_assert(sizeof(T) == -1, "Must used specialized function");
return T(0);
}
template <>
inline IOUAmount const&
get<IOUAmount>(EitherAmount const& amt)
{
XRPL_ASSERT(!amt.native, "xrpl::get<IOUAmount>(EitherAmount const&) : is not XRP");
return amt.iou;
}
template <>
inline XRPAmount const&
get<XRPAmount>(EitherAmount const& amt)
{
XRPL_ASSERT(amt.native, "xrpl::get<XRPAmount>(EitherAmount const&) : is XRP");
return amt.xrp;
}
inline AmountSpec
toAmountSpec(STAmount const& amt)
{
XRPL_ASSERT(
amt.mantissa() < std::numeric_limits<std::int64_t>::max(),
"xrpl::toAmountSpec(STAmount const&) : maximum mantissa");
bool const isNeg = amt.negative();
std::int64_t const sMant = isNeg ? -std::int64_t(amt.mantissa()) : amt.mantissa();
AmountSpec result;
result.native = isXRP(amt);
if (result.native)
{
result.xrp = XRPAmount(sMant);
}
else
{
result.iou = IOUAmount(sMant, amt.exponent());
result.issuer = amt.issue().account;
result.currency = amt.issue().currency;
}
return result;
}
inline EitherAmount
toEitherAmount(STAmount const& amt)
{
if (isXRP(amt))
return EitherAmount{amt.xrp()};
return EitherAmount{amt.iou()};
}
inline AmountSpec
toAmountSpec(EitherAmount const& ea, std::optional<Currency> const& c)
{
AmountSpec r;
r.native = (!c || isXRP(*c));
r.currency = c;
XRPL_ASSERT(
ea.native == r.native,
"xrpl::toAmountSpec(EitherAmount const&&, std::optional<Currency>) : "
"matching native");
if (r.native)
{
r.xrp = ea.xrp;
}
else
{
r.iou = ea.iou;
}
return r;
}
} // namespace xrpl

View File

@@ -0,0 +1,54 @@
#pragma once
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/XRPAmount.h>
namespace xrpl {
struct EitherAmount
{
std::variant<XRPAmount, IOUAmount, MPTAmount> amount;
explicit EitherAmount() = default;
template <StepAmount T>
explicit EitherAmount(T const& a) : amount(a)
{
}
template <StepAmount T>
[[nodiscard]] bool
holds() const
{
return std::holds_alternative<T>(amount);
}
template <StepAmount T>
[[nodiscard]] T const&
get() const
{
if (!holds<T>())
Throw<std::logic_error>("EitherAmount doesn't hold requested amount");
return std::get<T>(amount);
}
#ifndef NDEBUG
friend std::ostream&
operator<<(std::ostream& stream, EitherAmount const& amt)
{
std::visit([&]<StepAmount T>(T const& a) { stream << to_string(a); }, amt.amount);
return stream;
}
#endif
};
template <StepAmount T>
T const&
get(EitherAmount const& amt)
{
return amt.get<T>();
}
} // namespace xrpl

View File

@@ -208,14 +208,14 @@ struct FlowDebugInfo
auto writeXrpAmtList = [&write_list](
std::vector<EitherAmount> const& amts, char delim = ';') {
auto get_val = [](EitherAmount const& a) -> std::string {
return xrpl::to_string(a.xrp);
return xrpl::to_string(a.get<XRPAmount>());
};
write_list(amts, get_val, delim);
};
auto writeIouAmtList = [&write_list](
std::vector<EitherAmount> const& amts, char delim = ';') {
auto get_val = [](EitherAmount const& a) -> std::string {
return xrpl::to_string(a.iou);
return xrpl::to_string(a.get<IOUAmount>());
};
write_list(amts, get_val, delim);
};

View File

@@ -50,8 +50,7 @@ checkFreeze(
if (!sleAmm)
return tecINTERNAL; // LCOV_EXCL_LINE
if (isLPTokenFrozen(
view, src, (*sleAmm)[sfAsset].get<Issue>(), (*sleAmm)[sfAsset2].get<Issue>()))
if (isLPTokenFrozen(view, src, (*sleAmm)[sfAsset], (*sleAmm)[sfAsset2]))
{
return terNO_LINE;
}

View File

@@ -2,11 +2,11 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/QualityFunction.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/paths/detail/AmountSpec.h>
#include <xrpl/tx/paths/detail/EitherAmount.h>
#include <boost/container/flat_set.hpp>
@@ -44,6 +44,7 @@ issues(DebtDirection dir)
BookStepIX is an IOU/XRP offer book
BookStepXI is an XRP/IOU offer book
XRPEndpointStep is the source or destination account for XRP
MPTEndpointStep is the source or destination account for MPT
Amounts may be transformed through a step in either the forward or the
reverse direction. In the forward direction, the function `fwd` is used to
@@ -339,8 +340,8 @@ std::pair<TER, STPath>
normalizePath(
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
std::optional<Issue> const& sendMaxIssue,
Asset const& deliver,
std::optional<Asset> const& sendMaxAsset,
STPath const& path);
/**
@@ -355,7 +356,7 @@ normalizePath(
optimization. If, during direct offer crossing, the
quality of the tip of the book drops below this value,
then evaluating the strand can stop.
@param sendMaxIssue Optional asset to send.
@param sendMaxAsset Optional asset to send.
@param path Liquidity sources to use for this strand of the payment. The path
contains an ordered collection of the offer books to use and
accounts to ripple through.
@@ -372,9 +373,9 @@ toStrand(
ReadView const& sb,
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
Asset const& deliver,
std::optional<Quality> const& limitQuality,
std::optional<Issue> const& sendMaxIssue,
std::optional<Asset> const& sendMaxAsset,
STPath const& path,
bool ownerPaysTransferFee,
OfferCrossing offerCrossing,
@@ -413,9 +414,9 @@ toStrands(
ReadView const& sb,
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
Asset const& deliver,
std::optional<Quality> const& limitQuality,
std::optional<Issue> const& sendMax,
std::optional<Asset> const& sendMax,
STPathSet const& paths,
bool addDefaultPath,
bool ownerPaysTransferFee,
@@ -425,7 +426,7 @@ toStrands(
beast::Journal j);
/// @cond INTERNAL
template <class TIn, class TOut, class TDerived>
template <StepAmount TIn, StepAmount TOut, class TDerived>
struct StepImp : public Step
{
explicit StepImp() = default;
@@ -493,8 +494,16 @@ public:
// Check equal with tolerance
bool
checkNear(IOUAmount const& expected, IOUAmount const& actual);
bool
checkNear(XRPAmount const& expected, XRPAmount const& actual);
inline bool
checkNear(MPTAmount const& expected, MPTAmount const& actual)
{
return expected == actual;
}
inline bool
checkNear(XRPAmount const& expected, XRPAmount const& actual)
{
return expected == actual;
}
/// @endcond
/**
@@ -505,7 +514,7 @@ struct StrandContext
ReadView const& view; ///< Current ReadView
AccountID const strandSrc; ///< Strand source account
AccountID const strandDst; ///< Strand destination account
Issue const strandDeliver; ///< Issue strand delivers
Asset const strandDeliver; ///< Asset strand delivers
std::optional<Quality> const limitQuality; ///< Worst accepted quality
bool const isFirst; ///< true if Step is first in Strand
bool const isLast = false; ///< true if Step is last in Strand
@@ -522,11 +531,11 @@ struct StrandContext
at most twice: once as a src and once as a dst (hence the two element
array). The strandSrc and strandDst will only show up once each.
*/
std::array<boost::container::flat_set<Issue>, 2>& seenDirectIssues;
std::array<boost::container::flat_set<Asset>, 2>& seenDirectAssets;
/** A strand may not include an offer that output the same issue more
than once
*/
boost::container::flat_set<Issue>& seenBookOuts;
boost::container::flat_set<Asset>& seenBookOuts;
AMMContext& ammContext;
std::optional<uint256> domainID; // the domain the order book will use
beast::Journal const j;
@@ -539,15 +548,15 @@ struct StrandContext
// replicates the source or destination.
AccountID const& strandSrc_,
AccountID const& strandDst_,
Issue const& strandDeliver_,
Asset const& strandDeliver_,
std::optional<Quality> const& limitQuality_,
bool isLast_,
bool ownerPaysTransferFee_,
OfferCrossing offerCrossing_,
bool isDefaultPath_,
std::array<boost::container::flat_set<Issue>, 2>&
seenDirectIssues_, ///< For detecting currency loops
boost::container::flat_set<Issue>& seenBookOuts_, ///< For detecting book loops
std::array<boost::container::flat_set<Asset>, 2>&
seenDirectAssets_, ///< For detecting currency loops
boost::container::flat_set<Asset>& seenBookOuts_, ///< For detecting book loops
AMMContext& ammContext_,
std::optional<uint256> const& domainID,
beast::Journal j_); ///< Journal for logging
@@ -563,6 +572,13 @@ directStepEqual(
AccountID const& dst,
Currency const& currency);
bool
mptEndpointStepEqual(
Step const& step,
AccountID const& src,
AccountID const& dst,
MPTID const& mptid);
bool
xrpEndpointStepEqual(Step const& step, AccountID const& acc);
@@ -577,6 +593,13 @@ make_DirectStepI(
AccountID const& dst,
Currency const& c);
std::pair<TER, std::unique_ptr<Step>>
make_MPTEndpointStep(
StrandContext const& ctx,
AccountID const& src,
AccountID const& dst,
MPTID const& a);
std::pair<TER, std::unique_ptr<Step>>
make_BookStepII(StrandContext const& ctx, Issue const& in, Issue const& out);
@@ -589,9 +612,30 @@ make_BookStepXI(StrandContext const& ctx, Issue const& out);
std::pair<TER, std::unique_ptr<Step>>
make_XRPEndpointStep(StrandContext const& ctx, AccountID const& acc);
template <class InAmt, class OutAmt>
std::pair<TER, std::unique_ptr<Step>>
make_BookStepMM(StrandContext const& ctx, MPTIssue const& in, MPTIssue const& out);
std::pair<TER, std::unique_ptr<Step>>
make_BookStepMX(StrandContext const& ctx, MPTIssue const& in);
std::pair<TER, std::unique_ptr<Step>>
make_BookStepXM(StrandContext const& ctx, MPTIssue const& out);
std::pair<TER, std::unique_ptr<Step>>
make_BookStepMI(StrandContext const& ctx, MPTIssue const& in, Issue const& out);
std::pair<TER, std::unique_ptr<Step>>
make_BookStepIM(StrandContext const& ctx, Issue const& in, MPTIssue const& out);
template <StepAmount InAmt, StepAmount OutAmt>
bool
isDirectXrpToXrp(Strand const& strand);
isDirectXrpToXrp(Strand const& strand)
{
if constexpr (std::is_same_v<InAmt, XRPAmount> && std::is_same_v<OutAmt, XRPAmount>)
return strand.size() == 2;
else
return false;
}
/// @endcond
} // namespace xrpl

View File

@@ -246,7 +246,7 @@ flow(
EitherAmount stepIn(*strand[0]->cachedIn());
for (auto i = 0; i < s; ++i)
{
bool valid;
bool valid = false;
std::tie(valid, stepIn) = strand[i]->validFwd(checkSB, checkAfView, stepIn);
if (!valid)
{
@@ -379,8 +379,10 @@ limitOut(
return XRPAmount{*out};
else if constexpr (std::is_same_v<TOutAmt, IOUAmount>)
return IOUAmount{*out};
else if constexpr (std::is_same_v<TOutAmt, MPTAmount>)
return MPTAmount{*out};
else
return STAmount{remainingOut.issue(), out->mantissa(), out->exponent()};
return STAmount{remainingOut.asset(), out->mantissa(), out->exponent()};
}();
// A tiny difference could be due to the round off
if (withinRelativeDistance(out, remainingOut, Number(1, -9)))
@@ -534,7 +536,7 @@ public:
@return Actual amount in and out from the strands, errors, and payment
sandbox
*/
template <class TInAmt, class TOutAmt>
template <StepAmount TInAmt, StepAmount TOutAmt>
FlowResult<TInAmt, TOutAmt>
flow(
PaymentSandbox const& baseView,

View File

@@ -13,6 +13,9 @@ public:
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -13,6 +13,9 @@ public:
{
}
static bool
checkExtraFeatures(xrpl::PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -13,6 +13,9 @@ public:
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);

View File

@@ -224,7 +224,7 @@ private:
AccountID const& ammAccount,
STAmount const& amount,
STAmount const& amount2,
Issue const& lptIssue,
Asset const& lptIssue,
std::uint16_t tfee);
};

View File

@@ -98,7 +98,8 @@ public:
STAmount const& lpTokens,
STAmount const& lpTokensWithdraw,
std::uint16_t tfee,
FreezeHandling freezeHanding,
FreezeHandling freezeHandling,
AuthHandling authHandling,
WithdrawAll withdrawAll,
XRPAmount const& priorBalance,
beast::Journal const& journal);
@@ -132,6 +133,7 @@ public:
STAmount const& lpTokensWithdraw,
std::uint16_t tfee,
FreezeHandling freezeHandling,
AuthHandling authHandling,
WithdrawAll withdrawAll,
XRPAmount const& priorBalance,
beast::Journal const& journal);
@@ -141,8 +143,8 @@ public:
Sandbox& sb,
std::shared_ptr<SLE> const ammSle,
STAmount const& lpTokenBalance,
Issue const& issue1,
Issue const& issue2,
Asset const& asset1,
Asset const& asset2,
beast::Journal const& journal);
private:

View File

@@ -51,7 +51,7 @@ private:
ApplyFlags const flags,
AccountID const id,
beast::Journal const j,
Issue const& issue);
Asset const& asset);
// Use the payment flow code to perform offer crossing.
std::pair<TER, Amounts>

View File

@@ -43,13 +43,14 @@ AcceptedLedgerTx::AcceptedLedgerTx(
auto const amount = mTxn->getFieldAmount(sfTakerGets);
// If the offer create is not self funded then add the owner balance
if (account != amount.issue().account)
if (account != amount.getIssuer())
{
auto const ownerFunds = accountFunds(
*ledger,
account,
amount,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
beast::Journal{beast::Journal::getNullSink()});
mJson[jss::transaction][jss::owner_funds] = ownerFunds.getText();
}

View File

@@ -3,12 +3,14 @@
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/SField.h>
#include <algorithm>
namespace xrpl {
namespace detail {
auto
DeferredCredits::makeKey(AccountID const& a1, AccountID const& a2, Currency const& c) -> Key
DeferredCredits::makeKeyIOU(AccountID const& a1, AccountID const& a2, Currency const& c) -> KeyIOU
{
if (a1 < a2)
{
@@ -19,21 +21,23 @@ DeferredCredits::makeKey(AccountID const& a1, AccountID const& a2, Currency cons
}
void
DeferredCredits::credit(
DeferredCredits::creditIOU(
AccountID const& sender,
AccountID const& receiver,
STAmount const& amount,
STAmount const& preCreditSenderBalance)
{
XRPL_ASSERT(
sender != receiver, "xrpl::detail::DeferredCredits::credit : sender is not receiver");
XRPL_ASSERT(!amount.negative(), "xrpl::detail::DeferredCredits::credit : positive amount");
sender != receiver, "xrpl::detail::DeferredCredits::creditIOU : sender is not receiver");
XRPL_ASSERT(!amount.negative(), "xrpl::detail::DeferredCredits::creditIOU : positive amount");
XRPL_ASSERT(
amount.holds<Issue>(), "xrpl::detail::DeferredCredits::creditIOU : amount is for Issue");
auto const k = makeKey(sender, receiver, amount.getCurrency());
auto i = credits_.find(k);
if (i == credits_.end())
auto const k = makeKeyIOU(sender, receiver, amount.get<Issue>().currency);
auto i = creditsIOU_.find(k);
if (i == creditsIOU_.end())
{
Value v;
ValueIOU v;
if (sender < receiver)
{
@@ -48,7 +52,7 @@ DeferredCredits::credit(
v.lowAcctOrigBalance = -preCreditSenderBalance;
}
credits_[k] = v;
creditsIOU_[k] = v;
}
else
{
@@ -65,6 +69,93 @@ DeferredCredits::credit(
}
}
void
DeferredCredits::creditMPT(
AccountID const& sender,
AccountID const& receiver,
STAmount const& amount,
std::uint64_t preCreditBalanceHolder,
std::int64_t preCreditBalanceIssuer)
{
XRPL_ASSERT(
amount.holds<MPTIssue>(),
"xrpl::detail::DeferredCredits::creditMPT : amount is for MPTIssue");
XRPL_ASSERT(!amount.negative(), "xrpl::detail::DeferredCredits::creditMPT : positive amount");
XRPL_ASSERT(
sender != receiver, "xrpl::detail::DeferredCredits::creditMPT : sender is not receiver");
auto const mptAmtVal = amount.mpt().value();
auto const& issuer = amount.getIssuer();
auto const& mptIssue = amount.get<MPTIssue>();
auto const& mptID = mptIssue.getMptID();
bool const isSenderIssuer = sender == issuer;
auto i = creditsMPT_.find(mptID);
if (i == creditsMPT_.end())
{
IssuerValueMPT v;
if (isSenderIssuer)
{
v.credit = mptAmtVal;
v.holders[receiver].origBalance = preCreditBalanceHolder;
}
else
{
v.holders[sender].debit = mptAmtVal;
v.holders[sender].origBalance = preCreditBalanceHolder;
}
v.origBalance = preCreditBalanceIssuer;
creditsMPT_.emplace(mptID, std::move(v));
}
else
{
// only record the balance the first time, do not record it here
auto& v = i->second;
if (isSenderIssuer)
{
v.credit += mptAmtVal;
if (!v.holders.contains(receiver))
{
v.holders[receiver].origBalance = preCreditBalanceHolder;
}
}
else
{
if (!v.holders.contains(sender))
{
v.holders[sender].debit = mptAmtVal;
v.holders[sender].origBalance = preCreditBalanceHolder;
}
else
{
v.holders[sender].debit += mptAmtVal;
}
}
}
}
void
DeferredCredits::issuerSelfDebitMPT(
MPTIssue const& issue,
std::uint64_t amount,
std::int64_t origBalance)
{
auto const& mptID = issue.getMptID();
auto i = creditsMPT_.find(mptID);
if (i == creditsMPT_.end())
{
IssuerValueMPT v;
v.origBalance = origBalance;
v.selfDebit = amount;
creditsMPT_.emplace(mptID, std::move(v));
}
else
{
i->second.selfDebit += amount;
}
}
void
DeferredCredits::ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_t next)
{
@@ -88,16 +179,16 @@ DeferredCredits::ownerCount(AccountID const& id) const
// Get the adjustments for the balance between main and other.
auto
DeferredCredits::adjustments(
DeferredCredits::adjustmentsIOU(
AccountID const& main,
AccountID const& other,
Currency const& currency) const -> std::optional<Adjustment>
Currency const& currency) const -> std::optional<AdjustmentIOU>
{
std::optional<Adjustment> result;
std::optional<AdjustmentIOU> result;
Key const k = makeKey(main, other, currency);
auto i = credits_.find(k);
if (i == credits_.end())
KeyIOU const k = makeKeyIOU(main, other, currency);
auto i = creditsIOU_.find(k);
if (i == creditsIOU_.end())
return result;
auto const& v = i->second;
@@ -112,12 +203,21 @@ DeferredCredits::adjustments(
return result;
}
auto
DeferredCredits::adjustmentsMPT(xrpl::MPTID const& mptID) const -> std::optional<AdjustmentMPT>
{
auto i = creditsMPT_.find(mptID);
if (i == creditsMPT_.end())
return std::nullopt;
return i->second;
}
void
DeferredCredits::apply(DeferredCredits& to)
{
for (auto const& i : credits_)
for (auto const& i : creditsIOU_)
{
auto r = to.credits_.emplace(i);
auto r = to.creditsIOU_.emplace(i);
if (!r.second)
{
auto& toVal = r.first->second;
@@ -128,6 +228,30 @@ DeferredCredits::apply(DeferredCredits& to)
}
}
for (auto const& i : creditsMPT_)
{
auto r = to.creditsMPT_.emplace(i);
if (!r.second)
{
auto& toVal = r.first->second;
auto const& fromVal = i.second;
toVal.credit += fromVal.credit;
toVal.selfDebit += fromVal.selfDebit;
for (auto& [k, v] : fromVal.holders)
{
if (toVal.holders.find(k) == toVal.holders.end())
{
toVal.holders[k] = v;
}
else
{
toVal.holders[k].debit += v.debit;
}
}
// Do not update the orig balance, it's already correct
}
}
for (auto const& i : ownerCounts_)
{
auto r = to.ownerCounts_.emplace(i);
@@ -143,11 +267,13 @@ DeferredCredits::apply(DeferredCredits& to)
} // namespace detail
STAmount
PaymentSandbox::balanceHook(
PaymentSandbox::balanceHookIOU(
AccountID const& account,
AccountID const& issuer,
STAmount const& amount) const
{
XRPL_ASSERT(amount.holds<Issue>(), "balanceHookIOU: amount is for Issue");
/*
There are two algorithms here. The pre-switchover algorithm takes the
current amount and subtracts the recorded credits. The post-switchover
@@ -159,14 +285,14 @@ PaymentSandbox::balanceHook(
magnitudes, (B+C)-C may not equal B.
*/
auto const currency = amount.getCurrency();
auto const& currency = amount.get<Issue>().currency;
auto delta = amount.zeroed();
auto lastBal = amount;
auto minBal = amount;
for (auto curSB = this; curSB != nullptr; curSB = curSB->ps_)
{
if (auto adj = curSB->tab_.adjustments(account, issuer, currency))
if (auto adj = curSB->tab_.adjustmentsIOU(account, issuer, currency))
{
delta += adj->debits;
lastBal = adj->origBalance;
@@ -180,13 +306,13 @@ PaymentSandbox::balanceHook(
// to compute usable balance just slightly above what the ledger
// calculates (but always less than the actual balance).
auto adjustedAmt = std::min({amount, lastBal - delta, minBal});
adjustedAmt.setIssuer(amount.getIssuer());
adjustedAmt.get<Issue>().account = amount.getIssuer();
if (isXRP(issuer) && adjustedAmt < beast::zero)
{
// A calculated negative XRP balance is not an error case. Consider a
// payment snippet that credits a large XRP amount and then debits the
// same amount. The credit can't be used but we subtract the debit and
// same amount. The credit can't be used, but we subtract the debit and
// calculate a negative value. It's not an error case.
adjustedAmt.clear();
}
@@ -194,6 +320,64 @@ PaymentSandbox::balanceHook(
return adjustedAmt;
}
STAmount
PaymentSandbox::balanceHookMPT(AccountID const& account, MPTIssue const& issue, std::int64_t amount)
const
{
auto const& issuer = issue.getIssuer();
bool const accountIsHolder = account != issuer;
std::int64_t delta = 0;
std::int64_t lastBal = amount;
std::int64_t minBal = amount;
for (auto curSB = this; curSB != nullptr; curSB = curSB->ps_)
{
if (auto adj = curSB->tab_.adjustmentsMPT(issue))
{
if (accountIsHolder)
{
if (auto const i = adj->holders.find(account); i != adj->holders.end())
{
delta += i->second.debit;
lastBal = i->second.origBalance;
}
}
else
{
delta += adj->credit;
lastBal = adj->origBalance;
}
minBal = std::min(lastBal, minBal);
}
}
// The adjusted amount should never be larger than the balance.
auto const adjustedAmt = std::min({amount, lastBal - delta, minBal});
return adjustedAmt > 0 ? STAmount{issue, adjustedAmt} : STAmount{issue};
}
STAmount
PaymentSandbox::balanceHookSelfIssueMPT(xrpl::MPTIssue const& issue, std::int64_t amount) const
{
std::int64_t selfDebited = 0;
std::int64_t lastBal = amount;
for (auto curSB = this; curSB != nullptr; curSB = curSB->ps_)
{
if (auto adj = curSB->tab_.adjustmentsMPT(issue))
{
selfDebited += adj->selfDebit;
lastBal = adj->origBalance;
}
}
if (lastBal > selfDebited)
return STAmount{issue, lastBal - selfDebited};
return STAmount{issue};
}
std::uint32_t
PaymentSandbox::ownerCountHook(AccountID const& account, std::uint32_t count) const
{
@@ -207,13 +391,39 @@ PaymentSandbox::ownerCountHook(AccountID const& account, std::uint32_t count) co
}
void
PaymentSandbox::creditHook(
PaymentSandbox::creditHookIOU(
AccountID const& from,
AccountID const& to,
STAmount const& amount,
STAmount const& preCreditBalance)
{
tab_.credit(from, to, amount, preCreditBalance);
XRPL_ASSERT(amount.holds<Issue>(), "creditHookIOU: amount is for Issue");
tab_.creditIOU(from, to, amount, preCreditBalance);
}
void
PaymentSandbox::creditHookMPT(
AccountID const& from,
AccountID const& to,
STAmount const& amount,
std::uint64_t preCreditBalanceHolder,
std::int64_t preCreditBalanceIssuer)
{
XRPL_ASSERT(amount.holds<MPTIssue>(), "creditHookMPT: amount is for MPTIssue");
tab_.creditMPT(from, to, amount, preCreditBalanceHolder, preCreditBalanceIssuer);
}
void
PaymentSandbox::issuerSelfDebitHookMPT(
MPTIssue const& issue,
std::uint64_t amount,
std::int64_t origBalance)
{
XRPL_ASSERT(amount > 0, "PaymentSandbox::issuerSelfDebitHookMPT: amount must be > 0");
tab_.issuerSelfDebitMPT(issue, amount, origBalance);
}
void
@@ -346,7 +556,7 @@ PaymentSandbox::balanceChanges(ReadView const& view) const
}
// The following are now set, put them in the map
auto delta = newBalance - oldBalance;
auto const cur = newBalance.getCurrency();
auto const cur = newBalance.get<Issue>().currency;
result[std::make_tuple(lowID, highID, cur)] = delta;
auto r = result.emplace(std::make_tuple(lowID, lowID, cur), delta);
if (r.second)

View File

@@ -84,11 +84,10 @@ bool
isLPTokenFrozen(
ReadView const& view,
AccountID const& account,
Issue const& asset,
Issue const& asset2)
Asset const& asset,
Asset const& asset2)
{
return isFrozen(view, account, asset.currency, asset.account) ||
isFrozen(view, account, asset2.currency, asset2.account);
return isFrozen(view, account, asset) || isFrozen(view, account, asset2);
}
bool
@@ -334,22 +333,19 @@ withdrawToDestExceedsLimit(
if (from == to || to == issuer || isXRP(issuer))
return tesSUCCESS;
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) -> TER {
if constexpr (std::is_same_v<TIss, Issue>)
return amount.asset().visit(
[&](Issue const& issue) -> TER {
auto const& currency = issue.currency;
auto const owed = creditBalance(view, to, issuer, currency);
if (owed <= beast::zero)
{
auto const& currency = issue.currency;
auto const owed = creditBalance(view, to, issuer, currency);
if (owed <= beast::zero)
{
auto const limit = creditLimit(view, to, issuer, currency);
if (-owed >= limit || amount > (limit + owed))
return tecNO_LINE;
}
auto const limit = creditLimit(view, to, issuer, currency);
if (-owed >= limit || amount > (limit + owed))
return tecNO_LINE;
}
return tesSUCCESS;
},
amount.asset().value());
[](MPTIssue const&) -> TER { return tesSUCCESS; });
}
[[nodiscard]] TER

View File

@@ -1,9 +1,11 @@
#include <xrpl/ledger/helpers/AMMHelpers.h>
//
#include <xrpl/ledger/View.h>
namespace xrpl {
STAmount
ammLPTokens(STAmount const& asset1, STAmount const& asset2, Issue const& lptIssue)
ammLPTokens(STAmount const& asset1, STAmount const& asset2, Asset const& lptIssue)
{
// AMM invariant: sqrt(asset1 * asset2) >= LPTokensBalance
auto const rounding = isFeatureEnabled(fixAMMv1_3) ? Number::downward : Number::getround();
@@ -32,7 +34,7 @@ lpTokensOut(
if (!isFeatureEnabled(fixAMMv1_3))
{
auto const t = lptAMMBalance * (r - c) / (1 + c);
return toSTAmount(lptAMMBalance.issue(), t);
return toSTAmount(lptAMMBalance.asset(), t);
}
// minimize tokens out
@@ -68,7 +70,7 @@ ammAssetIn(
auto const c = d * d - f2 * f2;
if (!isFeatureEnabled(fixAMMv1_3))
{
return toSTAmount(asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c));
return toSTAmount(asset1Balance.asset(), asset1Balance * solveQuadraticEq(a, b, c));
}
// maximize deposit
@@ -93,7 +95,7 @@ lpTokensIn(
if (!isFeatureEnabled(fixAMMv1_3))
{
auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2;
return toSTAmount(lptAMMBalance.issue(), t);
return toSTAmount(lptAMMBalance.asset(), t);
}
// maximize tokens in
@@ -123,7 +125,7 @@ ammAssetOut(
if (!isFeatureEnabled(fixAMMv1_3))
{
auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1);
return toSTAmount(assetBalance.issue(), b);
return toSTAmount(assetBalance.asset(), b);
}
// minimize withdraw
@@ -183,8 +185,8 @@ adjustAmountsByLPTokens(
if (amount2)
{
Number const fr = lpTokensActual / lpTokens;
auto const amountActual = toSTAmount(amount.issue(), fr * amount);
auto const amount2Actual = toSTAmount(amount2->issue(), fr * *amount2);
auto const amountActual = toSTAmount(amount.asset(), fr * amount);
auto const amount2Actual = toSTAmount(amount2->asset(), fr * *amount2);
if (!ammRoundingEnabled)
{
return std::make_tuple(
@@ -253,7 +255,7 @@ multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm)
{
NumberRoundModeGuard const g(rm);
auto const t = amount * frac;
return toSTAmount(amount.issue(), t, rm);
return toSTAmount(amount.asset(), t, rm);
}
STAmount
@@ -265,13 +267,13 @@ getRoundedAsset(
IsDeposit isDeposit)
{
if (!rules.enabled(fixAMMv1_3))
return toSTAmount(balance.issue(), noRoundCb());
return toSTAmount(balance.asset(), noRoundCb());
auto const rm = detail::getAssetRounding(isDeposit);
if (isDeposit == IsDeposit::Yes)
return multiply(balance, productCb(), rm);
NumberRoundModeGuard const g(rm);
return toSTAmount(balance.issue(), productCb(), rm);
return toSTAmount(balance.asset(), productCb(), rm);
}
STAmount
@@ -282,7 +284,7 @@ getRoundedLPTokens(
IsDeposit isDeposit)
{
if (!rules.enabled(fixAMMv1_3))
return toSTAmount(balance.issue(), balance * frac);
return toSTAmount(balance.asset(), balance * frac);
auto const rm = detail::getLPTokenRounding(isDeposit);
auto const tokens = multiply(balance, frac, rm);
@@ -298,14 +300,14 @@ getRoundedLPTokens(
IsDeposit isDeposit)
{
if (!rules.enabled(fixAMMv1_3))
return toSTAmount(lptAMMBalance.issue(), noRoundCb());
return toSTAmount(lptAMMBalance.asset(), noRoundCb());
auto const tokens = [&] {
auto const rm = detail::getLPTokenRounding(isDeposit);
if (isDeposit == IsDeposit::Yes)
{
NumberRoundModeGuard const g(rm);
return toSTAmount(lptAMMBalance.issue(), productCb(), rm);
return toSTAmount(lptAMMBalance.asset(), productCb(), rm);
}
return multiply(lptAMMBalance, productCb(), rm);
}();
@@ -376,4 +378,535 @@ adjustFracByTokens(
return tokens / lptAMMBalance;
}
std::pair<STAmount, STAmount>
ammPoolHolds(
ReadView const& view,
AccountID const& ammAccountID,
Asset const& asset1,
Asset const& asset2,
FreezeHandling freezeHandling,
AuthHandling authHandling,
beast::Journal const j)
{
auto const assetInBalance =
accountHolds(view, ammAccountID, asset1, freezeHandling, authHandling, j);
auto const assetOutBalance =
accountHolds(view, ammAccountID, asset2, freezeHandling, authHandling, j);
return std::make_pair(assetInBalance, assetOutBalance);
}
Expected<std::tuple<STAmount, STAmount, STAmount>, TER>
ammHolds(
ReadView const& view,
SLE const& ammSle,
std::optional<Asset> const& optAsset1,
std::optional<Asset> const& optAsset2,
FreezeHandling freezeHandling,
AuthHandling authHandling,
beast::Journal const j)
{
auto const assets = [&]() -> std::optional<std::pair<Asset, Asset>> {
auto const asset1 = ammSle[sfAsset];
auto const asset2 = ammSle[sfAsset2];
if (optAsset1 && optAsset2)
{
if (invalidAMMAssetPair(
*optAsset1, *optAsset2, std::make_optional(std::make_pair(asset1, asset2))))
{
// This error can only be hit if the AMM is corrupted
// LCOV_EXCL_START
JLOG(j.debug()) << "ammHolds: Invalid optAsset1 or optAsset2 " << *optAsset1 << " "
<< *optAsset2;
return std::nullopt;
// LCOV_EXCL_STOP
}
return std::make_optional(std::make_pair(*optAsset1, *optAsset2));
}
auto const singleAsset = [&asset1, &asset2, &j](
Asset checkIssue,
char const* label) -> std::optional<std::pair<Asset, Asset>> {
if (checkIssue == asset1)
{
return std::make_optional(std::make_pair(asset1, asset2));
}
if (checkIssue == asset2)
{
return std::make_optional(std::make_pair(asset2, asset1));
}
// Unreachable unless AMM corrupted.
// LCOV_EXCL_START
JLOG(j.debug()) << "ammHolds: Invalid " << label << " " << checkIssue;
return std::nullopt;
// LCOV_EXCL_STOP
};
if (optAsset1)
{
return singleAsset(*optAsset1, "optAsset1");
}
if (optAsset2)
{
// Cannot have Amount2 without Amount.
return singleAsset(*optAsset2, "optAsset2"); // LCOV_EXCL_LINE
}
return std::make_optional(std::make_pair(asset1, asset2));
}();
if (!assets)
return Unexpected(tecAMM_INVALID_TOKENS);
auto const [amount1, amount2] = ammPoolHolds(
view,
ammSle.getAccountID(sfAccount),
assets->first,
assets->second,
freezeHandling,
authHandling,
j);
return std::make_tuple(amount1, amount2, ammSle[sfLPTokenBalance]);
}
STAmount
ammLPHolds(
ReadView const& view,
Asset const& asset1,
Asset const& asset2,
AccountID const& ammAccount,
AccountID const& lpAccount,
beast::Journal const j)
{
// This function looks similar to `accountHolds`. However, it only checks if
// a LPToken holder has enough balance. On the other hand, `accountHolds`
// checks if the underlying assets of LPToken are frozen with the
// fixFrozenLPTokenTransfer amendment
auto const currency = ammLPTCurrency(asset1, asset2);
STAmount amount;
auto const sle = view.read(keylet::line(lpAccount, ammAccount, currency));
if (!sle)
{
amount.clear(Issue{currency, ammAccount});
JLOG(j.trace()) << "ammLPHolds: no SLE "
<< " lpAccount=" << to_string(lpAccount)
<< " amount=" << amount.getFullText();
}
else if (isFrozen(view, lpAccount, currency, ammAccount))
{
amount.clear(Issue{currency, ammAccount});
JLOG(j.trace()) << "ammLPHolds: frozen currency "
<< " lpAccount=" << to_string(lpAccount)
<< " amount=" << amount.getFullText();
}
else
{
amount = sle->getFieldAmount(sfBalance);
if (lpAccount > ammAccount)
{
// Put balance in account terms.
amount.negate();
}
amount.get<Issue>().account = ammAccount;
JLOG(j.trace()) << "ammLPHolds:"
<< " lpAccount=" << to_string(lpAccount)
<< " amount=" << amount.getFullText();
}
return view.balanceHookIOU(lpAccount, ammAccount, amount);
}
STAmount
ammLPHolds(
ReadView const& view,
SLE const& ammSle,
AccountID const& lpAccount,
beast::Journal const j)
{
return ammLPHolds(view, ammSle[sfAsset], ammSle[sfAsset2], ammSle[sfAccount], lpAccount, j);
}
std::uint16_t
getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account)
{
using namespace std::chrono;
XRPL_ASSERT(
!view.rules().enabled(fixInnerObjTemplate) || ammSle.isFieldPresent(sfAuctionSlot),
"xrpl::getTradingFee : auction present");
if (ammSle.isFieldPresent(sfAuctionSlot))
{
auto const& auctionSlot = safe_downcast<STObject const&>(ammSle.peekAtField(sfAuctionSlot));
// Not expired
if (auto const expiration = auctionSlot[~sfExpiration];
duration_cast<seconds>(view.header().parentCloseTime.time_since_epoch()).count() <
expiration)
{
if (auctionSlot[~sfAccount] == account)
return auctionSlot[sfDiscountedFee];
if (auctionSlot.isFieldPresent(sfAuthAccounts))
{
for (auto const& acct : auctionSlot.getFieldArray(sfAuthAccounts))
{
if (acct[~sfAccount] == account)
return auctionSlot[sfDiscountedFee];
}
}
}
}
return ammSle[sfTradingFee];
}
STAmount
ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const& asset)
{
// Get the actual AMM balance without factoring in the balance hook
return asset.visit(
[&](MPTIssue const& issue) {
if (auto const sle = view.read(keylet::mptoken(issue, ammAccountID));
sle && !isFrozen(view, ammAccountID, issue))
return STAmount{issue, (*sle)[sfMPTAmount]};
return STAmount{asset};
},
[&](Issue const& issue) {
if (isXRP(issue))
{
if (auto const sle = view.read(keylet::account(ammAccountID)))
return (*sle)[sfBalance];
}
else if (
auto const sle =
view.read(keylet::line(ammAccountID, issue.account, issue.currency));
sle && !isFrozen(view, ammAccountID, issue.currency, issue.account))
{
STAmount amount = (*sle)[sfBalance];
if (ammAccountID > issue.account)
amount.negate();
amount.get<Issue>().account = issue.account;
return amount;
}
return STAmount{asset};
});
}
static TER
deleteAMMTrustLines(
Sandbox& sb,
AccountID const& ammAccountID,
std::uint16_t maxTrustlinesToDelete,
beast::Journal j)
{
return cleanupOnAccountDelete(
sb,
keylet::ownerDir(ammAccountID),
[&](LedgerEntryType nodeType,
uint256 const&,
std::shared_ptr<SLE>& sleItem) -> std::pair<TER, SkipEntry> {
// Skip AMM and MPToken
if (nodeType == ltAMM || nodeType == ltMPTOKEN)
return {tesSUCCESS, SkipEntry::Yes};
if (nodeType == ltRIPPLE_STATE)
{
// Trustlines must have zero balance
if (sleItem->getFieldAmount(sfBalance) != beast::zero)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMObjects: deleting trustline with "
"non-zero balance.";
return {tecINTERNAL, SkipEntry::No};
// LCOV_EXCL_STOP
}
return {deleteAMMTrustLine(sb, sleItem, ammAccountID, j), SkipEntry::No};
}
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMObjects: deleting non-trustline or non-MPT " << nodeType;
return {tecINTERNAL, SkipEntry::No};
// LCOV_EXCL_STOP
},
j,
maxTrustlinesToDelete);
}
static TER
deleteAMMMPTokens(Sandbox& sb, AccountID const& ammAccountID, beast::Journal j)
{
return cleanupOnAccountDelete(
sb,
keylet::ownerDir(ammAccountID),
[&](LedgerEntryType nodeType,
uint256 const&,
std::shared_ptr<SLE>& sleItem) -> std::pair<TER, SkipEntry> {
// Skip AMM
if (nodeType == ltAMM)
return {tesSUCCESS, SkipEntry::Yes};
if (nodeType == ltMPTOKEN)
{
// MPT must have zero balance
if (sleItem->getFieldU64(sfMPTAmount) != 0 ||
(*sleItem)[~sfLockedAmount].value_or(0) != 0)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMObjects: deleting MPT with "
"non-zero balance.";
return {tecINTERNAL, SkipEntry::No};
// LCOV_EXCL_STOP
}
return {deleteAMMMPToken(sb, sleItem, ammAccountID, j), SkipEntry::No};
}
if (nodeType == ltRIPPLE_STATE)
{
// Trustlines should have been deleted
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMObjects: trustlines should have been deleted";
return {tecINTERNAL, SkipEntry::No};
// LCOV_EXCL_STOP
}
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMObjects: deleting non-trustline or non-MPT " << nodeType;
return {tecINTERNAL, SkipEntry::No};
// LCOV_EXCL_STOP
},
j,
3); // At most two MPToken plus AMM object
}
TER
deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Journal j)
{
auto ammSle = sb.peek(keylet::amm(asset, asset2));
if (!ammSle)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: AMM object does not exist " << asset << " " << asset2;
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const ammAccountID = (*ammSle)[sfAccount];
auto sleAMMRoot = sb.peek(keylet::account(ammAccountID));
if (!sleAMMRoot)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: AMM account does not exist "
<< to_string(ammAccountID);
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (auto const ter = deleteAMMTrustLines(sb, ammAccountID, maxDeletableAMMTrustLines, j);
!isTesSuccess(ter))
return ter;
// Delete AMM's MPTokens only if all trustlines are deleted. If trustlines
// are not deleted then AMM can be re-created with Deposit and
// AMM's MPToken(s) must exist.
if (auto const ter = deleteAMMMPTokens(sb, ammAccountID, j); !isTesSuccess(ter))
return ter;
auto const ownerDirKeylet = keylet::ownerDir(ammAccountID);
if (!sb.dirRemove(ownerDirKeylet, (*ammSle)[sfOwnerNode], ammSle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: failed to remove dir link";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (sb.exists(ownerDirKeylet) && !sb.emptyDirDelete(ownerDirKeylet))
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: cannot delete root dir node of "
<< toBase58(ammAccountID);
return tecINTERNAL;
// LCOV_EXCL_STOP
}
sb.erase(ammSle);
sb.erase(sleAMMRoot);
return tesSUCCESS;
}
void
initializeFeeAuctionVote(
ApplyView& view,
std::shared_ptr<SLE>& ammSle,
AccountID const& account,
Asset const& lptAsset,
std::uint16_t tfee)
{
auto const& rules = view.rules();
// AMM creator gets the voting slot.
STArray voteSlots;
STObject voteEntry = STObject::makeInnerObject(sfVoteEntry);
if (tfee != 0)
voteEntry.setFieldU16(sfTradingFee, tfee);
voteEntry.setFieldU32(sfVoteWeight, VOTE_WEIGHT_SCALE_FACTOR);
voteEntry.setAccountID(sfAccount, account);
voteSlots.push_back(voteEntry);
ammSle->setFieldArray(sfVoteSlots, voteSlots);
// AMM creator gets the auction slot for free.
// AuctionSlot is created on AMMCreate and updated on AMMDeposit
// when AMM is in an empty state
if (rules.enabled(fixInnerObjTemplate) && !ammSle->isFieldPresent(sfAuctionSlot))
{
STObject auctionSlot = STObject::makeInnerObject(sfAuctionSlot);
ammSle->set(std::move(auctionSlot));
}
STObject& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
auctionSlot.setAccountID(sfAccount, account);
// current + sec in 24h
auto const expiration = std::chrono::duration_cast<std::chrono::seconds>(
view.header().parentCloseTime.time_since_epoch())
.count() +
TOTAL_TIME_SLOT_SECS;
auctionSlot.setFieldU32(sfExpiration, expiration);
auctionSlot.setFieldAmount(sfPrice, STAmount{lptAsset, 0});
// Set the fee
if (tfee != 0)
{
ammSle->setFieldU16(sfTradingFee, tfee);
}
else if (ammSle->isFieldPresent(sfTradingFee))
{
ammSle->makeFieldAbsent(sfTradingFee); // LCOV_EXCL_LINE
}
if (auto const dfee = tfee / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION)
{
auctionSlot.setFieldU16(sfDiscountedFee, dfee);
}
else if (auctionSlot.isFieldPresent(sfDiscountedFee))
{
auctionSlot.makeFieldAbsent(sfDiscountedFee); // LCOV_EXCL_LINE
}
}
Expected<bool, TER>
isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID const& lpAccount)
{
// Liquidity Provider (LP) must have one LPToken trustline
std::uint8_t nLPTokenTrustLines = 0;
// AMM account has at most two IOU (pool tokens, not LPToken) trustlines.
// One or both trustlines could be to the LP if LP is the issuer,
// or a different account if LP is not an issuer. For instance,
// if AMM has two tokens USD and EUR and LP is not the issuer of the tokens
// then the trustlines are between AMM account and the issuer.
// There is one LPToken trustline for each LP. Only remaining LP has
// exactly one LPToken trustlines and at most two IOU trustline for each
// pool token. One or both tokens could be MPT.
std::uint8_t nIOUTrustLines = 0;
// There are at most two MPT objects, one for each side of the pool.
std::uint8_t nMPT = 0;
// There is only one AMM object
bool hasAMM = false;
// AMM LP has at most three trustlines, at most two MPTs, and only one
// AMM object must exist. If there are more than four objects then
// it's either an error or there are more than one LP. Ten pages should
// be sufficient to include four objects.
std::uint8_t limit = 10;
auto const root = keylet::ownerDir(ammIssue.account);
auto currentIndex = root;
// Iterate over AMM owner directory objects.
while (limit-- >= 1)
{
auto const ownerDir = view.read(currentIndex);
if (!ownerDir)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
for (auto const& key : ownerDir->getFieldV256(sfIndexes))
{
auto const sle = view.read(keylet::child(key));
if (!sle)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
auto const entryType = sle->getFieldU16(sfLedgerEntryType);
// Only one AMM object
if (entryType == ltAMM)
{
if (hasAMM)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
hasAMM = true;
continue;
}
if (entryType == ltMPTOKEN)
{
++nMPT;
continue;
}
if (entryType != ltRIPPLE_STATE)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
auto const lowLimit = sle->getFieldAmount(sfLowLimit);
auto const highLimit = sle->getFieldAmount(sfHighLimit);
auto const isLPTrustline =
lowLimit.getIssuer() == lpAccount || highLimit.getIssuer() == lpAccount;
auto const isLPTokenTrustline =
lowLimit.asset() == ammIssue || highLimit.asset() == ammIssue;
// Liquidity Provider trustline
if (isLPTrustline)
{
// LPToken trustline
if (isLPTokenTrustline)
{
// LP has exactly one LPToken trustline
if (++nLPTokenTrustLines > 1)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
}
// AMM account has at most two IOU trustlines
else if (++nIOUTrustLines > 2)
{
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
}
}
// Another Liquidity Provider LPToken trustline
else if (isLPTokenTrustline)
{
return false;
}
// AMM account has at most two IOU trustlines
else if (++nIOUTrustLines > 2)
{
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
}
}
auto const uNodeNext = ownerDir->getFieldU64(sfIndexNext);
if (uNodeNext == 0)
{
if (nLPTokenTrustLines != 1 || (nIOUTrustLines == 0 && nMPT == 0) ||
(nIOUTrustLines > 2 || nMPT > 2) || (nIOUTrustLines + nMPT) > 2)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
return true;
}
currentIndex = keylet::page(root, uNodeNext);
}
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
}
Expected<bool, TER>
verifyAndAdjustLPTokenBalance(
Sandbox& sb,
STAmount const& lpTokens,
std::shared_ptr<SLE>& ammSle,
AccountID const& account)
{
auto const res = isOnlyLiquidityProvider(sb, lpTokens.get<Issue>(), account);
if (!res.has_value())
{
return Unexpected<TER>(res.error());
}
if (res.value())
{
if (withinRelativeDistance(
lpTokens, ammSle->getFieldAmount(sfLPTokenBalance), Number{1, -3}))
{
ammSle->setFieldAmount(sfLPTokenBalance, lpTokens);
sb.update(ammSle);
}
else
{
return Unexpected<TER>(tecAMM_INVALID_TOKENS);
}
}
return true;
}
} // namespace xrpl

View File

@@ -1,462 +0,0 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/AMMUtils.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/STObject.h>
namespace xrpl {
std::pair<STAmount, STAmount>
ammPoolHolds(
ReadView const& view,
AccountID const& ammAccountID,
Issue const& issue1,
Issue const& issue2,
FreezeHandling freezeHandling,
beast::Journal const j)
{
auto const assetInBalance = accountHolds(view, ammAccountID, issue1, freezeHandling, j);
auto const assetOutBalance = accountHolds(view, ammAccountID, issue2, freezeHandling, j);
return std::make_pair(assetInBalance, assetOutBalance);
}
Expected<std::tuple<STAmount, STAmount, STAmount>, TER>
ammHolds(
ReadView const& view,
SLE const& ammSle,
std::optional<Issue> const& optIssue1,
std::optional<Issue> const& optIssue2,
FreezeHandling freezeHandling,
beast::Journal const j)
{
auto const issues = [&]() -> std::optional<std::pair<Issue, Issue>> {
auto const issue1 = ammSle[sfAsset].get<Issue>();
auto const issue2 = ammSle[sfAsset2].get<Issue>();
if (optIssue1 && optIssue2)
{
if (invalidAMMAssetPair(
*optIssue1, *optIssue2, std::make_optional(std::make_pair(issue1, issue2))))
{
// This error can only be hit if the AMM is corrupted
// LCOV_EXCL_START
JLOG(j.debug()) << "ammHolds: Invalid optIssue1 or optIssue2 " << *optIssue1 << " "
<< *optIssue2;
return std::nullopt;
// LCOV_EXCL_STOP
}
return std::make_optional(std::make_pair(*optIssue1, *optIssue2));
}
auto const singleIssue = [&issue1, &issue2, &j](
Issue checkIssue,
char const* label) -> std::optional<std::pair<Issue, Issue>> {
if (checkIssue == issue1)
{
return std::make_optional(std::make_pair(issue1, issue2));
}
if (checkIssue == issue2)
{
return std::make_optional(std::make_pair(issue2, issue1));
}
// Unreachable unless AMM corrupted.
// LCOV_EXCL_START
JLOG(j.debug()) << "ammHolds: Invalid " << label << " " << checkIssue;
return std::nullopt;
// LCOV_EXCL_STOP
};
if (optIssue1)
{
return singleIssue(*optIssue1, "optIssue1");
}
if (optIssue2)
{
// Cannot have Amount2 without Amount.
return singleIssue(*optIssue2, "optIssue2"); // LCOV_EXCL_LINE
}
return std::make_optional(std::make_pair(issue1, issue2));
}();
if (!issues)
return Unexpected(tecAMM_INVALID_TOKENS);
auto const [asset1, asset2] = ammPoolHolds(
view, ammSle.getAccountID(sfAccount), issues->first, issues->second, freezeHandling, j);
return std::make_tuple(asset1, asset2, ammSle[sfLPTokenBalance]);
}
STAmount
ammLPHolds(
ReadView const& view,
Currency const& cur1,
Currency const& cur2,
AccountID const& ammAccount,
AccountID const& lpAccount,
beast::Journal const j)
{
// This function looks similar to `accountHolds`. However, it only checks if
// a LPToken holder has enough balance. On the other hand, `accountHolds`
// checks if the underlying assets of LPToken are frozen with the
// fixFrozenLPTokenTransfer amendment
auto const currency = ammLPTCurrency(cur1, cur2);
STAmount amount;
auto const sle = view.read(keylet::line(lpAccount, ammAccount, currency));
if (!sle)
{
amount.clear(Issue{currency, ammAccount});
JLOG(j.trace()) << "ammLPHolds: no SLE "
<< " lpAccount=" << to_string(lpAccount)
<< " amount=" << amount.getFullText();
}
else if (isFrozen(view, lpAccount, currency, ammAccount))
{
amount.clear(Issue{currency, ammAccount});
JLOG(j.trace()) << "ammLPHolds: frozen currency "
<< " lpAccount=" << to_string(lpAccount)
<< " amount=" << amount.getFullText();
}
else
{
amount = sle->getFieldAmount(sfBalance);
if (lpAccount > ammAccount)
{
// Put balance in account terms.
amount.negate();
}
amount.setIssuer(ammAccount);
JLOG(j.trace()) << "ammLPHolds:"
<< " lpAccount=" << to_string(lpAccount)
<< " amount=" << amount.getFullText();
}
return view.balanceHook(lpAccount, ammAccount, amount);
}
STAmount
ammLPHolds(
ReadView const& view,
SLE const& ammSle,
AccountID const& lpAccount,
beast::Journal const j)
{
return ammLPHolds(
view,
ammSle[sfAsset].get<Issue>().currency,
ammSle[sfAsset2].get<Issue>().currency,
ammSle[sfAccount],
lpAccount,
j);
}
std::uint16_t
getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account)
{
using namespace std::chrono;
XRPL_ASSERT(
!view.rules().enabled(fixInnerObjTemplate) || ammSle.isFieldPresent(sfAuctionSlot),
"xrpl::getTradingFee : auction present");
if (ammSle.isFieldPresent(sfAuctionSlot))
{
auto const& auctionSlot = safe_downcast<STObject const&>(ammSle.peekAtField(sfAuctionSlot));
// Not expired
if (auto const expiration = auctionSlot[~sfExpiration];
duration_cast<seconds>(view.header().parentCloseTime.time_since_epoch()).count() <
expiration)
{
if (auctionSlot[~sfAccount] == account)
return auctionSlot[sfDiscountedFee];
if (auctionSlot.isFieldPresent(sfAuthAccounts))
{
for (auto const& acct : auctionSlot.getFieldArray(sfAuthAccounts))
{
if (acct[~sfAccount] == account)
return auctionSlot[sfDiscountedFee];
}
}
}
}
return ammSle[sfTradingFee];
}
STAmount
ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Issue const& issue)
{
if (isXRP(issue))
{
if (auto const sle = view.read(keylet::account(ammAccountID)))
return (*sle)[sfBalance];
}
else if (
auto const sle = view.read(keylet::line(ammAccountID, issue.account, issue.currency));
sle && !isFrozen(view, ammAccountID, issue.currency, issue.account))
{
auto amount = (*sle)[sfBalance];
if (ammAccountID > issue.account)
amount.negate();
amount.setIssuer(issue.account);
return amount;
}
return STAmount{issue};
}
static TER
deleteAMMTrustLines(
Sandbox& sb,
AccountID const& ammAccountID,
std::uint16_t maxTrustlinesToDelete,
beast::Journal j)
{
return cleanupOnAccountDelete(
sb,
keylet::ownerDir(ammAccountID),
[&](LedgerEntryType nodeType,
uint256 const&,
std::shared_ptr<SLE>& sleItem) -> std::pair<TER, SkipEntry> {
// Skip AMM
if (nodeType == LedgerEntryType::ltAMM)
return {tesSUCCESS, SkipEntry::Yes};
// Should only have the trustlines
if (nodeType != LedgerEntryType::ltRIPPLE_STATE)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMTrustLines: deleting non-trustline " << nodeType;
return {tecINTERNAL, SkipEntry::No};
// LCOV_EXCL_STOP
}
// Trustlines must have zero balance
if (sleItem->getFieldAmount(sfBalance) != beast::zero)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMTrustLines: deleting trustline with "
"non-zero balance.";
return {tecINTERNAL, SkipEntry::No};
// LCOV_EXCL_STOP
}
return {deleteAMMTrustLine(sb, sleItem, ammAccountID, j), SkipEntry::No};
},
j,
maxTrustlinesToDelete);
}
TER
deleteAMMAccount(Sandbox& sb, Issue const& asset, Issue const& asset2, beast::Journal j)
{
auto ammSle = sb.peek(keylet::amm(asset, asset2));
if (!ammSle)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: AMM object does not exist " << asset << " " << asset2;
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const ammAccountID = (*ammSle)[sfAccount];
auto sleAMMRoot = sb.peek(keylet::account(ammAccountID));
if (!sleAMMRoot)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: AMM account does not exist "
<< to_string(ammAccountID);
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (auto const ter = deleteAMMTrustLines(sb, ammAccountID, maxDeletableAMMTrustLines, j);
!isTesSuccess(ter))
return ter;
auto const ownerDirKeylet = keylet::ownerDir(ammAccountID);
if (!sb.dirRemove(ownerDirKeylet, (*ammSle)[sfOwnerNode], ammSle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: failed to remove dir link";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (sb.exists(ownerDirKeylet) && !sb.emptyDirDelete(ownerDirKeylet))
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: cannot delete root dir node of "
<< toBase58(ammAccountID);
return tecINTERNAL;
// LCOV_EXCL_STOP
}
sb.erase(ammSle);
sb.erase(sleAMMRoot);
return tesSUCCESS;
}
void
initializeFeeAuctionVote(
ApplyView& view,
std::shared_ptr<SLE>& ammSle,
AccountID const& account,
Issue const& lptIssue,
std::uint16_t tfee)
{
auto const& rules = view.rules();
// AMM creator gets the voting slot.
STArray voteSlots;
STObject voteEntry = STObject::makeInnerObject(sfVoteEntry);
if (tfee != 0)
voteEntry.setFieldU16(sfTradingFee, tfee);
voteEntry.setFieldU32(sfVoteWeight, VOTE_WEIGHT_SCALE_FACTOR);
voteEntry.setAccountID(sfAccount, account);
voteSlots.push_back(voteEntry);
ammSle->setFieldArray(sfVoteSlots, voteSlots);
// AMM creator gets the auction slot for free.
// AuctionSlot is created on AMMCreate and updated on AMMDeposit
// when AMM is in an empty state
if (rules.enabled(fixInnerObjTemplate) && !ammSle->isFieldPresent(sfAuctionSlot))
{
STObject auctionSlot = STObject::makeInnerObject(sfAuctionSlot);
ammSle->set(std::move(auctionSlot));
}
STObject& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
auctionSlot.setAccountID(sfAccount, account);
// current + sec in 24h
auto const expiration = std::chrono::duration_cast<std::chrono::seconds>(
view.header().parentCloseTime.time_since_epoch())
.count() +
TOTAL_TIME_SLOT_SECS;
auctionSlot.setFieldU32(sfExpiration, expiration);
auctionSlot.setFieldAmount(sfPrice, STAmount{lptIssue, 0});
// Set the fee
if (tfee != 0)
{
ammSle->setFieldU16(sfTradingFee, tfee);
}
else if (ammSle->isFieldPresent(sfTradingFee))
{
ammSle->makeFieldAbsent(sfTradingFee); // LCOV_EXCL_LINE
}
if (auto const dfee = tfee / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION)
{
auctionSlot.setFieldU16(sfDiscountedFee, dfee);
}
else if (auctionSlot.isFieldPresent(sfDiscountedFee))
{
auctionSlot.makeFieldAbsent(sfDiscountedFee); // LCOV_EXCL_LINE
}
}
Expected<bool, TER>
isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID const& lpAccount)
{
// Liquidity Provider (LP) must have one LPToken trustline
std::uint8_t nLPTokenTrustLines = 0;
// There are at most two IOU trustlines. One or both could be to the LP
// if LP is the issuer, or a different account if LP is not an issuer.
// For instance, if AMM has two tokens USD and EUR and LP is not the issuer
// of the tokens then the trustlines are between AMM account and the issuer.
std::uint8_t nIOUTrustLines = 0;
// There is only one AMM object
bool hasAMM = false;
// AMM LP has at most three trustlines and only one AMM object must exist.
// If there are more than five objects then it's either an error or
// there are more than one LP. Ten pages should be sufficient to include
// five objects.
std::uint8_t limit = 10;
auto const root = keylet::ownerDir(ammIssue.account);
auto currentIndex = root;
// Iterate over AMM owner directory objects.
while (limit-- >= 1)
{
auto const ownerDir = view.read(currentIndex);
if (!ownerDir)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
for (auto const& key : ownerDir->getFieldV256(sfIndexes))
{
auto const sle = view.read(keylet::child(key));
if (!sle)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
// Only one AMM object
if (sle->getFieldU16(sfLedgerEntryType) == ltAMM)
{
if (hasAMM)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
hasAMM = true;
continue;
}
if (sle->getFieldU16(sfLedgerEntryType) != ltRIPPLE_STATE)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
auto const lowLimit = sle->getFieldAmount(sfLowLimit);
auto const highLimit = sle->getFieldAmount(sfHighLimit);
auto const isLPTrustline =
lowLimit.getIssuer() == lpAccount || highLimit.getIssuer() == lpAccount;
auto const isLPTokenTrustline =
lowLimit.issue() == ammIssue || highLimit.issue() == ammIssue;
// Liquidity Provider trustline
if (isLPTrustline)
{
// LPToken trustline
if (isLPTokenTrustline)
{
if (++nLPTokenTrustLines > 1)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
}
else if (++nIOUTrustLines > 2)
{
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
}
}
// Another Liquidity Provider LPToken trustline
else if (isLPTokenTrustline)
{
return false;
}
else if (++nIOUTrustLines > 2)
{
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
}
}
auto const uNodeNext = ownerDir->getFieldU64(sfIndexNext);
if (uNodeNext == 0)
{
if (nLPTokenTrustLines != 1 || nIOUTrustLines == 0 || nIOUTrustLines > 2)
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
return true;
}
currentIndex = keylet::page(root, uNodeNext);
}
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
}
Expected<bool, TER>
verifyAndAdjustLPTokenBalance(
Sandbox& sb,
STAmount const& lpTokens,
std::shared_ptr<SLE>& ammSle,
AccountID const& account)
{
auto const res = isOnlyLiquidityProvider(sb, lpTokens.issue(), account);
if (!res.has_value())
{
return Unexpected<TER>(res.error());
}
if (res.value())
{
if (withinRelativeDistance(
lpTokens, ammSle->getFieldAmount(sfLPTokenBalance), Number{1, -3}))
{
ammSle->setFieldAmount(sfLPTokenBalance, lpTokens);
sb.update(ammSle);
}
else
{
return Unexpected<TER>(tecAMM_INVALID_TOKENS);
}
}
return true;
}
} // namespace xrpl

View File

@@ -80,7 +80,7 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj,
auto const fullBalance = sle->getFieldAmount(sfBalance);
auto const balance = view.balanceHook(id, xrpAccount(), fullBalance);
auto const balance = view.balanceHookIOU(id, xrpAccount(), fullBalance);
STAmount const amount = (balance < reserve) ? STAmount{0} : balance - reserve;

View File

@@ -306,18 +306,11 @@ requireAuth(
return tefINTERNAL; // LCOV_EXCL_LINE
auto const asset = sleVault->at(sfAsset);
if (auto const err = std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
{
return requireAuth(view, issue, account, authType);
}
else
{
return requireAuth(view, issue, account, authType, depth + 1);
}
},
asset.value());
if (auto const err = asset.visit(
[&](Issue const& issue) { return requireAuth(view, issue, account, authType); },
[&](MPTIssue const& issue) {
return requireAuth(view, issue, account, authType, depth + 1);
});
!isTesSuccess(err))
return err;
}
@@ -487,6 +480,37 @@ canTransfer(
return tesSUCCESS;
}
TER
canTrade(ReadView const& view, Asset const& asset)
{
return asset.visit(
[&](Issue const&) -> TER { return tesSUCCESS; },
[&](MPTIssue const& mptIssue) -> TER {
auto const sleIssuance = view.read(keylet::mptIssuance(mptIssue.getMptID()));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanTrade))
return tecNO_PERMISSION;
return tesSUCCESS;
});
}
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)
{
@@ -770,4 +794,88 @@ createMPToken(
return tesSUCCESS;
}
TER
checkCreateMPT(
xrpl::ApplyView& view,
xrpl::MPTIssue const& mptIssue,
xrpl::AccountID const& holder,
beast::Journal j)
{
if (mptIssue.getIssuer() == holder)
return tesSUCCESS;
auto const mptIssuanceID = keylet::mptIssuance(mptIssue.getMptID());
auto const mptokenID = keylet::mptoken(mptIssuanceID.key, holder);
if (!view.exists(mptokenID))
{
if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, 0);
!isTesSuccess(err))
{
return err;
}
auto const sleAcct = view.peek(keylet::account(holder));
if (!sleAcct)
{
return tecINTERNAL;
}
adjustOwnerCount(view, sleAcct, 1, j);
}
return tesSUCCESS;
}
std::int64_t
maxMPTAmount(SLE const& sleIssuance)
{
return sleIssuance[~sfMaximumAmount].value_or(maxMPTokenAmount);
}
std::int64_t
availableMPTAmount(SLE const& sleIssuance)
{
auto const max = maxMPTAmount(sleIssuance);
auto const outstanding = sleIssuance[sfOutstandingAmount];
return max - outstanding;
}
std::int64_t
availableMPTAmount(ReadView const& view, MPTID const& mptID)
{
auto const sle = view.read(keylet::mptIssuance(mptID));
if (!sle)
Throw<std::runtime_error>(transHuman(tecINTERNAL));
return availableMPTAmount(*sle);
}
bool
isMPTOverflow(
std::int64_t sendAmount,
std::uint64_t outstandingAmount,
std::int64_t maximumAmount,
AllowMPTOverflow allowOverflow)
{
std::uint64_t const limit = (allowOverflow == AllowMPTOverflow::Yes)
? std::numeric_limits<std::uint64_t>::max()
: maximumAmount;
return (sendAmount > maximumAmount || outstandingAmount > (limit - sendAmount));
}
STAmount
issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue)
{
STAmount amount{issue};
auto const sle = view.read(keylet::mptIssuance(issue));
if (!sle)
return amount;
auto const available = availableMPTAmount(*sle);
return view.balanceHookSelfIssueMPT(issue, available);
}
void
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount)
{
auto const available = availableMPTAmount(view, issue);
view.issuerSelfDebitHookMPT(issue, amount, available);
}
} // namespace xrpl

View File

@@ -855,15 +855,15 @@ tokenOfferCreatePreclaim(
if (view.rules().enabled(featureNFTokenMintOffer))
{
if (nftIssuer != amount.getIssuer() &&
!view.read(keylet::line(nftIssuer, amount.issue())))
!view.read(keylet::line(nftIssuer, amount.get<Issue>())))
return tecNO_LINE;
}
else if (!view.exists(keylet::line(nftIssuer, amount.issue())))
else if (!view.exists(keylet::line(nftIssuer, amount.get<Issue>())))
{
return tecNO_LINE;
}
if (isFrozen(view, nftIssuer, amount.getCurrency(), amount.getIssuer()))
if (isFrozen(view, nftIssuer, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
}
@@ -876,7 +876,7 @@ tokenOfferCreatePreclaim(
return tefNFTOKEN_IS_NOT_TRANSFERABLE;
}
if (isFrozen(view, acctID, amount.getCurrency(), amount.getIssuer()))
if (isFrozen(view, acctID, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
// If this is an offer to buy the token, the account must have the

View File

@@ -33,11 +33,14 @@ creditLimit(
if (sleRippleState)
{
result = sleRippleState->getFieldAmount(account < issuer ? sfLowLimit : sfHighLimit);
result.setIssuer(account);
result.get<Issue>().account = account;
}
XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditLimit : result issuer match");
XRPL_ASSERT(result.getCurrency() == currency, "xrpl::creditLimit : result currency match");
XRPL_ASSERT(
result.get<Issue>().currency == currency,
"xrpl::creditLimit : result currency "
"match");
return result;
}
@@ -63,11 +66,14 @@ creditBalance(
result = sleRippleState->getFieldAmount(sfBalance);
if (account < issuer)
result.negate();
result.setIssuer(account);
result.get<Issue>().account = account;
}
XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditBalance : result issuer match");
XRPL_ASSERT(result.getCurrency() == currency, "xrpl::creditBalance : result currency match");
XRPL_ASSERT(
result.get<Issue>().currency == currency,
"xrpl::creditBalance : result currency "
"match");
return result;
}
@@ -222,7 +228,7 @@ trustCreate(
sleRippleState->setFieldAmount(bSetHigh ? sfHighLimit : sfLowLimit, saLimit);
sleRippleState->setFieldAmount(
bSetHigh ? sfLowLimit : sfHighLimit,
STAmount(Issue{saBalance.getCurrency(), bSetDst ? uSrcAccountID : uDstAccountID}));
STAmount(Issue{saBalance.get<Issue>().currency, bSetDst ? uSrcAccountID : uDstAccountID}));
if (uQualityIn != 0u)
sleRippleState->setFieldU32(bSetHigh ? sfHighQualityIn : sfLowQualityIn, uQualityIn);
@@ -261,7 +267,7 @@ trustCreate(
// ONLY: Create ripple balance.
sleRippleState->setFieldAmount(sfBalance, bSetHigh ? -saBalance : saBalance);
view.creditHook(uSrcAccountID, uDstAccountID, saBalance, saBalance.zeroed());
view.creditHookIOU(uSrcAccountID, uDstAccountID, saBalance, saBalance.zeroed());
return tesSUCCESS;
}
@@ -367,7 +373,7 @@ issueIOU(
"xrpl::issueIOU : neither account nor issuer is XRP");
// Consistency check
XRPL_ASSERT(issue == amount.issue(), "xrpl::issueIOU : matching issue");
XRPL_ASSERT(issue == amount.get<Issue>(), "xrpl::issueIOU : matching issue");
// Can't send to self!
XRPL_ASSERT(issue.account != account, "xrpl::issueIOU : not issuer account");
@@ -392,7 +398,7 @@ issueIOU(
auto const must_delete = updateTrustLine(
view, state, bSenderHigh, issue.account, start_balance, final_balance, j);
view.creditHook(issue.account, account, amount, start_balance);
view.creditHookIOU(issue.account, account, amount, start_balance);
if (bSenderHigh)
final_balance.negate();
@@ -422,7 +428,7 @@ issueIOU(
STAmount const limit(Issue{issue.currency, account});
STAmount final_balance = amount;
final_balance.setIssuer(noAccount());
final_balance.get<Issue>().account = noAccount();
auto const receiverAccount = view.peek(keylet::account(account));
if (!receiverAccount)
@@ -461,7 +467,7 @@ redeemIOU(
"xrpl::redeemIOU : neither account nor issuer is XRP");
// Consistency check
XRPL_ASSERT(issue == amount.issue(), "xrpl::redeemIOU : matching issue");
XRPL_ASSERT(issue == amount.get<Issue>(), "xrpl::redeemIOU : matching issue");
// Can't send to self!
XRPL_ASSERT(issue.account != account, "xrpl::redeemIOU : not issuer account");
@@ -484,7 +490,7 @@ redeemIOU(
auto const must_delete =
updateTrustLine(view, state, bSenderHigh, account, start_balance, final_balance, j);
view.creditHook(account, issue.account, amount, start_balance);
view.creditHookIOU(account, issue.account, amount, start_balance);
if (bSenderHigh)
final_balance.negate();
@@ -757,4 +763,20 @@ deleteAMMTrustLine(
return tesSUCCESS;
}
TER
deleteAMMMPToken(
ApplyView& view,
std::shared_ptr<SLE> sleMpt,
AccountID const& ammAccountID,
beast::Journal j)
{
if (!view.dirRemove(
keylet::ownerDir(ammAccountID), (*sleMpt)[sfOwnerNode], sleMpt->key(), false))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
view.erase(sleMpt);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -23,8 +23,8 @@ bool
isLPTokenFrozen(
ReadView const& view,
AccountID const& account,
Issue const& asset,
Issue const& asset2);
Asset const& asset,
Asset const& asset2);
//------------------------------------------------------------------------------
//
@@ -35,18 +35,17 @@ isLPTokenFrozen(
bool
isGlobalFrozen(ReadView const& view, Asset const& asset)
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
{
return isGlobalFrozen(view, issue.getIssuer());
}
else
{
return isGlobalFrozen(view, issue);
}
},
asset.value());
return asset.visit(
[&](Issue const& issue) { return isGlobalFrozen(view, issue.getIssuer()); },
[&](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
@@ -56,6 +55,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)
{
@@ -103,18 +110,9 @@ isAnyFrozen(
Asset const& asset,
int depth)
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
{
return isAnyFrozen(view, accounts, issue);
}
else
{
return isAnyFrozen(view, accounts, issue, depth);
}
},
asset.value());
return asset.visit(
[&](Issue const& issue) { return isAnyFrozen(view, accounts, issue); },
[&](MPTIssue const& issue) { return isAnyFrozen(view, accounts, issue, depth); });
}
bool
@@ -190,11 +188,7 @@ getLineIfUsable(
auto const sleAmm = view.read(keylet::amm((*sleIssuer)[sfAMMID]));
if (!sleAmm ||
isLPTokenFrozen(
view,
account,
(*sleAmm)[sfAsset].get<Issue>(),
(*sleAmm)[sfAsset2].get<Issue>()))
isLPTokenFrozen(view, account, (*sleAmm)[sfAsset], (*sleAmm)[sfAsset2]))
{
return nullptr;
}
@@ -230,7 +224,7 @@ getTrustLineBalance(
{
amount += sle->getFieldAmount(oppositeField);
}
amount.setIssuer(issuer);
amount.get<Issue>().account = issuer;
}
else
{
@@ -240,7 +234,7 @@ getTrustLineBalance(
JLOG(j.trace()) << "getTrustLineBalance:" << " account=" << to_string(account)
<< " amount=" << amount.getFullText();
return view.balanceHook(account, issuer, amount);
return view.balanceHookIOU(account, issuer, amount);
}
STAmount
@@ -298,6 +292,9 @@ accountHolds(
SpendableHandling includeFullBalance)
{
bool const returnSpendable = (includeFullBalance == shFULL_BALANCE);
STAmount amount{mptIssue};
auto const& issuer = mptIssue.getIssuer();
bool const mptokensV2 = view.rules().enabled(featureMPTokensV2);
if (returnSpendable && account == mptIssue.getIssuer())
{
@@ -307,16 +304,14 @@ accountHolds(
if (!issuance)
{
return STAmount{mptIssue};
return amount;
}
return STAmount{
mptIssue,
issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) -
issuance->at(sfOutstandingAmount)};
auto const available = availableMPTAmount(*issuance);
if (!mptokensV2)
return STAmount{mptIssue, available};
return view.balanceHookMPT(issuer, mptIssue, available);
}
STAmount amount;
auto const sleMpt = view.read(keylet::mptoken(mptIssue.getMptID(), account));
if (!sleMpt)
@@ -352,6 +347,8 @@ accountHolds(
}
}
if (view.rules().enabled(featureMPTokensV2))
return view.balanceHookMPT(account, mptIssue, amount.mpt().value());
return amount;
}
@@ -365,19 +362,14 @@ accountHolds(
beast::Journal j,
SpendableHandling includeFullBalance)
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& value) {
if constexpr (std::is_same_v<TIss, Issue>)
{
return accountHolds(view, account, value, zeroIfFrozen, j, includeFullBalance);
}
else if constexpr (std::is_same_v<TIss, MPTIssue>)
{
return accountHolds(
view, account, value, zeroIfFrozen, zeroIfUnauthorized, j, includeFullBalance);
}
return asset.visit(
[&](Issue const& issue) {
return accountHolds(view, account, issue, zeroIfFrozen, j, includeFullBalance);
},
asset.value());
[&](MPTIssue const& issue) {
return accountHolds(
view, account, issue, zeroIfFrozen, zeroIfUnauthorized, j, includeFullBalance);
});
}
STAmount
@@ -388,28 +380,38 @@ accountFunds(
FreezeHandling freezeHandling,
beast::Journal j)
{
XRPL_ASSERT(saDefault.holds<Issue>(), "xrpl::accountFunds: saDefault holds Issue");
if (!saDefault.native() && saDefault.getIssuer() == id)
return saDefault;
return accountHolds(
view, id, saDefault.getCurrency(), saDefault.getIssuer(), freezeHandling, j);
view, id, saDefault.get<Issue>().currency, saDefault.getIssuer(), freezeHandling, j);
}
STAmount
accountFunds(
ReadView const& view,
AccountID const& id,
STAmount const& saDefault,
FreezeHandling freezeHandling,
AuthHandling authHandling,
beast::Journal j)
{
return saDefault.asset().visit(
[&](Issue const&) { return accountFunds(view, id, saDefault, freezeHandling, j); },
[&](MPTIssue const&) {
return accountHolds(
view, id, saDefault.asset(), freezeHandling, authHandling, j, shFULL_BALANCE);
});
}
Rate
transferRate(ReadView const& view, STAmount const& amount)
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
{
return transferRate(view, issue.getIssuer());
}
else
{
return transferRate(view, issue.getMptID());
}
},
amount.asset().value());
return amount.asset().visit(
[&](Issue const& issue) { return transferRate(view, issue.getIssuer()); },
[&](MPTIssue const& issue) { return transferRate(view, issue.getMptID()); });
}
//------------------------------------------------------------------------------
@@ -522,7 +524,7 @@ directSendNoFeeIOU(
beast::Journal j)
{
AccountID const& issuer = saAmount.getIssuer();
Currency const& currency = saAmount.getCurrency();
Currency const& currency = saAmount.get<Issue>().currency;
// Make sure issuer is involved.
XRPL_ASSERT(
@@ -551,7 +553,7 @@ directSendNoFeeIOU(
if (bSenderHigh)
saBalance.negate(); // Put balance in sender terms.
view.creditHook(uSenderID, uReceiverID, saAmount, saBalance);
view.creditHookIOU(uSenderID, uReceiverID, saAmount, saBalance);
STAmount const saBefore = saBalance;
@@ -622,7 +624,7 @@ directSendNoFeeIOU(
STAmount const saReceiverLimit(Issue{currency, uReceiverID});
STAmount saBalance{saAmount};
saBalance.setIssuer(noAccount());
saBalance.get<Issue>().account = noAccount();
JLOG(j.debug()) << "directSendNoFeeIOU: "
"create line: "
@@ -859,7 +861,7 @@ accountSendIOU(
else
{
auto const sndBal = sender->getFieldAmount(sfBalance);
view.creditHook(uSenderID, xrpAccount(), saAmount, sndBal);
view.creditHookIOU(uSenderID, xrpAccount(), saAmount, sndBal);
// Decrement XRP balance.
sender->setFieldAmount(sfBalance, sndBal - saAmount);
@@ -872,7 +874,7 @@ accountSendIOU(
// Increment XRP balance.
auto const rcvBal = receiver->getFieldAmount(sfBalance);
receiver->setFieldAmount(sfBalance, rcvBal + saAmount);
view.creditHook(xrpAccount(), uReceiverID, saAmount, -rcvBal);
view.creditHookIOU(xrpAccount(), uReceiverID, saAmount, -rcvBal);
view.update(receiver);
}
@@ -975,7 +977,7 @@ accountSendMultiIOU(
// Increment XRP balance.
auto const rcvBal = receiver->getFieldAmount(sfBalance);
receiver->setFieldAmount(sfBalance, rcvBal + amount);
view.creditHook(xrpAccount(), receiverID, amount, -rcvBal);
view.creditHookIOU(xrpAccount(), receiverID, amount, -rcvBal);
view.update(receiver);
@@ -1003,7 +1005,7 @@ accountSendMultiIOU(
return TER{tecFAILED_PROCESSING};
}
auto const sndBal = sender->getFieldAmount(sfBalance);
view.creditHook(senderID, xrpAccount(), takeFromSender, sndBal);
view.creditHookIOU(senderID, xrpAccount(), takeFromSender, sndBal);
// Decrement XRP balance.
sender->setFieldAmount(sfBalance, sndBal - takeFromSender);
@@ -1037,9 +1039,20 @@ directSendNoFeeMPT(
auto sleIssuance = view.peek(mptID);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
auto const maxAmount = maxMPTAmount(*sleIssuance);
auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount);
auto const available = availableMPTAmount(*sleIssuance);
auto const amt = saAmount.mpt().value();
if (uSenderID == issuer)
{
(*sleIssuance)[sfOutstandingAmount] += saAmount.mpt().value();
if (view.rules().enabled(featureMPTokensV2))
{
if (isMPTOverflow(amt, outstanding, maxAmount, AllowMPTOverflow::Yes))
return tecPATH_DRY;
}
(*sleIssuance)[sfOutstandingAmount] += amt;
view.update(sleIssuance);
}
else
@@ -1047,11 +1060,11 @@ directSendNoFeeMPT(
auto const mptokenID = keylet::mptoken(mptID.key, uSenderID);
if (auto sle = view.peek(mptokenID))
{
auto const amt = sle->getFieldU64(sfMPTAmount);
auto const pay = saAmount.mpt().value();
if (amt < pay)
auto const senderBalance = sle->getFieldU64(sfMPTAmount);
if (senderBalance < amt)
return tecINSUFFICIENT_FUNDS;
(*sle)[sfMPTAmount] = amt - pay;
view.creditHookMPT(uSenderID, uReceiverID, saAmount, (*sle)[sfMPTAmount], available);
(*sle)[sfMPTAmount] = senderBalance - amt;
view.update(sle);
}
else
@@ -1062,11 +1075,9 @@ directSendNoFeeMPT(
if (uReceiverID == issuer)
{
auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount);
auto const redeem = saAmount.mpt().value();
if (outstanding >= redeem)
if (outstanding >= amt)
{
sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem);
sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - amt);
view.update(sleIssuance);
}
else
@@ -1079,7 +1090,8 @@ directSendNoFeeMPT(
auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID);
if (auto sle = view.peek(mptokenID))
{
(*sle)[sfMPTAmount] += saAmount.mpt().value();
view.creditHookMPT(uSenderID, uReceiverID, saAmount, (*sle)[sfMPTAmount], available);
(*sle)[sfMPTAmount] += amt;
view.update(sle);
}
else
@@ -1099,7 +1111,8 @@ directSendNoLimitMPT(
STAmount const& saAmount,
STAmount& saActual,
beast::Journal j,
WaiveTransferFee waiveFee)
WaiveTransferFee waiveFee,
AllowMPTOverflow allowOverflow)
{
XRPL_ASSERT(uSenderID != uReceiverID, "xrpl::directSendNoLimitMPT : sender is not receiver");
@@ -1117,9 +1130,13 @@ directSendNoLimitMPT(
if (uSenderID == issuer)
{
auto const sendAmount = saAmount.mpt().value();
auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
if (sendAmount > maximumAmount ||
sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount)
auto const maxAmount = maxMPTAmount(*sle);
auto const outstanding = sle->getFieldU64(sfOutstandingAmount);
auto const mptokensV2 = view.rules().enabled(featureMPTokensV2);
allowOverflow = (allowOverflow == AllowMPTOverflow::Yes && mptokensV2)
? AllowMPTOverflow::Yes
: AllowMPTOverflow::No;
if (isMPTOverflow(sendAmount, outstanding, maxAmount, allowOverflow))
return tecPATH_DRY;
}
@@ -1233,7 +1250,8 @@ directSendNoLimitMultiMPT(
}
// Direct send: redeeming MPTs and/or sending own MPTs.
if (auto const ter = directSendNoFeeMPT(view, senderID, receiverID, amount, j))
if (auto const ter = directSendNoFeeMPT(view, senderID, receiverID, amount, j);
!isTesSuccess(ter))
return ter;
actual += amount;
// Do not add amount to takeFromSender, because directSendNoFeeMPT took it.
@@ -1252,13 +1270,15 @@ directSendNoLimitMultiMPT(
<< to_string(receiverID) << " : deliver=" << amount.getFullText()
<< " cost=" << actualSend.getFullText();
if (auto const terResult = directSendNoFeeMPT(view, issuer, receiverID, amount, j))
return terResult;
if (auto const ter = directSendNoFeeMPT(view, issuer, receiverID, amount, j);
!isTesSuccess(ter))
return ter;
}
if (senderID != issuer && takeFromSender)
{
if (TER const terResult = directSendNoFeeMPT(view, senderID, issuer, takeFromSender, j))
return terResult;
if (auto const ter = directSendNoFeeMPT(view, senderID, issuer, takeFromSender, j);
!isTesSuccess(ter))
return ter;
}
return tesSUCCESS;
@@ -1271,7 +1291,8 @@ accountSendMPT(
AccountID const& uReceiverID,
STAmount const& saAmount,
beast::Journal j,
WaiveTransferFee waiveFee)
WaiveTransferFee waiveFee,
AllowMPTOverflow allowOverflow)
{
XRPL_ASSERT(
saAmount >= beast::zero && saAmount.holds<MPTIssue>(),
@@ -1285,7 +1306,8 @@ accountSendMPT(
STAmount saActual{saAmount.asset()};
return directSendNoLimitMPT(view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee);
return directSendNoLimitMPT(
view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee, allowOverflow);
}
static TER
@@ -1317,19 +1339,14 @@ directSendNoFee(
bool bCheckIssuer,
beast::Journal j)
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
{
return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, j);
}
else
{
XRPL_ASSERT(!bCheckIssuer, "xrpl::directSendNoFee : not checking issuer");
return directSendNoFeeMPT(view, uSenderID, uReceiverID, saAmount, j);
}
return saAmount.asset().visit(
[&](Issue const&) {
return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, j);
},
saAmount.asset().value());
[&](MPTIssue const&) {
XRPL_ASSERT(!bCheckIssuer, "xrpl::directSendNoFee : not checking issuer");
return directSendNoFeeMPT(view, uSenderID, uReceiverID, saAmount, j);
});
}
TER
@@ -1339,20 +1356,17 @@ accountSend(
AccountID const& uReceiverID,
STAmount const& saAmount,
beast::Journal j,
WaiveTransferFee waiveFee)
WaiveTransferFee waiveFee,
AllowMPTOverflow allowOverflow)
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
{
return accountSendIOU(view, uSenderID, uReceiverID, saAmount, j, waiveFee);
}
else
{
return accountSendMPT(view, uSenderID, uReceiverID, saAmount, j, waiveFee);
}
return saAmount.asset().visit(
[&](Issue const&) {
return accountSendIOU(view, uSenderID, uReceiverID, saAmount, j, waiveFee);
},
saAmount.asset().value());
[&](MPTIssue const&) {
return accountSendMPT(
view, uSenderID, uReceiverID, saAmount, j, waiveFee, allowOverflow);
});
}
TER
@@ -1366,18 +1380,13 @@ accountSendMulti(
{
XRPL_ASSERT_PARTS(
receivers.size() > 1, "xrpl::accountSendMulti", "multiple recipients provided");
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
{
return accountSendMultiIOU(view, senderID, issue, receivers, j, waiveFee);
}
else
{
return accountSendMultiMPT(view, senderID, issue, receivers, j, waiveFee);
}
return asset.visit(
[&](Issue const& issue) {
return accountSendMultiIOU(view, senderID, issue, receivers, j, waiveFee);
},
asset.value());
[&](MPTIssue const& issue) {
return accountSendMultiMPT(view, senderID, issue, receivers, j, waiveFee);
});
}
TER

View File

@@ -21,12 +21,23 @@
namespace xrpl {
Currency
ammLPTCurrency(Currency const& cur1, Currency const& cur2)
ammLPTCurrency(Asset const& asset1, Asset const& asset2)
{
// AMM LPToken is 0x03 plus 19 bytes of the hash
std::int32_t constexpr AMMCurrencyCode = 0x03;
auto const [minC, maxC] = std::minmax(cur1, cur2);
auto const hash = sha512Half(minC, maxC);
auto const& [minA, maxA] = std::minmax(asset1, asset2);
uint256 const hash = std::visit(
[](auto&& issue1, auto&& issue2) {
auto fromIss = []<ValidIssueType T>(T const& issue) {
if constexpr (std::is_same_v<T, Issue>)
return issue.currency;
if constexpr (std::is_same_v<T, MPTIssue>)
return issue.getMptID();
};
return sha512Half(fromIss(issue1), fromIss(issue2));
},
minA.value(),
maxA.value());
Currency currency;
*currency.begin() = AMMCurrencyCode;
std::copy(hash.begin(), hash.begin() + currency.size() - 1, currency.begin() + 1);
@@ -34,34 +45,45 @@ ammLPTCurrency(Currency const& cur1, Currency const& cur2)
}
Issue
ammLPTIssue(Currency const& cur1, Currency const& cur2, AccountID const& ammAccountID)
ammLPTIssue(Asset const& asset1, Asset const& asset2, AccountID const& ammAccountID)
{
return Issue(ammLPTCurrency(cur1, cur2), ammAccountID);
return Issue(ammLPTCurrency(asset1, asset2), ammAccountID);
}
NotTEC
invalidAMMAsset(Issue const& issue, std::optional<std::pair<Issue, Issue>> const& pair)
invalidAMMAsset(Asset const& asset, std::optional<std::pair<Asset, Asset>> const& pair)
{
if (badCurrency() == issue.currency)
return temBAD_CURRENCY;
if (isXRP(issue) && issue.account.isNonZero())
return temBAD_ISSUER;
if (pair && issue != pair->first && issue != pair->second)
auto const err = asset.visit(
[](MPTIssue const& issue) -> std::optional<NotTEC> {
if (issue.getIssuer() == beast::zero)
return temBAD_MPT;
return std::nullopt;
},
[](Issue const& issue) -> std::optional<NotTEC> {
if (badCurrency() == issue.currency)
return temBAD_CURRENCY;
if (isXRP(issue) && issue.getIssuer().isNonZero())
return temBAD_ISSUER;
return std::nullopt;
});
if (err)
return *err;
if (pair && asset != pair->first && asset != pair->second)
return temBAD_AMM_TOKENS;
return tesSUCCESS;
}
NotTEC
invalidAMMAssetPair(
Issue const& issue1,
Issue const& issue2,
std::optional<std::pair<Issue, Issue>> const& pair)
Asset const& asset1,
Asset const& asset2,
std::optional<std::pair<Asset, Asset>> const& pair)
{
if (issue1 == issue2)
if (asset1 == asset2)
return temBAD_AMM_TOKENS;
if (auto const res = invalidAMMAsset(issue1, pair))
if (auto const res = invalidAMMAsset(asset1, pair))
return res;
if (auto const res = invalidAMMAsset(issue2, pair))
if (auto const res = invalidAMMAsset(asset2, pair))
return res;
return tesSUCCESS;
}
@@ -69,10 +91,10 @@ invalidAMMAssetPair(
NotTEC
invalidAMMAmount(
STAmount const& amount,
std::optional<std::pair<Issue, Issue>> const& pair,
std::optional<std::pair<Asset, Asset>> const& pair,
bool validZero)
{
if (auto const res = invalidAMMAsset(amount.issue(), pair))
if (auto const res = invalidAMMAsset(amount.asset(), pair))
return res;
if (amount < beast::zero || (!validZero && amount == beast::zero))
return temBAD_AMOUNT;

View File

@@ -62,4 +62,11 @@ assetFromJson(Json::Value const& v)
return mptIssueFromJson(v);
}
std::ostream&
operator<<(std::ostream& os, Asset const& x)
{
std::visit([&]<ValidIssueType TIss>(TIss const& issue) { os << issue; }, x.value());
return os;
}
} // namespace xrpl

View File

@@ -99,19 +99,36 @@ getBookBase(Book const& book)
{
XRPL_ASSERT(isConsistent(book), "xrpl::getBookBase : input is consistent");
auto const index = book.domain ? indexHash(
LedgerNameSpace::BOOK_DIR,
book.in.currency,
book.out.currency,
book.in.account,
book.out.account,
*(book.domain))
: indexHash(
LedgerNameSpace::BOOK_DIR,
book.in.currency,
book.out.currency,
book.in.account,
book.out.account);
auto getIndexHash = [&book]<typename... Args>(Args... args) {
if (book.domain)
return indexHash(std::forward<Args>(args)..., *book.domain);
return indexHash(std::forward<Args>(args)...);
};
auto const index = std::visit(
[&]<ValidIssueType TIn, ValidIssueType TOut>(TIn const& in, TOut const& out) {
if constexpr (std::is_same_v<TIn, Issue> && std::is_same_v<TOut, Issue>)
{
return getIndexHash(
LedgerNameSpace::BOOK_DIR, in.currency, out.currency, in.account, out.account);
}
else if constexpr (std::is_same_v<TIn, Issue> && std::is_same_v<TOut, MPTIssue>)
{
return getIndexHash(
LedgerNameSpace::BOOK_DIR, in.currency, out.getMptID(), in.account);
}
else if constexpr (std::is_same_v<TIn, MPTIssue> && std::is_same_v<TOut, Issue>)
{
return getIndexHash(
LedgerNameSpace::BOOK_DIR, in.getMptID(), out.currency, out.account);
}
else
{
return getIndexHash(LedgerNameSpace::BOOK_DIR, in.getMptID(), out.getMptID());
}
},
book.in.value(),
book.out.value());
// Return with quality 0.
auto k = keylet::quality({ltDIR_NODE, index}, 0);
@@ -401,11 +418,37 @@ nft_sells(uint256 const& id) noexcept
}
Keylet
amm(Asset const& issue1, Asset const& issue2) noexcept
amm(Asset const& asset1, Asset const& asset2) noexcept
{
auto const& [minI, maxI] = std::minmax(issue1.get<Issue>(), issue2.get<Issue>());
return amm(
indexHash(LedgerNameSpace::AMM, minI.account, minI.currency, maxI.account, maxI.currency));
auto const& [minA, maxA] = std::minmax(asset1, asset2);
return std::visit(
[]<ValidIssueType TIss1, ValidIssueType TIss2>(TIss1 const& issue1, TIss2 const& issue2) {
if constexpr (std::is_same_v<TIss1, Issue> && std::is_same_v<TIss2, Issue>)
{
return amm(indexHash(
LedgerNameSpace::AMM,
issue1.account,
issue1.currency,
issue2.account,
issue2.currency));
}
else if constexpr (std::is_same_v<TIss1, Issue> && std::is_same_v<TIss2, MPTIssue>)
{
return amm(indexHash(
LedgerNameSpace::AMM, issue1.account, issue1.currency, issue2.getMptID()));
}
else if constexpr (std::is_same_v<TIss1, MPTIssue> && std::is_same_v<TIss2, Issue>)
{
return amm(indexHash(
LedgerNameSpace::AMM, issue1.getMptID(), issue2.account, issue2.currency));
}
else if constexpr (std::is_same_v<TIss1, MPTIssue> && std::is_same_v<TIss2, MPTIssue>)
{
return amm(indexHash(LedgerNameSpace::AMM, issue1.getMptID(), issue2.getMptID()));
}
},
minA.value(),
maxA.value());
}
Keylet

View File

@@ -2,9 +2,8 @@
#include <xrpl/basics/contract.h>
#include <xrpl/json/json_errors.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
@@ -17,6 +16,11 @@ MPTIssue::MPTIssue(MPTID const& issuanceID) : mptID_(issuanceID)
{
}
MPTIssue::MPTIssue(std::uint32_t sequence, AccountID const& account)
: MPTIssue(xrpl::makeMptID(sequence, account))
{
}
AccountID const&
MPTIssue::getIssuer() const
{
@@ -86,4 +90,11 @@ mptIssueFromJson(Json::Value const& v)
return MPTIssue{id};
}
std::ostream&
operator<<(std::ostream& os, MPTIssue const& x)
{
os << to_string(x);
return os;
}
} // namespace xrpl

View File

@@ -0,0 +1,19 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PathAsset.h>
namespace xrpl {
std::string
to_string(PathAsset const& asset)
{
return std::visit([&](auto const& issue) { return to_string(issue); }, asset.value());
}
std::ostream&
operator<<(std::ostream& os, PathAsset const& x)
{
os << to_string(x);
return os;
}
} // namespace xrpl

View File

@@ -67,6 +67,11 @@ Permission::Permission()
#pragma pop_macro("PERMISSION")
};
XRPL_ASSERT(
txFeatureMap_.size() == delegableTx_.size(),
"xrpl::Permission : txFeatureMap_ and delegableTx_ must have same "
"size");
for ([[maybe_unused]] auto const& permission : granularPermissionMap_)
{
XRPL_ASSERT(

View File

@@ -90,11 +90,23 @@ getMPTValue(STAmount const& amount)
static bool
areComparable(STAmount const& v1, STAmount const& v2)
{
if (v1.holds<Issue>() && v2.holds<Issue>())
return v1.native() == v2.native() && v1.get<Issue>().currency == v2.get<Issue>().currency;
if (v1.holds<MPTIssue>() && v2.holds<MPTIssue>())
return v1.get<MPTIssue>() == v2.get<MPTIssue>();
return false;
return std::visit(
[&]<ValidIssueType TIss1, ValidIssueType TIss2>(TIss1 const& issue1, TIss2 const& issue2) {
if constexpr (is_issue_v<TIss1> && is_issue_v<TIss2>)
{
return v1.native() == v2.native() && issue1.currency == issue2.currency;
}
else if constexpr (is_mptissue_v<TIss1> && is_mptissue_v<TIss2>)
{
return issue1 == issue2;
}
else
{
return false;
}
},
v1.asset().value(),
v2.asset().value());
}
static_assert(INITIAL_XRP.drops() == STAmount::cMaxNativeN);
@@ -513,26 +525,35 @@ canAdd(STAmount const& a, STAmount const& b)
}
// IOU case (precision check)
if (a.holds<Issue>() && b.holds<Issue>())
{
static STAmount const one{IOUAmount{1, 0}, noIssue()};
static STAmount const maxLoss{IOUAmount{1, -4}, noIssue()};
STAmount const lhs = divide((a - b) + b, a, noIssue()) - one;
STAmount const rhs = divide((b - a) + a, b, noIssue()) - one;
return ((rhs.negative() ? -rhs : rhs) + (lhs.negative() ? -lhs : lhs)) <= maxLoss;
}
auto const ret = std::visit(
[&]<ValidIssueType TIss1, ValidIssueType TIss2>(
TIss1 const&, TIss2 const&) -> std::optional<bool> {
if constexpr (is_issue_v<TIss1> && is_issue_v<TIss2>)
{
static STAmount const one{IOUAmount{1, 0}, noIssue()};
static STAmount const maxLoss{IOUAmount{1, -4}, noIssue()};
STAmount const lhs = divide((a - b) + b, a, noIssue()) - one;
STAmount const rhs = divide((b - a) + a, b, noIssue()) - one;
return ((rhs.negative() ? -rhs : rhs) + (lhs.negative() ? -lhs : lhs)) <= maxLoss;
}
// MPT (overflow & underflow check)
if (a.holds<MPTIssue>() && b.holds<MPTIssue>())
{
MPTAmount const A = a.mpt();
MPTAmount const B = b.mpt();
return !(
(B > MPTAmount{0} &&
A > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} - B) ||
(B < MPTAmount{0} &&
A < MPTAmount{std::numeric_limits<MPTAmount::value_type>::min()} - B));
}
// MPT (overflow & underflow check)
if constexpr (is_mptissue_v<TIss1> && is_mptissue_v<TIss2>)
{
MPTAmount const A = a.mpt();
MPTAmount const B = b.mpt();
return !(
(B > MPTAmount{0} &&
A > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} - B) ||
(B < MPTAmount{0} &&
A < MPTAmount{std::numeric_limits<MPTAmount::value_type>::min()} - B));
}
return std::nullopt;
},
a.asset().value(),
b.asset().value());
if (ret)
return *ret;
// LCOV_EXCL_START
UNREACHABLE("STAmount::canAdd : unexpected STAmount type");
return false;
@@ -585,27 +606,36 @@ canSubtract(STAmount const& a, STAmount const& b)
}
// IOU case (no underflow)
if (a.holds<Issue>() && b.holds<Issue>())
{
return true;
}
auto const ret = std::visit(
[&]<ValidIssueType TIss1, ValidIssueType TIss2>(
TIss1 const&, TIss2 const&) -> std::optional<bool> {
if constexpr (is_issue_v<TIss1> && is_issue_v<TIss2>)
{
return true;
}
// MPT case (underflow & overflow check)
if (a.holds<MPTIssue>() && b.holds<MPTIssue>())
{
MPTAmount const A = a.mpt();
MPTAmount const B = b.mpt();
// MPT case (underflow & overflow check)
if constexpr (is_mptissue_v<TIss1> && is_mptissue_v<TIss2>)
{
MPTAmount const A = a.mpt();
MPTAmount const B = b.mpt();
// Underflow check
if (B > MPTAmount{0} && A < B)
return false;
// Underflow check
if (B > MPTAmount{0} && A < B)
return false;
// Overflow check
if (B < MPTAmount{0} &&
A > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} + B)
return false;
return true;
}
// Overflow check
if (B < MPTAmount{0} &&
A > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} + B)
return false;
return true;
}
return std::nullopt;
},
a.asset().value(),
b.asset().value());
if (ret)
return *ret;
// LCOV_EXCL_START
UNREACHABLE("STAmount::canSubtract : unexpected STAmount type");
return false;
@@ -751,45 +781,49 @@ STAmount::getJson(JsonOptions) const
void
STAmount::add(Serializer& s) const
{
if (native())
{
XRPL_ASSERT(mOffset == 0, "xrpl::STAmount::add : zero offset");
if (!mIsNegative)
{
s.add64(mValue | cPositive);
}
else
{
mAsset.visit(
[&](MPTIssue const& issue) {
auto u8 = static_cast<unsigned char>(cMPToken >> 56);
if (!mIsNegative)
u8 |= static_cast<unsigned char>(cPositive >> 56);
s.add8(u8);
s.add64(mValue);
}
}
else if (mAsset.holds<MPTIssue>())
{
auto u8 = static_cast<unsigned char>(cMPToken >> 56);
if (!mIsNegative)
u8 |= static_cast<unsigned char>(cPositive >> 56);
s.add8(u8);
s.add64(mValue);
s.addBitString(mAsset.get<MPTIssue>().getMptID());
}
else
{
if (*this == beast::zero)
{
s.add64(cIssuedCurrency);
}
else if (mIsNegative)
{ // 512 = not native
s.add64(mValue | (static_cast<std::uint64_t>(mOffset + 512 + 97) << (64 - 10)));
}
else
{ // 256 = positive
s.add64(mValue | (static_cast<std::uint64_t>(mOffset + 512 + 256 + 97) << (64 - 10)));
}
s.addBitString(mAsset.get<Issue>().currency);
s.addBitString(mAsset.get<Issue>().account);
}
s.addBitString(issue.getMptID());
},
[&](Issue const& issue) {
if (native())
{
XRPL_ASSERT(mOffset == 0, "xrpl::STAmount::add : zero offset");
if (!mIsNegative)
{
s.add64(mValue | cPositive);
}
else
{
s.add64(mValue);
}
}
else
{
if (*this == beast::zero)
{
s.add64(cIssuedCurrency);
}
else if (mIsNegative) // 512 = not native
{
s.add64(mValue | (static_cast<std::uint64_t>(mOffset + 512 + 97) << (64 - 10)));
}
else // 256 = positive
{
s.add64(
mValue |
(static_cast<std::uint64_t>(mOffset + 512 + 256 + 97) << (64 - 10)));
}
s.addBitString(issue.currency);
s.addBitString(issue.account);
}
});
}
bool
@@ -1065,7 +1099,7 @@ amountFromJson(SField const& name, Json::Value const& v)
if (isMPT)
{
// sequence (32 bits) + account (160 bits)
uint192 u;
MPTID u;
if (!u.parseHex(currencyOrMPTID.asString()))
Throw<std::runtime_error>("invalid MPTokenIssuanceID");
asset = u;
@@ -1255,7 +1289,7 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
int numOffset = num.exponent();
int denOffset = den.exponent();
if (num.native() || num.holds<MPTIssue>())
if (num.integral())
{
while (numVal < STAmount::cMinValue)
{
@@ -1265,7 +1299,7 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
}
}
if (den.native() || den.holds<MPTIssue>())
if (den.integral())
{
while (denVal < STAmount::cMinValue)
{
@@ -1330,7 +1364,7 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset)
int offset1 = v1.exponent();
int offset2 = v2.exponent();
if (v1.native() || v1.holds<MPTIssue>())
if (v1.integral())
{
while (value1 < STAmount::cMinValue)
{
@@ -1339,7 +1373,7 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset)
}
}
if (v2.native() || v2.holds<MPTIssue>())
if (v2.integral())
{
while (value2 < STAmount::cMinValue)
{
@@ -1363,7 +1397,7 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset)
// for years, so it is deeply embedded in the behavior of cross-currency
// transactions.
//
// However in 2022 it was noticed that the rounding characteristics were
// However, in 2022 it was noticed that the rounding characteristics were
// surprising. When the code converts from IOU-like to XRP-like there may
// be a fraction of the IOU-like representation that is too small to be
// represented in drops. `canonicalizeRound()` currently does some unusual
@@ -1380,9 +1414,9 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset)
// So an alternative rounding approach was introduced. You'll see that
// alternative below.
static void
canonicalizeRound(bool native, std::uint64_t& value, int& offset, bool)
canonicalizeRound(bool integral, std::uint64_t& value, int& offset, bool)
{
if (native)
if (integral)
{
if (offset < 0)
{
@@ -1419,9 +1453,9 @@ canonicalizeRound(bool native, std::uint64_t& value, int& offset, bool)
// rounding decisions. canonicalizeRoundStrict() tracks all of the bits in
// the value being rounded.
static void
canonicalizeRoundStrict(bool native, std::uint64_t& value, int& offset, bool roundUp)
canonicalizeRoundStrict(bool integral, std::uint64_t& value, int& offset, bool roundUp)
{
if (native)
if (integral)
{
if (offset < 0)
{
@@ -1512,9 +1546,7 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
if (v1 == beast::zero || v2 == beast::zero)
return {asset};
bool const xrp = asset.native();
if (v1.native() && v2.native() && xrp)
if (v1.native() && v2.native() && asset.native())
{
std::uint64_t const minV = std::min(getSNValue(v1), getSNValue(v2));
std::uint64_t const maxV = std::max(getSNValue(v1), getSNValue(v2));
@@ -1545,7 +1577,7 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
std::uint64_t value1 = v1.mantissa(), value2 = v2.mantissa();
int offset1 = v1.exponent(), offset2 = v2.exponent();
if (v1.native() || v1.holds<MPTIssue>())
if (v1.integral())
{
while (value1 < STAmount::cMinValue)
{
@@ -1554,7 +1586,7 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
}
}
if (v2.native() || v2.holds<MPTIssue>())
if (v2.integral())
{
while (value2 < STAmount::cMinValue)
{
@@ -1570,7 +1602,7 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
// range. Dividing their product by 10^14 maintains the
// precision, by scaling the result to 10^16 to 10^18.
//
// If the we're rounding up, we want to round up away
// If we're rounding up, we want to round up away
// from zero, and if we're rounding down, truncation
// is implicit.
std::uint64_t amount =
@@ -1579,7 +1611,7 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
int offset = offset1 + offset2 + 14;
if (resultNegative != roundUp)
{
CanonicalizeFunc(xrp, amount, offset, roundUp);
CanonicalizeFunc(asset.integral(), amount, offset, roundUp);
}
STAmount result = [&]() {
// If appropriate, tell Number to round down. This gives the desired
@@ -1590,7 +1622,7 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
if (roundUp && !resultNegative && !result)
{
if (xrp)
if (asset.integral())
{
// return the smallest value above zero
amount = 1;
@@ -1634,7 +1666,7 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
std::uint64_t numVal = num.mantissa(), denVal = den.mantissa();
int numOffset = num.exponent(), denOffset = den.exponent();
if (num.native() || num.holds<MPTIssue>())
if (num.integral())
{
while (numVal < STAmount::cMinValue)
{
@@ -1643,7 +1675,7 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
}
}
if (den.native() || den.holds<MPTIssue>())
if (den.integral())
{
while (denVal < STAmount::cMinValue)
{
@@ -1668,12 +1700,12 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
int offset = numOffset - denOffset - 17;
if (resultNegative != roundUp)
canonicalizeRound(asset.native() || asset.holds<MPTIssue>(), amount, offset, roundUp);
canonicalizeRound(asset.integral(), amount, offset, roundUp);
STAmount result = [&]() {
// If appropriate, tell Number the rounding mode we are using.
// Note that "roundUp == true" actually means "round away from zero".
// Otherwise round toward zero.
// Otherwise, round toward zero.
using enum Number::rounding_mode;
MightSaveRound const savedRound(roundUp ^ resultNegative ? upward : downward);
return STAmount(asset, amount, offset, resultNegative);
@@ -1681,7 +1713,7 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
if (roundUp && !resultNegative && !result)
{
if (asset.native() || asset.holds<MPTIssue>())
if (asset.integral())
{
// return the smallest value above zero
amount = 1;

View File

@@ -88,22 +88,19 @@ STIssue::getJson(JsonOptions) const
void
STIssue::add(Serializer& s) const
{
if (holds<Issue>())
{
auto const& issue = asset_.get<Issue>();
s.addBitString(issue.currency);
if (!isXRP(issue.currency))
s.addBitString(issue.account);
}
else
{
auto const& issue = asset_.get<MPTIssue>();
s.addBitString(issue.getIssuer());
s.addBitString(noAccount());
std::uint32_t sequence = 0;
memcpy(&sequence, issue.getMptID().data(), sizeof(sequence));
s.add32(sequence);
}
asset_.visit(
[&](Issue const& issue) {
s.addBitString(issue.currency);
if (!isXRP(issue.currency))
s.addBitString(issue.account);
},
[&](MPTIssue const& issue) {
s.addBitString(issue.getIssuer());
s.addBitString(noAccount());
std::uint32_t sequence = 0;
memcpy(&sequence, issue.getMptID().data(), sizeof(sequence));
s.add32(sequence);
});
}
bool
@@ -116,7 +113,9 @@ STIssue::isEquivalent(STBase const& t) const
bool
STIssue::isDefault() const
{
return holds<Issue>() && asset_.get<Issue>() == xrpIssue();
return asset_.visit(
[](Issue const& issue) { return issue == xrpIssue(); },
[](MPTIssue const&) { return false; });
}
STBase*

View File

@@ -749,6 +749,12 @@ STObject::setFieldH128(SField const& field, uint128 const& v)
setFieldUsingSetValue<STUInt128>(field, v);
}
void
STObject::setFieldH192(SField const& field, uint192 const& v)
{
setFieldUsingSetValue<STUInt192>(field, v);
}
void
STObject::setFieldH256(SField const& field, uint256 const& v)
{

View File

@@ -736,7 +736,7 @@ parseLeaf(
std::string const element_name(json_name + "." + ss.str());
// each element in this path has some combination of
// account, currency, or issuer
// account, asset, or issuer
Json::Value pathEl = value[i][j];
@@ -746,14 +746,22 @@ parseLeaf(
return ret;
}
Json::Value const& account = pathEl["account"];
Json::Value const& currency = pathEl["currency"];
Json::Value const& issuer = pathEl["issuer"];
bool hasCurrency = false;
AccountID uAccount, uIssuer;
Currency uCurrency;
if (pathEl.isMember(jss::currency) && pathEl.isMember(jss::mpt_issuance_id))
{
error = RPC::make_error(rpcINVALID_PARAMS, "Invalid Asset.");
return ret;
}
if (!account && !currency && !issuer)
bool const isMPT = pathEl.isMember(jss::mpt_issuance_id);
auto const assetName = isMPT ? jss::mpt_issuance_id : jss::currency;
Json::Value const& account = pathEl[jss::account];
Json::Value const& asset = pathEl[assetName];
Json::Value const& issuer = pathEl[jss::issuer];
bool hasAsset = false;
AccountID uAccount, uIssuer;
PathAsset uAsset;
if (!account && !asset && !issuer)
{
error = invalid_data(element_name);
return ret;
@@ -764,7 +772,7 @@ parseLeaf(
// human account id
if (!account.isString())
{
error = string_expected(element_name, "account");
error = string_expected(element_name, jss::account.c_str());
return ret;
}
@@ -775,31 +783,51 @@ parseLeaf(
auto const a = parseBase58<AccountID>(account.asString());
if (!a)
{
error = invalid_data(element_name, "account");
error = invalid_data(element_name, jss::account.c_str());
return ret;
}
uAccount = *a;
}
}
if (currency)
if (asset)
{
// human currency
if (!currency.isString())
// human asset
if (!asset.isString())
{
error = string_expected(element_name, "currency");
error = string_expected(element_name, assetName.c_str());
return ret;
}
hasCurrency = true;
hasAsset = true;
if (!uCurrency.parseHex(currency.asString()))
if (isMPT)
{
if (!to_currency(uCurrency, currency.asString()))
MPTID u;
if (!u.parseHex(asset.asString()))
{
error = invalid_data(element_name, "currency");
error = invalid_data(element_name, assetName.c_str());
return ret;
}
if (getMPTIssuer(u) == beast::zero)
{
error = invalid_data(element_name, jss::account.c_str());
return ret;
}
uAsset = u;
}
else
{
Currency currency;
if (!currency.parseHex(asset.asString()))
{
if (!to_currency(currency, asset.asString()))
{
error = invalid_data(element_name, assetName.c_str());
return ret;
}
}
uAsset = currency;
}
}
@@ -808,7 +836,7 @@ parseLeaf(
// human account id
if (!issuer.isString())
{
error = string_expected(element_name, "issuer");
error = string_expected(element_name, jss::issuer.c_str());
return ret;
}
@@ -817,14 +845,20 @@ parseLeaf(
auto const a = parseBase58<AccountID>(issuer.asString());
if (!a)
{
error = invalid_data(element_name, "issuer");
error = invalid_data(element_name, jss::issuer.c_str());
return ret;
}
uIssuer = *a;
}
if (isMPT && uIssuer != getMPTIssuer(uAsset.get<MPTID>()))
{
error = invalid_data(element_name, jss::issuer.c_str());
return ret;
}
}
p.emplace_back(uAccount, uCurrency, uIssuer, hasCurrency);
p.emplace_back(uAccount, uAsset, uIssuer, hasAsset);
}
tail.push_back(p);

View File

@@ -31,8 +31,15 @@ STPathElement::get_hash(STPathElement const& element)
for (auto const x : element.getAccountID())
hash_account += (hash_account * 257) ^ x;
for (auto const x : element.getCurrency())
hash_currency += (hash_currency * 509) ^ x;
// Check pathAsset type instead of element's mType
// In some cases mType might be account but the asset
// is still set to either MPT or currency (see Pathfinder::addLink())
element.getPathAsset().visit(
[&](MPTID const& mpt) { hash_currency += beast::uhash<>{}(mpt); },
[&](Currency const& currency) {
for (auto const x : currency)
hash_currency += (hash_currency * 509) ^ x;
});
for (auto const x : element.getIssuerID())
hash_issuer += (hash_issuer * 911) ^ x;
@@ -68,24 +75,30 @@ STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name)
}
else
{
auto hasAccount = iType & STPathElement::typeAccount;
auto hasCurrency = iType & STPathElement::typeCurrency;
auto hasIssuer = iType & STPathElement::typeIssuer;
auto const hasAccount = (iType & STPathElement::typeAccount) != 0u;
auto const hasCurrency = (iType & STPathElement::typeCurrency) != 0u;
auto const hasIssuer = (iType & STPathElement::typeIssuer) != 0u;
auto const hasMPT = (iType & STPathElement::typeMPT) != 0u;
AccountID account;
Currency currency;
PathAsset asset;
AccountID issuer;
if (hasAccount != 0)
if (hasAccount)
account = sit.get160();
if (hasCurrency != 0)
currency = sit.get160();
XRPL_ASSERT(
!(hasCurrency && hasMPT), "xrpl::STPathSet::STPathSet : not has Currency and MPT");
if (hasCurrency)
asset = static_cast<Currency>(sit.get160());
if (hasIssuer != 0)
if (hasMPT)
asset = sit.get192();
if (hasIssuer)
issuer = sit.get160();
path.emplace_back(account, currency, issuer, hasCurrency);
path.emplace_back(account, asset, issuer, hasCurrency);
}
}
}
@@ -137,11 +150,11 @@ STPathSet::isDefault() const
}
bool
STPath::hasSeen(AccountID const& account, Currency const& currency, AccountID const& issuer) const
STPath::hasSeen(AccountID const& account, PathAsset const& asset, AccountID const& issuer) const
{
for (auto& p : mPath)
{
if (p.getAccountID() == account && p.getCurrency() == currency && p.getIssuerID() == issuer)
if (p.getAccountID() == account && p.getPathAsset() == asset && p.getIssuerID() == issuer)
return true;
}
@@ -163,9 +176,16 @@ STPath::getJson(JsonOptions) const
if ((iType & STPathElement::typeAccount) != 0u)
elem[jss::account] = to_string(it.getAccountID());
XRPL_ASSERT(
((iType & STPathElement::typeCurrency) == 0u) ||
((iType & STPathElement::typeMPT) == 0u),
"xrpl::STPath::getJson : not type Currency and MPT");
if ((iType & STPathElement::typeCurrency) != 0u)
elem[jss::currency] = to_string(it.getCurrency());
if ((iType & STPathElement::typeMPT) != 0u)
elem[jss::mpt_issuance_id] = to_string(it.getMPTID());
if ((iType & STPathElement::typeIssuer) != 0u)
elem[jss::issuer] = to_string(it.getIssuerID());
@@ -209,13 +229,16 @@ STPathSet::add(Serializer& s) const
s.add8(iType);
if ((iType & STPathElement::typeAccount) != 0)
if ((iType & STPathElement::typeAccount) != 0u)
s.addBitString(speElement.getAccountID());
if ((iType & STPathElement::typeCurrency) != 0)
if ((iType & STPathElement::typeMPT) != 0u)
s.addBitString(speElement.getMPTID());
if ((iType & STPathElement::typeCurrency) != 0u)
s.addBitString(speElement.getCurrency());
if ((iType & STPathElement::typeIssuer) != 0)
if ((iType & STPathElement::typeIssuer) != 0u)
s.addBitString(speElement.getIssuerID());
}

View File

@@ -156,6 +156,7 @@ transResults()
MAKE_ERROR(temBAD_FEE, "Invalid fee, negative or not XRP."),
MAKE_ERROR(temBAD_ISSUER, "Malformed: Bad issuer."),
MAKE_ERROR(temBAD_LIMIT, "Limits must be non-negative."),
MAKE_ERROR(temBAD_MPT, "Malformed: Bad MPT."),
MAKE_ERROR(temBAD_OFFER, "Malformed: Bad offer."),
MAKE_ERROR(temBAD_PATH, "Malformed: Bad path."),
MAKE_ERROR(temBAD_PATH_LOOP, "Malformed: Loop in path."),
@@ -214,6 +215,7 @@ transResults()
MAKE_ERROR(terNO_AMM, "AMM doesn't exist for the asset pair."),
MAKE_ERROR(terADDRESS_COLLISION, "Failed to allocate an unique account address."),
MAKE_ERROR(terNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."),
MAKE_ERROR(terLOCKED, "Fund is locked."),
MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."),
};

View File

@@ -1006,6 +1006,26 @@ removeDeletedTrustLines(
}
}
static void
removeDeletedMPTs(ApplyView& view, std::vector<uint256> const& mpts, beast::Journal viewJ)
{
// There could be at most two MPTs - one for each side of AMM pool
if (mpts.size() > 2)
{
JLOG(viewJ.error()) << "removeDeletedMPTs: deleted mpts exceed 2 " << mpts.size();
return;
}
for (auto const& index : mpts)
{
if (auto const sleState = view.peek({ltMPTOKEN, index}); sleState &&
deleteAMMMPToken(view, sleState, (*sleState)[sfIssuer], viewJ) != tesSUCCESS)
{
JLOG(viewJ.error()) << "removeDeletedMPTs: failed to delete AMM MPT";
}
}
}
/** Reset the context, discarding any changes made and adjust the fee.
@param fee The transaction fee to be charged.
@@ -1144,19 +1164,21 @@ Transactor::operator()()
// when transactions fail with a `tec` code.
std::vector<uint256> removedOffers;
std::vector<uint256> removedTrustLines;
std::vector<uint256> removedMPTs;
std::vector<uint256> expiredNFTokenOffers;
std::vector<uint256> expiredCredentials;
bool const doOffers = ((result == tecOVERSIZE) || (result == tecKILLED));
bool const doLines = (result == tecINCOMPLETE);
bool const doLinesOrMPTs = (result == tecINCOMPLETE);
bool const doNFTokenOffers = (result == tecEXPIRED);
bool const doCredentials = (result == tecEXPIRED);
if (doOffers || doLines || doNFTokenOffers || doCredentials)
if (doOffers || doLinesOrMPTs || doNFTokenOffers || doCredentials)
{
ctx_.visit([doOffers,
&removedOffers,
doLines,
doLinesOrMPTs,
&removedTrustLines,
&removedMPTs,
doNFTokenOffers,
&expiredNFTokenOffers,
doCredentials,
@@ -1178,10 +1200,17 @@ Transactor::operator()()
removedOffers.push_back(index);
}
if (doLines && before && after && (before->getType() == ltRIPPLE_STATE))
if (doLinesOrMPTs && before && after)
{
// Removal of obsolete AMM trust line
removedTrustLines.push_back(index);
if (before->getType() == ltRIPPLE_STATE)
{
removedTrustLines.push_back(index);
}
else if (before->getType() == ltMPTOKEN)
{
removedMPTs.push_back(index);
}
}
if (doNFTokenOffers && before && after &&
@@ -1219,6 +1248,7 @@ Transactor::operator()()
{
removeDeletedTrustLines(
view(), removedTrustLines, ctx_.registry.get().getJournal("View"));
removeDeletedMPTs(view(), removedMPTs, ctx_.registry.get().getJournal("View"));
}
if (result == tecEXPIRED)

View File

@@ -3,7 +3,6 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/AMMUtils.h>
#include <xrpl/protocol/TxFormats.h>
namespace xrpl {
@@ -128,15 +127,16 @@ ValidAMM::finalizeCreate(
auto const [amount, amount2] = ammPoolHolds(
view,
*ammAccount_,
tx[sfAmount].get<Issue>(),
tx[sfAmount2].get<Issue>(),
tx[sfAmount].asset(),
tx[sfAmount2].asset(),
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j);
// Create invariant:
// sqrt(amount * amount2) == LPTokens
// all balances are greater than zero
if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) ||
ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != *lptAMMBalanceAfter_)
ammLPTokens(amount, amount2, lptAMMBalanceAfter_->get<Issue>()) != *lptAMMBalanceAfter_)
{
JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " "
<< *lptAMMBalanceAfter_;
@@ -188,12 +188,7 @@ ValidAMM::generalInvariant(
beast::Journal const& j) const
{
auto const [amount, amount2] = ammPoolHolds(
view,
*ammAccount_,
tx[sfAsset].get<Issue>(),
tx[sfAsset2].get<Issue>(),
fhIGNORE_FREEZE,
j);
view, *ammAccount_, tx[sfAsset], tx[sfAsset2], fhIGNORE_FREEZE, ahIGNORE_AUTH, j);
// Deposit and Withdrawal invariant:
// sqrt(amount * amount2) >= LPTokens
// all balances are greater than zero

View File

@@ -172,7 +172,7 @@ TransfersNotFrozen::recordBalanceChanges(
STAmount const& balanceChange)
{
auto const balanceChangeSign = balanceChange.signum();
auto const currency = after->at(sfBalance).getCurrency();
auto const currency = after->at(sfBalance).get<Issue>().currency;
// Change from low account's perspective, which is trust line default
recordBalance({currency, after->at(sfHighLimit).getIssuer()}, {after, balanceChangeSign});

View File

@@ -284,25 +284,29 @@ NoZeroEscrow::visitEntry(
}
else
{
// IOU case
if (amount.holds<Issue>())
{
if (amount <= beast::zero)
return true;
return amount.asset().visit(
[&](Issue const& issue) {
// IOU case
if (amount <= beast::zero)
return true;
if (badCurrency() == amount.getCurrency())
return true;
}
if (badCurrency() == issue.currency)
return true;
// MPT case
if (amount.holds<MPTIssue>())
{
if (amount <= beast::zero)
return true;
return false;
}
if (amount.mpt() > MPTAmount{maxMPTokenAmount})
return true; // LCOV_EXCL_LINE
}
// MPT case
,
[&](MPTIssue const&) {
if (amount <= beast::zero)
return true;
if (amount.mpt() > MPTAmount{maxMPTokenAmount})
return true; // LCOV_EXCL_LINE
return false;
});
}
return false;
};
@@ -600,8 +604,8 @@ NoXRPTrustLines::visitEntry(
// checking the issue directly here instead of
// relying on .native() just in case native somehow
// were systematically incorrect
xrpTrustLine_ = after->getFieldAmount(sfLowLimit).issue() == xrpIssue() ||
after->getFieldAmount(sfHighLimit).issue() == xrpIssue();
xrpTrustLine_ = after->getFieldAmount(sfLowLimit).asset() == xrpIssue() ||
after->getFieldAmount(sfHighLimit).asset() == xrpIssue();
}
}
@@ -774,17 +778,23 @@ ValidClawback::finalize(
return false;
}
if (trustlinesChanged == 1)
bool const mptV2Enabled = view.rules().enabled(featureMPTokensV2);
if (trustlinesChanged == 1 || (mptV2Enabled && mptokensChanged == 1))
{
AccountID const issuer = tx.getAccountID(sfAccount);
STAmount const& amount = tx.getFieldAmount(sfAmount);
AccountID const& holder = amount.getIssuer();
STAmount const holderBalance =
accountHolds(view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j);
STAmount const holderBalance = amount.asset().visit(
[&](Issue const& issue) {
return accountHolds(view, holder, issue.currency, issuer, fhIGNORE_FREEZE, j);
},
[&](MPTIssue const& issue) {
return accountHolds(view, issuer, issue, fhIGNORE_FREEZE, ahIGNORE_AUTH, j);
});
if (holderBalance.signum() < 0)
{
JLOG(j.fatal()) << "Invariant failed: trustline balance is negative";
JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative";
return false;
}
}

View File

@@ -2,6 +2,7 @@
//
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
@@ -52,9 +53,10 @@ ValidMPTIssuance::finalize(
ReadView const& view,
beast::Journal const& j) const
{
if (isTesSuccess(result))
auto const& rules = view.rules();
bool const mptV2Enabled = rules.enabled(featureMPTokensV2);
if (isTesSuccess(result) || (mptV2Enabled && result == tecINCOMPLETE))
{
auto const& rules = view.rules();
[[maybe_unused]]
bool const enforceCreatedByIssuer =
rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol);
@@ -112,12 +114,12 @@ ValidMPTIssuance::finalize(
return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1;
}
bool const lendingProtocolEnabled = view.rules().enabled(featureLendingProtocol);
bool const lendingProtocolEnabled = rules.enabled(featureLendingProtocol);
// ttESCROW_FINISH may authorize an MPT, but it can't have the
// mayAuthorizeMPT privilege, because that may cause
// non-amendment-gated side effects.
bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) &&
(view.rules().enabled(featureSingleAssetVault) || lendingProtocolEnabled);
(rules.enabled(featureSingleAssetVault) || lendingProtocolEnabled);
if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || enforceEscrowFinish)
{
bool const submittedByIssuer = tx.isFieldPresent(sfHolder);
@@ -134,19 +136,42 @@ ValidMPTIssuance::finalize(
"succeeded but deleted issuances";
return false;
}
if (lendingProtocolEnabled && mptokensCreated_ + mptokensDeleted_ > 1)
if (mptV2Enabled && hasPrivilege(tx, mayAuthorizeMPT) &&
(txnType == ttAMM_WITHDRAW || txnType == ttAMM_CLAWBACK))
{
if (submittedByIssuer && txnType == ttAMM_WITHDRAW && mptokensCreated_ > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize "
"submitted by issuer succeeded "
"but created bad number of mptokens";
return false;
}
// At most one MPToken may be created on withdraw/clawback since:
// - Liquidity Provider must have at least one token in order
// participate in AMM pool liquidity.
// - At most two MPTokens may be deleted if AMM pool, which has exactly
// two tokens, is empty after withdraw/clawback.
if (mptokensCreated_ > 1 || mptokensDeleted_ > 2)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
"but created/deleted bad number of mptokens";
return false;
}
}
else if (lendingProtocolEnabled && (mptokensCreated_ + mptokensDeleted_) > 1)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
"but created/deleted bad number mptokens";
return false;
}
if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0))
else if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0))
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer "
"succeeded but created/deleted mptokens";
return false;
}
if (!submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) &&
else if (
!submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) &&
(mptokensCreated_ + mptokensDeleted_ != 1))
{
// if the holder submitted this tx, then a mptoken must be
@@ -158,6 +183,52 @@ ValidMPTIssuance::finalize(
return true;
}
if (hasPrivilege(tx, mayCreateMPT))
{
bool const submittedByIssuer = tx.isFieldPresent(sfHolder);
if (mptIssuancesCreated_ > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize "
"succeeded but created MPT issuances";
return false;
}
if (mptIssuancesDeleted_ > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize "
"succeeded but deleted issuances";
return false;
}
if (mptokensDeleted_ > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize "
"succeeded but deleted MPTokens";
return false;
}
// AMMCreate may auto-create up to two MPT objects:
// - one per asset side in an MPT/MPT AMM, or one in an IOU/MPT AMM.
// CheckCash may auto-create at most one MPT object for the receiver.
if ((txnType == ttAMM_CREATE && mptokensCreated_ > 2) ||
(txnType == ttCHECK_CASH && mptokensCreated_ > 1))
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize "
"succeeded but created bad number of mptokens";
return false;
}
if (submittedByIssuer)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer "
"succeeded but created mptokens";
return false;
}
// Offer crossing or payment may consume multiple offers
// where takerPays is MPT amount. If the offer owner doesn't
// own MPT then MPT is created automatically.
return true;
}
if (txnType == ttESCROW_FINISH)
{
// ttESCROW_FINISH may authorize an MPT, but it can't have the
@@ -168,8 +239,9 @@ ValidMPTIssuance::finalize(
return true;
}
if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 && mptokensCreated_ == 0 &&
mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0)
if (hasPrivilege(tx, mayDeleteMPT) &&
((txnType == ttAMM_DELETE && mptokensDeleted_ <= 2) || mptokensDeleted_ == 1) &&
mptokensCreated_ == 0 && mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0)
return true;
}
@@ -194,4 +266,223 @@ ValidMPTIssuance::finalize(
mptokensDeleted_ == 0;
}
void
ValidMPTPayment::visitEntry(
bool,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (overflow_)
return;
auto makeKey = [](SLE const& sle) {
if (sle.getType() == ltMPTOKEN_ISSUANCE)
return makeMptID(sle[sfSequence], sle[sfIssuer]);
return sle[sfMPTokenIssuanceID];
};
auto update = [&](SLE const& sle, Order order) -> bool {
auto const type = sle.getType();
if (type == ltMPTOKEN_ISSUANCE)
{
auto const outstanding = sle[sfOutstandingAmount];
if (outstanding > maxMPTokenAmount)
{
overflow_ = true;
return false;
}
data_[makeKey(sle)].outstanding[order] = outstanding;
}
else if (type == ltMPTOKEN)
{
auto const mptAmt = sle[sfMPTAmount];
auto const lockedAmt = sle[~sfLockedAmount].value_or(0);
if (mptAmt > maxMPTokenAmount || lockedAmt > maxMPTokenAmount ||
lockedAmt > (maxMPTokenAmount - mptAmt))
{
overflow_ = true;
return false;
}
auto const res = static_cast<std::int64_t>(mptAmt + lockedAmt);
// subtract before from after
if (order == Before)
{
data_[makeKey(sle)].mptAmount -= res;
}
else
{
data_[makeKey(sle)].mptAmount += res;
}
}
return true;
};
if (before && !update(*before, Before))
return;
if (after)
{
if (after->getType() == ltMPTOKEN_ISSUANCE)
{
overflow_ = (*after)[sfOutstandingAmount] > maxMPTAmount(*after);
}
if (!update(*after, After))
return;
}
}
bool
ValidMPTPayment::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (isTesSuccess(result))
{
bool const enforce = view.rules().enabled(featureMPTokensV2);
if (overflow_)
{
JLOG(j.fatal()) << "Invariant failed: OutstandingAmount overflow";
return !enforce;
}
auto const signedMax = static_cast<std::int64_t>(maxMPTokenAmount);
for (auto const& [id, data] : data_)
{
(void)id;
bool const addOverflows =
(data.mptAmount > 0 && data.outstanding[Before] > (signedMax - data.mptAmount)) ||
(data.mptAmount < 0 && data.outstanding[Before] < (-signedMax - data.mptAmount));
if (addOverflows ||
data.outstanding[After] != (data.outstanding[Before] + data.mptAmount))
{
JLOG(j.fatal()) << "Invariant failed: invalid OutstandingAmount balance "
<< data.outstanding[Before] << " " << data.outstanding[After] << " "
<< data.mptAmount;
return !enforce;
}
}
}
return true;
}
void
ValidMPTTransfer::visitEntry(
bool,
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 (before)
update(*before, true);
if (after)
update(*after, false);
}
bool
ValidMPTTransfer::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// AMMClawback is called by the issuer, so freeze restrictions do not apply.
auto const txnType = tx.getTxnType();
if (txnType == ttAMM_CLAWBACK)
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, &txnType] {
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 frozen = false;
auto const sleIssuance = view.read(keylet::mptIssuance(mptID));
if (!sleIssuance)
{
continue;
}
auto const canTransfer = sleIssuance->isFlag(lsfMPTCanTransfer);
auto const canTrade = sleIssuance->isFlag(lsfMPTCanTrade);
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 (!frozen && isFrozen(view, account, MPTIssue{mptID}))
{
frozen = 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 ((frozen || !canTransfer || (isDEX && !canTrade)) && senders > 0 && receivers > 0)
{
JLOG(j.fatal()) << "Invariant failed: invalid MPToken transfer between holders";
return enforce;
}
}
return true;
}
} // namespace xrpl

View File

@@ -8,15 +8,15 @@ AMMLiquidity<TIn, TOut>::AMMLiquidity(
ReadView const& view,
AccountID const& ammAccountID,
std::uint32_t tradingFee,
Issue const& in,
Issue const& out,
Asset const& in,
Asset const& out,
AMMContext& ammContext,
beast::Journal j)
: ammContext_(ammContext)
, ammAccountID_(ammAccountID)
, tradingFee_(tradingFee)
, issueIn_(in)
, issueOut_(out)
, assetIn_(in)
, assetOut_(out)
, initialBalances_{fetchBalances(view)}
, j_(j)
{
@@ -26,13 +26,13 @@ template <typename TIn, typename TOut>
TAmounts<TIn, TOut>
AMMLiquidity<TIn, TOut>::fetchBalances(ReadView const& view) const
{
auto const assetIn = ammAccountHolds(view, ammAccountID_, issueIn_);
auto const assetOut = ammAccountHolds(view, ammAccountID_, issueOut_);
auto const amountIn = ammAccountHolds(view, ammAccountID_, assetIn_);
auto const amountOut = ammAccountHolds(view, ammAccountID_, assetOut_);
// This should not happen.
if (assetIn < beast::zero || assetOut < beast::zero)
if (amountIn < beast::zero || amountOut < beast::zero)
Throw<std::runtime_error>("AMMLiquidity: invalid balances");
return TAmounts{get<TIn>(assetIn), get<TOut>(assetOut)};
return TAmounts{get<TIn>(amountIn), get<TOut>(amountOut)};
}
template <typename TIn, typename TOut>
@@ -42,7 +42,7 @@ AMMLiquidity<TIn, TOut>::generateFibSeqOffer(TAmounts<TIn, TOut> const& balances
TAmounts<TIn, TOut> cur{};
cur.in = toAmount<TIn>(
getIssue(balances.in),
getAsset(balances.in),
InitialFibSeqPct * initialBalances_.in,
Number::rounding_mode::upward);
cur.out = swapAssetIn(initialBalances_, cur.in, tradingFee_);
@@ -60,7 +60,7 @@ AMMLiquidity<TIn, TOut>::generateFibSeqOffer(TAmounts<TIn, TOut> const& balances
"xrpl::AMMLiquidity::generateFibSeqOffer : maximum iterations");
cur.out = toAmount<TOut>(
getIssue(balances.out),
getAsset(balances.out),
cur.out * fib[ammContext_.curIters() - 1],
Number::rounding_mode::downward);
// swapAssetOut() returns negative in this case
@@ -89,14 +89,18 @@ maxAmount()
{
return STAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset);
}
else if constexpr (std::is_same_v<T, MPTAmount>)
{
return MPTAmount(maxMPTokenAmount);
}
}
template <typename T>
T
maxOut(T const& out, Issue const& iss)
maxOut(T const& out, Asset const& asset)
{
Number const res = out * Number{99, -2};
return toAmount<T>(iss, res, Number::rounding_mode::downward);
return toAmount<T>(asset, res, Number::rounding_mode::downward);
}
} // namespace
@@ -113,7 +117,7 @@ AMMLiquidity<TIn, TOut>::maxOffer(TAmounts<TIn, TOut> const& balances, Rules con
Quality{balances});
}
auto const out = maxOut<TOut>(balances.out, issueOut());
auto const out = maxOut<TOut>(balances.out, assetOut());
if (out <= TOut{0} || out >= balances.out)
return std::nullopt;
return AMMOffer<TIn, TOut>(
@@ -211,8 +215,8 @@ AMMLiquidity<TIn, TOut>::getOffer(ReadView const& view, std::optional<Quality> c
if (offer->amount().in > beast::zero && offer->amount().out > beast::zero)
{
JLOG(j_.trace()) << "AMMLiquidity::getOffer, created " << to_string(offer->amount().in)
<< "/" << issueIn_ << " " << to_string(offer->amount().out) << "/"
<< issueOut_;
<< "/" << assetIn_ << " " << to_string(offer->amount().out) << "/"
<< assetOut_;
return offer;
}
@@ -225,9 +229,13 @@ AMMLiquidity<TIn, TOut>::getOffer(ReadView const& view, std::optional<Quality> c
return std::nullopt;
}
template class AMMLiquidity<STAmount, STAmount>;
template class AMMLiquidity<IOUAmount, IOUAmount>;
template class AMMLiquidity<XRPAmount, IOUAmount>;
template class AMMLiquidity<IOUAmount, XRPAmount>;
template class AMMLiquidity<MPTAmount, MPTAmount>;
template class AMMLiquidity<XRPAmount, MPTAmount>;
template class AMMLiquidity<MPTAmount, XRPAmount>;
template class AMMLiquidity<MPTAmount, IOUAmount>;
template class AMMLiquidity<IOUAmount, MPTAmount>;
} // namespace xrpl

View File

@@ -4,7 +4,7 @@
namespace xrpl {
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
AMMOffer<TIn, TOut>::AMMOffer(
AMMLiquidity<TIn, TOut> const& ammLiquidity,
TAmounts<TIn, TOut> const& amounts,
@@ -15,28 +15,35 @@ AMMOffer<TIn, TOut>::AMMOffer(
{
}
template <typename TIn, typename TOut>
Issue const&
AMMOffer<TIn, TOut>::issueIn() const
template <StepAmount TIn, StepAmount TOut>
Asset const&
AMMOffer<TIn, TOut>::assetIn() const
{
return ammLiquidity_.issueIn();
return ammLiquidity_.assetIn();
}
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
Asset const&
AMMOffer<TIn, TOut>::assetOut() const
{
return ammLiquidity_.assetOut();
}
template <StepAmount TIn, StepAmount TOut>
AccountID const&
AMMOffer<TIn, TOut>::owner() const
{
return ammLiquidity_.ammAccount();
}
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
TAmounts<TIn, TOut> const&
AMMOffer<TIn, TOut>::amount() const
{
return amounts_;
}
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
void
AMMOffer<TIn, TOut>::consume(ApplyView& view, TAmounts<TIn, TOut> const& consumed)
{
@@ -52,7 +59,7 @@ AMMOffer<TIn, TOut>::consume(ApplyView& view, TAmounts<TIn, TOut> const& consume
ammLiquidity_.context().setAMMUsed();
}
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
TAmounts<TIn, TOut>
AMMOffer<TIn, TOut>::limitOut(
TAmounts<TIn, TOut> const& offerAmount,
@@ -77,7 +84,7 @@ AMMOffer<TIn, TOut>::limitOut(
return {swapAssetOut(balances_, limit, ammLiquidity_.tradingFee()), limit};
}
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
TAmounts<TIn, TOut>
AMMOffer<TIn, TOut>::limitIn(TAmounts<TIn, TOut> const& offerAmount, TIn const& limit, bool roundUp)
const
@@ -94,7 +101,7 @@ AMMOffer<TIn, TOut>::limitIn(TAmounts<TIn, TOut> const& offerAmount, TIn const&
return {limit, swapAssetIn(balances_, limit, ammLiquidity_.tradingFee())};
}
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
QualityFunction
AMMOffer<TIn, TOut>::getQualityFunc() const
{
@@ -103,7 +110,7 @@ AMMOffer<TIn, TOut>::getQualityFunc() const
return QualityFunction{balances_, ammLiquidity_.tradingFee(), QualityFunction::AMMTag{}};
}
template <typename TIn, typename TOut>
template <StepAmount TIn, StepAmount TOut>
bool
AMMOffer<TIn, TOut>::checkInvariant(TAmounts<TIn, TOut> const& consumed, beast::Journal j) const
{
@@ -133,9 +140,13 @@ AMMOffer<TIn, TOut>::checkInvariant(TAmounts<TIn, TOut> const& consumed, beast::
return false;
}
template class AMMOffer<STAmount, STAmount>;
template class AMMOffer<IOUAmount, IOUAmount>;
template class AMMOffer<XRPAmount, IOUAmount>;
template class AMMOffer<IOUAmount, XRPAmount>;
template class AMMOffer<MPTAmount, MPTAmount>;
template class AMMOffer<XRPAmount, MPTAmount>;
template class AMMOffer<MPTAmount, XRPAmount>;
template class AMMOffer<IOUAmount, MPTAmount>;
template class AMMOffer<MPTAmount, IOUAmount>;
} // namespace xrpl

View File

@@ -2,8 +2,9 @@
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/helpers/AMMUtils.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>
@@ -14,6 +15,7 @@
#include <xrpl/tx/paths/OfferStream.h>
#include <xrpl/tx/paths/detail/FlatSets.h>
#include <xrpl/tx/paths/detail/Steps.h>
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
#include <boost/container/flat_set.hpp>
@@ -40,7 +42,7 @@ protected:
/** Number of offers consumed or partially consumed the last time
the step ran, including expired and unfunded offers.
N.B. This this not the total number offers consumed by this step for the
N.B. This is not the total number offers consumed by this step for the
entire payment, it is only the number the last time it ran. Offers may
be partially consumed multiple times during a payment.
*/
@@ -50,6 +52,7 @@ protected:
// quality or there is no CLOB offer.
std::optional<AMMLiquidity<TIn, TOut>> ammLiquidity_;
beast::Journal const j_;
Asset const strandDeliver_;
struct Cache
{
@@ -64,13 +67,14 @@ protected:
std::optional<Cache> cache_;
private:
BookStep(StrandContext const& ctx, Issue const& in, Issue const& out)
BookStep(StrandContext const& ctx, Asset const& in, Asset const& out)
: book_(in, out, ctx.domainID)
, strandSrc_(ctx.strandSrc)
, strandDst_(ctx.strandDst)
, prevStep_(ctx.prevStep)
, ownerPaysTransferFee_(ctx.ownerPaysTransferFee)
, j_(ctx.j)
, strandDeliver_(ctx.strandDeliver)
{
if (auto const ammSle = ctx.view.read(keylet::amm(in, out));
ammSle && ammSle->getFieldAmount(sfLPTokenBalance) != beast::zero)
@@ -163,11 +167,14 @@ protected:
{
std::ostringstream ostr;
ostr << name << ": "
<< "\ninIss: " << book_.in.account << "\noutIss: " << book_.out.account
<< "\ninCur: " << book_.in.currency << "\noutCur: " << book_.out.currency;
<< "\ninIss: " << book_.in.getIssuer() << "\noutIss: " << book_.out.getIssuer()
<< "\ninCur: " << to_string(book_.in) << "\noutCur: " << to_string(book_.out);
return ostr.str();
}
Rate
rate(ReadView const& view, Asset const& asset, AccountID const& dstAccount) const;
private:
friend bool
operator==(BookStep const& lhs, BookStep const& rhs)
@@ -188,7 +195,7 @@ private:
// Unfunded offers and bad offers are skipped (and returned).
// callback is called with the offer SLE, taker pays, taker gets.
// If callback returns false, don't process any more offers.
// Return the unfunded and bad offers and the number of offers consumed.
// Return the unfunded, bad offers and the number of offers consumed.
template <class Callback>
std::pair<boost::container::flat_set<uint256>, std::uint32_t>
forEachOffer(
@@ -227,6 +234,11 @@ private:
std::optional<QualityFunction>
tipOfferQualityF(ReadView const& view) const;
// Check that takerPays/takerGets can be transferred/traded.
// Applies to MPT assets.
bool
checkMPTDEX(ReadView const& view, AccountID const& owner) const;
friend TDerived;
};
@@ -245,7 +257,7 @@ class BookPaymentStep : public BookStep<TIn, TOut, BookPaymentStep<TIn, TOut>>
public:
explicit BookPaymentStep() = default;
BookPaymentStep(StrandContext const& ctx, Issue const& in, Issue const& out)
BookPaymentStep(StrandContext const& ctx, Asset const& in, Asset const& out)
: BookStep<TIn, TOut, BookPaymentStep<TIn, TOut>>(ctx, in, out)
{
}
@@ -310,17 +322,12 @@ public:
// (the old code does not charge a fee)
// Calculate amount that goes to the taker and the amount charged the
// offer owner
auto rate = [&](AccountID const& id) {
if (isXRP(id) || id == this->strandDst_)
return parityRate;
return transferRate(v, id);
};
auto const trIn = redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate;
auto const trIn =
redeems(prevStepDir) ? this->rate(v, this->book_.in, this->strandDst_) : parityRate;
// Always charge the transfer fee, even if the owner is the issuer,
// unless the fee is waived
auto const trOut = (this->ownerPaysTransferFee_ && waiveFee == WaiveTransferFee::No)
? rate(this->book_.out.account)
? this->rate(v, this->book_.out, this->strandDst_)
: parityRate;
Quality const q1{getRate(STAmount(trOut.value), STAmount(trIn.value))};
@@ -355,7 +362,7 @@ private:
}
public:
BookOfferCrossingStep(StrandContext const& ctx, Issue const& in, Issue const& out)
BookOfferCrossingStep(StrandContext const& ctx, Asset const& in, Asset const& out)
: BookStep<TIn, TOut, BookOfferCrossingStep<TIn, TOut>>(ctx, in, out)
, defaultPath_(ctx.isDefaultPath)
, qualityThreshold_(getQuality(ctx.limitQuality))
@@ -454,7 +461,7 @@ public:
auto const srcAcct = (prevStep != nullptr) ? prevStep->directStepSrcAcct() : std::nullopt;
return owner == srcAcct // If offer crossing && prevStep is DirectI
? QUALITY_ONE // && src is offer owner
? QUALITY_ONE // or MPTEndpoint && src is offer owner
: trIn; // then rate = QUALITY_ONE
}
@@ -502,13 +509,8 @@ public:
return ofrQ;
}
auto rate = [&](AccountID const& id) {
if (isXRP(id) || id == this->strandDst_)
return parityRate;
return transferRate(v, id);
};
auto const trIn = redeems(prevStepDir) ? rate(this->book_.in.account) : parityRate;
auto const trIn =
redeems(prevStepDir) ? this->rate(v, this->book_.in, this->strandDst_) : parityRate;
// AMM doesn't pay the transfer fee on the out amount
auto const trOut = parityRate;
@@ -659,15 +661,11 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
// (the old code does not charge a fee)
// Calculate amount that goes to the taker and the amount charged the offer
// owner
auto rate = [this, &sb](AccountID const& id) -> std::uint32_t {
if (isXRP(id) || id == this->strandDst_)
return QUALITY_ONE;
return transferRate(sb, id).value;
};
std::uint32_t const trIn = redeems(prevStepDir) ? rate(book_.in.account) : QUALITY_ONE;
std::uint32_t const trIn =
redeems(prevStepDir) ? rate(sb, book_.in, this->strandDst_).value : QUALITY_ONE;
// Always charge the transfer fee, even if the owner is the issuer
std::uint32_t const trOut = ownerPaysTransferFee_ ? rate(book_.out.account) : QUALITY_ONE;
std::uint32_t const trOut =
ownerPaysTransferFee_ ? rate(sb, book_.out, this->strandDst_).value : QUALITY_ONE;
typename FlowOfferStream<TIn, TOut>::StepCounter counter(MaxOffersToConsume, j_);
@@ -691,45 +689,49 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
strandSrc_, strandDst_, offer, ofrQ, offers, offerAttempted))
return true;
// Make sure offer owner has authorization to own IOUs from issuer.
// An account can always own XRP or their own IOUs.
if (!isXRP(offer.issueIn().currency) && offer.owner() != offer.issueIn().account)
Asset const& assetIn = offer.assetIn();
bool const isAssetInMPT = assetIn.holds<MPTIssue>();
auto const& owner = offer.owner();
if (isAssetInMPT)
{
auto const& issuerID = offer.issueIn().account;
auto const issuer = afView.read(keylet::account(issuerID));
if (issuer && ((*issuer)[sfFlags] & lsfRequireAuth))
// Create MPToken for the offer's owner. No need to check
// for the reserve since the offer is removed if it is consumed.
// Therefore, the owner count remains the same.
if (auto const err = checkCreateMPT(sb, assetIn.get<MPTIssue>(), owner, j_);
!isTesSuccess(err))
{
// Issuer requires authorization. See if offer owner has that.
auto const& ownerID = offer.owner();
auto const authFlag = issuerID > ownerID ? lsfHighAuth : lsfLowAuth;
auto const line =
afView.read(keylet::line(ownerID, issuerID, offer.issueIn().currency));
if (!line || (((*line)[sfFlags] & authFlag) == 0))
{
// Offer owner not authorized to hold IOU from issuer.
// Remove this offer even if no crossing occurs.
if (auto const key = offer.key())
offers.permRmOffer(*key);
if (!offerAttempted)
{
// Change quality only if no previous offers were tried.
ofrQ = std::nullopt;
}
// Returning true causes offers.step() to delete the offer.
return true;
}
return true;
}
}
// It shouldn't matter from auth point of view whether it's sb
// or afView. Amendment guard this change just in case.
auto& applyView = sb.rules().enabled(featureMPTokensV2) ? sb : afView;
// Make sure offer owner has authorization to own Assets from issuer
// and MPT assets can be traded/transferred.
// An account can always own XRP or their own Assets.
if (!isTesSuccess(requireAuth(applyView, assetIn, owner)) || !checkMPTDEX(sb, owner))
{
// Offer owner not authorized to hold IOU/MPT from issuer.
// Remove this offer even if no crossing occurs.
if (auto const key = offer.key())
offers.permRmOffer(*key);
if (!offerAttempted)
{
// Change quality only if no previous offers were tried.
ofrQ = std::nullopt;
}
// Returning true causes offers.step() to delete the offer.
return true;
}
if (!static_cast<TDerived const*>(this)->checkQualityThreshold(offer.quality()))
return false;
auto const [ofrInRate, ofrOutRate] = offer.adjustRates(
static_cast<TDerived const*>(this)->getOfrInRate(prevStep_, offer.owner(), trIn),
static_cast<TDerived const*>(this)->getOfrOutRate(
prevStep_, offer.owner(), strandDst_, trOut));
static_cast<TDerived const*>(this)->getOfrInRate(prevStep_, owner, trIn),
static_cast<TDerived const*>(this)->getOfrOutRate(prevStep_, owner, strandDst_, trOut));
auto ofrAmt = offer.amount();
TAmounts stpAmt{mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true), ofrAmt.out};
@@ -756,6 +758,26 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
}
// Limit offer's input if MPT, BookStep is the first step (an issuer
// is making a cross-currency payment), and this offer is not owned
// by the issuer. Otherwise, OutstandingAmount may overflow.
auto const& issuer = assetIn.getIssuer();
if (isAssetInMPT && !prevStep_ && offer.owner() != issuer)
{
// Funds available to issue
auto const available = toAmount<TIn>(accountFunds(
sb,
issuer,
assetIn, // STAmount{0}, but the default is not used
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_));
if (stpAmt.in > available)
{
limitStepIn(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available);
}
}
offerAttempted = true;
return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate);
};
@@ -820,8 +842,8 @@ BookStep<TIn, TOut, TDerived>::consumeOffer(
// The offer owner gets the ofrAmt. The difference between ofrAmt and
// stepAmt is a transfer fee that goes to book_.in.account
{
auto const dr =
offer.send(sb, book_.in.account, offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_);
auto const dr = offer.send(
sb, book_.in.getIssuer(), offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_);
if (!isTesSuccess(dr))
Throw<FlowException>(dr);
}
@@ -829,10 +851,16 @@ BookStep<TIn, TOut, TDerived>::consumeOffer(
// The offer owner pays `ownerGives`. The difference between ownerGives and
// stepAmt is a transfer fee that goes to book_.out.account
{
auto const& issuer = book_.out.getIssuer();
auto const cr =
offer.send(sb, offer.owner(), book_.out.account, toSTAmount(ownerGives, book_.out), j_);
offer.send(sb, offer.owner(), issuer, toSTAmount(ownerGives, book_.out), j_);
if (!isTesSuccess(cr))
Throw<FlowException>(cr);
if constexpr (std::is_same_v<TOut, MPTAmount>)
{
if (offer.owner() == issuer)
issuerSelfDebitHookMPT(sb, book_.out.get<MPTIssue>(), ofrAmt.out.value());
}
}
offer.consume(sb, ofrAmt);
@@ -1260,20 +1288,20 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
// Do not allow two books to output the same issue. This may cause offers on
// one step to unfund offers in another step.
if (!ctx.seenBookOuts.insert(book_.out).second ||
(ctx.seenDirectIssues[0].count(book_.out) != 0u))
(ctx.seenDirectAssets[0].count(book_.out) != 0u))
{
JLOG(j_.debug()) << "BookStep: loop detected: " << *this;
return temBAD_PATH_LOOP;
}
if (ctx.seenDirectIssues[1].count(book_.out) != 0u)
if (ctx.seenDirectAssets[1].count(book_.out) != 0u)
{
JLOG(j_.debug()) << "BookStep: loop detected: " << *this;
return temBAD_PATH_LOOP;
}
auto issuerExists = [](ReadView const& view, Issue const& iss) -> bool {
return isXRP(iss.account) || view.read(keylet::account(iss.account));
auto issuerExists = [](ReadView const& view, Asset const& iss) -> bool {
return isXRP(iss.getIssuer()) || view.exists(keylet::account(iss.getIssuer()));
};
if (!issuerExists(ctx.view, book_.in) || !issuerExists(ctx.view, book_.out))
@@ -1287,19 +1315,101 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
if (auto const prev = ctx.prevStep->directStepSrcAcct())
{
auto const& view = ctx.view;
auto const& cur = book_.in.account;
auto const& cur = book_.in.getIssuer();
auto sle = view.read(keylet::line(*prev, cur, book_.in.currency));
if (!sle)
return terNO_LINE;
if (((*sle)[sfFlags] & ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) != 0u)
return terNO_RIPPLE;
auto const err = book_.in.visit(
[&](Issue const& issue) -> std::optional<TER> {
auto sle = view.read(keylet::line(*prev, cur, issue.currency));
if (!sle)
return terNO_LINE;
if (((*sle)[sfFlags] & ((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple)) !=
0u)
return terNO_RIPPLE;
return std::nullopt;
},
[&](MPTIssue const& issue) -> std::optional<TER> {
// Check if can trade on DEX.
if (auto const ter = canTrade(view, book_.in); !isTesSuccess(ter))
return ter;
if (auto const ter = canTrade(view, book_.out); !isTesSuccess(ter))
return ter;
return std::nullopt;
});
if (err)
return *err;
}
}
return tesSUCCESS;
}
template <class TIn, class TOut, class TDerived>
Rate
BookStep<TIn, TOut, TDerived>::rate(
ReadView const& view,
Asset const& asset,
AccountID const& dstAccount) const
{
auto const& issuer = asset.getIssuer();
if (isXRP(issuer) || issuer == dstAccount)
return parityRate;
return asset.visit(
[&](Issue const&) { return transferRate(view, issuer); },
[&](MPTIssue const& issue) { return transferRate(view, issue.getMptID()); });
};
template <class TIn, class TOut, class TDerived>
bool
BookStep<TIn, TOut, TDerived>::checkMPTDEX(ReadView const& view, AccountID const& owner) const
{
if (!isTesSuccess(canTrade(view, book_.in)) || !isTesSuccess(canTrade(view, book_.out)))
return false;
if (book_.in.holds<MPTIssue>())
{
auto ret = [&]() {
auto const& asset = book_.in;
// Strand's source is an issuer
if (!prevStep_)
return true;
// Offer's owner is an issuer
if (asset.getIssuer() == owner)
return true;
// The previous step could be MPTEndpointStep with non issuer account or
// BookStep. Fail both if in asset is locked. In the former case it is holder
// to locked holder transfer. In the latter case it is not possible to tell if
// it is issuer to holder or holder to holder transfer.
if (isFrozen(view, owner, book_.in.get<MPTIssue>()))
return false;
// Previous step is BookStep. BookStep only sends if CanTransfer is
// set and not locked or the offer is owned by an issuer
if (prevStep_->bookStepBook())
return true;
// Previous step is MPTEndpointStep and offer's owner is not an
// issuer
return isTesSuccess(canTransfer(view, asset, owner, owner));
}();
if (!ret)
return false;
}
if (book_.out.holds<MPTIssue>())
{
auto const& asset = book_.out;
// Last step if the strand's destination is an issuer
if (strandDeliver_ == asset && strandDst_ == asset.getIssuer())
return true;
// Offer's owner is an issuer
if (asset.getIssuer() == owner)
return true;
// Next step is BookStep and offer's owner is not an issuer.
return isTesSuccess(canTransfer(view, asset, owner, owner));
}
return true;
}
//------------------------------------------------------------------------------
namespace test {
@@ -1317,22 +1427,21 @@ equalHelper(Step const& step, xrpl::Book const& book)
bool
bookStepEqual(Step const& step, xrpl::Book const& book)
{
bool const inXRP = isXRP(book.in.currency);
bool const outXRP = isXRP(book.out.currency);
if (inXRP && outXRP)
if (isXRP(book.in) && isXRP(book.out))
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::test::bookStepEqual : no XRP to XRP book step");
return false; // no such thing as xrp/xrp book step
// LCOV_EXCL_STOP
}
if (inXRP && !outXRP)
return equalHelper<XRPAmount, IOUAmount, BookPaymentStep<XRPAmount, IOUAmount>>(step, book);
if (!inXRP && outXRP)
return equalHelper<IOUAmount, XRPAmount, BookPaymentStep<IOUAmount, XRPAmount>>(step, book);
if (!inXRP && !outXRP)
return equalHelper<IOUAmount, IOUAmount, BookPaymentStep<IOUAmount, IOUAmount>>(step, book);
return false;
return std::visit(
[&]<typename TIn, typename TOut>(TIn const&, TOut const&) {
using TIn_ = typename TIn::amount_type;
using TOut_ = typename TOut::amount_type;
return equalHelper<TIn_, TOut_, BookPaymentStep<TIn_, TOut_>>(step, book);
},
book.in.getAmountType(),
book.out.getAmountType());
}
} // namespace test
@@ -1340,7 +1449,7 @@ bookStepEqual(Step const& step, xrpl::Book const& book)
template <class TIn, class TOut>
static std::pair<TER, std::unique_ptr<Step>>
make_BookStepHelper(StrandContext const& ctx, Issue const& in, Issue const& out)
make_BookStepHelper(StrandContext const& ctx, Asset const& in, Asset const& out)
{
TER ter = tefINTERNAL;
std::unique_ptr<Step> r;
@@ -1380,4 +1489,35 @@ make_BookStepXI(StrandContext const& ctx, Issue const& out)
return make_BookStepHelper<XRPAmount, IOUAmount>(ctx, xrpIssue(), out);
}
// MPT's
std::pair<TER, std::unique_ptr<Step>>
make_BookStepMM(StrandContext const& ctx, MPTIssue const& in, MPTIssue const& out)
{
return make_BookStepHelper<MPTAmount, MPTAmount>(ctx, in, out);
}
std::pair<TER, std::unique_ptr<Step>>
make_BookStepMI(StrandContext const& ctx, MPTIssue const& in, Issue const& out)
{
return make_BookStepHelper<MPTAmount, IOUAmount>(ctx, in, out);
}
std::pair<TER, std::unique_ptr<Step>>
make_BookStepIM(StrandContext const& ctx, Issue const& in, MPTIssue const& out)
{
return make_BookStepHelper<IOUAmount, MPTAmount>(ctx, in, out);
}
std::pair<TER, std::unique_ptr<Step>>
make_BookStepMX(StrandContext const& ctx, MPTIssue const& in)
{
return make_BookStepHelper<MPTAmount, XRPAmount>(ctx, in, xrpIssue());
}
std::pair<TER, std::unique_ptr<Step>>
make_BookStepXM(StrandContext const& ctx, MPTIssue const& out)
{
return make_BookStepHelper<XRPAmount, MPTAmount>(ctx, xrpIssue(), out);
}
} // namespace xrpl

View File

@@ -671,7 +671,7 @@ DirectStepI<TDerived>::validFwd(PaymentSandbox& sb, ApplyView& afView, EitherAmo
auto const savCache = *cache_;
XRPL_ASSERT(!in.native, "xrpl::DirectStepI::validFwd : input is not XRP");
XRPL_ASSERT(in.holds<IOUAmount>(), "xrpl::DirectStepI::validFwd : input is IOU");
auto const [maxSrcToDst, srcDebtDir] =
static_cast<TDerived const*>(this)->maxFlow(sb, cache_->srcToDst);
@@ -680,7 +680,7 @@ DirectStepI<TDerived>::validFwd(PaymentSandbox& sb, ApplyView& afView, EitherAmo
try
{
boost::container::flat_set<uint256> dummy;
fwdImp(sb, afView, dummy, in.iou); // changes cache
fwdImp(sb, afView, dummy, in.get<IOUAmount>()); // changes cache
}
catch (FlowException const&)
{
@@ -857,13 +857,13 @@ DirectStepI<TDerived>::check(StrandContext const& ctx) const
// issue
if (auto book = ctx.prevStep->bookStepBook())
{
if (book->out != srcIssue)
if (book->out.get<Issue>() != srcIssue)
return temBAD_PATH_LOOP;
}
}
if (!ctx.seenDirectIssues[0].insert(srcIssue).second ||
!ctx.seenDirectIssues[1].insert(dstIssue).second)
if (!ctx.seenDirectAssets[0].insert(srcIssue).second ||
!ctx.seenDirectAssets[1].insert(dstIssue).second)
{
JLOG(j_.debug()) << "DirectStepI: loop detected: Index: " << ctx.strandSize << ' '
<< *this;

View File

@@ -12,7 +12,7 @@ namespace xrpl {
template <class FlowResult>
static auto
finishFlow(PaymentSandbox& sb, Issue const& srcIssue, Issue const& dstIssue, FlowResult&& f)
finishFlow(PaymentSandbox& sb, Asset const& srcAsset, Asset const& dstAsset, FlowResult&& f)
{
path::RippleCalc::Output result;
if (isTesSuccess(f.ter))
@@ -25,8 +25,8 @@ finishFlow(PaymentSandbox& sb, Issue const& srcIssue, Issue const& dstIssue, Flo
}
result.setResult(f.ter);
result.actualAmountIn = toSTAmount(f.in, srcIssue);
result.actualAmountOut = toSTAmount(f.out, dstIssue);
result.actualAmountIn = toSTAmount(f.in, srcAsset);
result.actualAmountOut = toSTAmount(f.out, dstAsset);
return result;
};
@@ -48,19 +48,23 @@ flow(
beast::Journal j,
path::detail::FlowDebugInfo* flowDebugInfo)
{
Issue const srcIssue = [&] {
Asset const srcAsset = [&]() -> Asset {
if (sendMax)
return sendMax->issue();
if (!isXRP(deliver.issue().currency))
return Issue(deliver.issue().currency, src);
return xrpIssue();
return sendMax->asset();
return deliver.asset().visit(
[&](Issue const& issue) -> Asset {
if (isXRP(issue))
return xrpIssue();
return Issue(issue.currency, src);
},
[&](MPTIssue const&) { return deliver.asset(); });
}();
Issue const dstIssue = deliver.issue();
Asset const dstAsset = deliver.asset();
std::optional<Issue> sendMaxIssue;
std::optional<Asset> sendMaxAsset;
if (sendMax)
sendMaxIssue = sendMax->issue();
sendMaxAsset = sendMax->asset();
AMMContext ammContext(src, false);
@@ -71,9 +75,9 @@ flow(
sb,
src,
dst,
dstIssue,
dstAsset,
limitQuality,
sendMaxIssue,
sendMaxAsset,
paths,
defaultPaths,
ownerPaysTransferFee,
@@ -93,8 +97,8 @@ flow(
if (j.trace())
{
j.trace() << "\nsrc: " << src << "\ndst: " << dst << "\nsrcIssue: " << srcIssue
<< "\ndstIssue: " << dstIssue;
j.trace() << "\nsrc: " << src << "\ndst: " << dst << "\nsrcAsset: " << srcAsset
<< "\ndstAsset: " << dstAsset;
j.trace() << "\nNumStrands: " << strands.size();
for (auto const& curStrand : strands)
{
@@ -106,87 +110,32 @@ flow(
}
}
bool const srcIsXRP = isXRP(srcIssue.currency);
bool const dstIsXRP = isXRP(dstIssue.currency);
auto const asDeliver = toAmountSpec(deliver);
// The src account may send either xrp or iou. The dst account may receive
// either xrp or iou. Since XRP and IOU amounts are represented by different
// types, use templates to tell `flow` about the amount types.
if (srcIsXRP && dstIsXRP)
{
return finishFlow(
sb,
srcIssue,
dstIssue,
flow<XRPAmount, XRPAmount>(
// The src account may send either xrp,iou,or mpt. The dst account may
// receive either xrp,iou, or mpt. Since XRP, IOU, and MPT amounts are
// represented by different types, use templates to tell `flow` about the
// amount types.
return std::visit(
[&, &strands_ = strands]<typename TIn, typename TOut>(TIn const&, TOut const&) {
using TIn_ = typename TIn::amount_type;
using TOut_ = typename TOut::amount_type;
return finishFlow(
sb,
strands,
asDeliver.xrp,
partialPayment,
offerCrossing,
limitQuality,
sendMax,
j,
ammContext,
flowDebugInfo));
}
if (srcIsXRP && !dstIsXRP)
{
return finishFlow(
sb,
srcIssue,
dstIssue,
flow<XRPAmount, IOUAmount>(
sb,
strands,
asDeliver.iou,
partialPayment,
offerCrossing,
limitQuality,
sendMax,
j,
ammContext,
flowDebugInfo));
}
if (!srcIsXRP && dstIsXRP)
{
return finishFlow(
sb,
srcIssue,
dstIssue,
flow<IOUAmount, XRPAmount>(
sb,
strands,
asDeliver.xrp,
partialPayment,
offerCrossing,
limitQuality,
sendMax,
j,
ammContext,
flowDebugInfo));
}
XRPL_ASSERT(!srcIsXRP && !dstIsXRP, "xrpl::flow : neither is XRP");
return finishFlow(
sb,
srcIssue,
dstIssue,
flow<IOUAmount, IOUAmount>(
sb,
strands,
asDeliver.iou,
partialPayment,
offerCrossing,
limitQuality,
sendMax,
j,
ammContext,
flowDebugInfo));
srcAsset,
dstAsset,
flow<TIn_, TOut_>(
sb,
strands_,
get<TOut_>(deliver),
partialPayment,
offerCrossing,
limitQuality,
sendMax,
j,
ammContext,
flowDebugInfo));
},
srcAsset.getAmountType(),
dstAsset.getAmountType());
}
} // namespace xrpl

View File

@@ -0,0 +1,919 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/tx/paths/detail/Steps.h>
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
#include <boost/container/flat_set.hpp>
#include <algorithm>
#include <sstream>
namespace xrpl {
template <class TDerived>
class MPTEndpointStep : public StepImp<MPTAmount, MPTAmount, MPTEndpointStep<TDerived>>
{
protected:
AccountID const src_;
AccountID const dst_;
MPTIssue const mptIssue_;
// Charge transfer fees when the prev step redeems
Step const* const prevStep_ = nullptr;
bool const isLast_;
// Direct payment between the holders
// Used by maxFlow's last step.
bool const isDirectBetweenHolders_ = false;
beast::Journal const j_;
struct Cache
{
MPTAmount in;
MPTAmount srcToDst;
MPTAmount out;
DebtDirection srcDebtDir;
Cache(
MPTAmount const& in_,
MPTAmount const& srcToDst_,
MPTAmount const& out_,
DebtDirection srcDebtDir_)
: in(in_), srcToDst(srcToDst_), out(out_), srcDebtDir(srcDebtDir_)
{
}
};
std::optional<Cache> cache_;
// Compute the maximum value that can flow from src->dst at
// the best available quality.
// return: first element is max amount that can flow,
// second is the debt direction of the source w.r.t. the dst
std::pair<MPTAmount, DebtDirection>
maxPaymentFlow(ReadView const& sb) const;
// Compute srcQOut and dstQIn when the source redeems.
std::pair<std::uint32_t, std::uint32_t>
qualitiesSrcRedeems(ReadView const& sb) const;
// Compute srcQOut and dstQIn when the source issues.
std::pair<std::uint32_t, std::uint32_t>
qualitiesSrcIssues(ReadView const& sb, DebtDirection prevStepDebtDirection) const;
// Returns srcQOut, dstQIn
std::pair<std::uint32_t, std::uint32_t>
qualities(ReadView const& sb, DebtDirection srcDebtDir, StrandDirection strandDir) const;
void
resetCache(DebtDirection dir);
private:
MPTEndpointStep(
StrandContext const& ctx,
AccountID const& src,
AccountID const& dst,
MPTID const& mpt)
: src_(src)
, dst_(dst)
, mptIssue_(mpt)
, prevStep_(ctx.prevStep)
, isLast_(ctx.isLast)
, isDirectBetweenHolders_(
mptIssue_ == ctx.strandDeliver && ctx.strandSrc != mptIssue_.getIssuer() &&
ctx.strandDst != mptIssue_.getIssuer() &&
(ctx.isFirst || (ctx.prevStep != nullptr && !ctx.prevStep->bookStepBook())))
, j_(ctx.j)
{
XRPL_ASSERT(
src_ == mptIssue_.getIssuer() || dst_ == mptIssue_.getIssuer(),
"MPTEndpointStep::MPTEndpointStep src or dst must be an issuer");
}
public:
AccountID const&
src() const
{
return src_;
}
AccountID const&
dst() const
{
return dst_;
}
MPTID const&
mptID() const
{
return mptIssue_.getMptID();
}
std::optional<EitherAmount>
cachedIn() const override
{
if (!cache_)
return std::nullopt;
return EitherAmount(cache_->in);
}
std::optional<EitherAmount>
cachedOut() const override
{
if (!cache_)
return std::nullopt;
return EitherAmount(cache_->out);
}
std::optional<AccountID>
directStepSrcAcct() const override
{
return src_;
}
std::optional<std::pair<AccountID, AccountID>>
directStepAccts() const override
{
return std::make_pair(src_, dst_);
}
DebtDirection
debtDirection(ReadView const& sb, StrandDirection dir) const override;
std::uint32_t
lineQualityIn(ReadView const& v) const override;
std::pair<std::optional<Quality>, DebtDirection>
qualityUpperBound(ReadView const& v, DebtDirection dir) const override;
std::pair<MPTAmount, MPTAmount>
revImp(
PaymentSandbox& sb,
ApplyView& afView,
boost::container::flat_set<uint256>& ofrsToRm,
MPTAmount const& out);
std::pair<MPTAmount, MPTAmount>
fwdImp(
PaymentSandbox& sb,
ApplyView& afView,
boost::container::flat_set<uint256>& ofrsToRm,
MPTAmount const& in);
std::pair<bool, EitherAmount>
validFwd(PaymentSandbox& sb, ApplyView& afView, EitherAmount const& in) override;
// Check for error, existing liquidity, and violations of auth/frozen
// constraints.
TER
check(StrandContext const& ctx) const;
void
setCacheLimiting(
MPTAmount const& fwdIn,
MPTAmount const& fwdSrcToDst,
MPTAmount const& fwdOut,
DebtDirection srcDebtDir);
friend bool
operator==(MPTEndpointStep const& lhs, MPTEndpointStep const& rhs)
{
return lhs.src_ == rhs.src_ && lhs.dst_ == rhs.dst_ && lhs.mptIssue_ == rhs.mptIssue_;
}
friend bool
operator!=(MPTEndpointStep const& lhs, MPTEndpointStep const& rhs)
{
return !(lhs == rhs);
}
protected:
std::string
logStringImpl(char const* name) const
{
std::ostringstream ostr;
ostr << name << ": "
<< "\nSrc: " << src_ << "\nDst: " << dst_;
return ostr.str();
}
private:
bool
equal(Step const& rhs) const override
{
if (auto ds = dynamic_cast<MPTEndpointStep const*>(&rhs))
{
return *this == *ds;
}
return false;
}
friend TDerived;
};
//------------------------------------------------------------------------------
// Flow is used in two different circumstances for transferring funds:
// o Payments, and
// o Offer crossing.
// The rules for handling funds in these two cases are almost, but not
// quite, the same.
// Payment MPTEndpointStep class (not offer crossing).
class MPTEndpointPaymentStep : public MPTEndpointStep<MPTEndpointPaymentStep>
{
public:
using MPTEndpointStep<MPTEndpointPaymentStep>::MPTEndpointStep;
using MPTEndpointStep<MPTEndpointPaymentStep>::check;
MPTEndpointPaymentStep(
StrandContext const& ctx,
AccountID const& src,
AccountID const& dst,
MPTID const& mpt)
: MPTEndpointStep<MPTEndpointPaymentStep>(ctx, src, dst, mpt)
{
}
static bool
verifyPrevStepDebtDirection(DebtDirection)
{
// A payment doesn't care regardless of prevStepRedeems.
return true;
}
// Verify the consistency of the step. These checks are specific to
// payments and assume that general checks were already performed.
TER
check(StrandContext const& ctx, std::shared_ptr<const SLE> const& sleSrc) const;
std::string
logString() const override
{
return logStringImpl("MPTEndpointPaymentStep");
}
// Not applicable for payment
static TER
checkCreateMPT(ApplyView&, DebtDirection)
{
return tesSUCCESS;
}
};
// Offer crossing MPTEndpointStep class (not a payment).
class MPTEndpointOfferCrossingStep : public MPTEndpointStep<MPTEndpointOfferCrossingStep>
{
public:
using MPTEndpointStep<MPTEndpointOfferCrossingStep>::MPTEndpointStep;
using MPTEndpointStep<MPTEndpointOfferCrossingStep>::check;
MPTEndpointOfferCrossingStep(
StrandContext const& ctx,
AccountID const& src,
AccountID const& dst,
MPTID const& mpt)
: MPTEndpointStep<MPTEndpointOfferCrossingStep>(ctx, src, dst, mpt)
{
}
static bool
verifyPrevStepDebtDirection(DebtDirection prevStepDir)
{
// During offer crossing we rely on the fact that prevStepRedeems
// will *always* issue. That's because:
// o If there's a prevStep_, it will always be a BookStep.
// o BookStep::debtDirection() always returns `issues` when offer
// crossing.
// An assert based on this return value will tell us if that
// behavior changes.
return issues(prevStepDir);
}
// Verify the consistency of the step. These checks are specific to
// offer crossing and assume that general checks were already performed.
static TER
check(StrandContext const& ctx, std::shared_ptr<const SLE> const& sleSrc);
std::string
logString() const override
{
return logStringImpl("MPTEndpointOfferCrossingStep");
}
// Can be created in rev or fwd (if limiting step) direction.
TER
checkCreateMPT(ApplyView& view, DebtDirection srcDebtDir);
};
//------------------------------------------------------------------------------
TER
MPTEndpointPaymentStep::check(StrandContext const& ctx, std::shared_ptr<const SLE> const& sleSrc)
const
{
// Since this is a payment, MPToken must be present. Perform all
// MPToken related checks.
// requireAuth checks if MPTIssuance exist. Note that issuer to issuer
// payment is invalid
auto const& issuer = mptIssue_.getIssuer();
if (src_ != issuer)
{
if (auto const ter = requireAuth(ctx.view, mptIssue_, src_); !isTesSuccess(ter))
return ter;
}
if (dst_ != issuer)
{
if (auto const ter = requireAuth(ctx.view, mptIssue_, dst_); !isTesSuccess(ter))
return ter;
}
// Direct MPT payment, no DEX
if (mptIssue_ == ctx.strandDeliver &&
(ctx.isFirst || (ctx.prevStep != nullptr && !ctx.prevStep->bookStepBook())))
{
// Between holders
if (isDirectBetweenHolders_)
{
auto const& holder = ctx.isFirst ? src_ : dst_;
// Payment between the holders
if (isFrozen(ctx.view, holder, mptIssue_))
return tecLOCKED;
if (auto const ter = canTransfer(ctx.view, mptIssue_, holder, ctx.strandDst);
!isTesSuccess(ter))
return ter;
}
// Don't need to check if a payment is between issuer and holder
// in either direction
}
// Cross-token MPT payment via DEX
else
{
if (auto const ter = canTrade(ctx.view, mptIssue_); !isTesSuccess(ter))
return ter;
}
// Can't check for creditBalance/Limit unless it's the first step.
// Otherwise, even if OutstandingAmount is equal to MaximumAmount
// a payment can still be successful. For instance, when a balance
// is shifted from one holder to another.
if (prevStep_ == nullptr)
{
auto const owed =
accountFunds(ctx.view, src_, mptIssue_, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
// Already at MaximumAmount
if (owed <= beast::zero)
return tecPATH_DRY;
}
return tesSUCCESS;
}
TER
MPTEndpointOfferCrossingStep::check(StrandContext const& ctx, std::shared_ptr<const SLE> const&)
{
return tesSUCCESS;
}
TER
MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirection srcDebtDir)
{
// TakerPays is the last step if offer crossing
if (isLast_)
{
// Create MPToken for the offer's owner. No need to check
// for the reserve since the offer doesn't go on the books
// if crossed. Insufficient reserve is allowed if the offer
// crossed. See CreateOffer::applyGuts() for reserve check.
if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, j_); !isTesSuccess(err))
{
JLOG(j_.trace()) << "MPTEndpointStep::checkCreateMPT: failed create MPT";
resetCache(srcDebtDir);
return err;
}
}
return tesSUCCESS;
}
//------------------------------------------------------------------------------
template <class TDerived>
std::pair<MPTAmount, DebtDirection>
MPTEndpointStep<TDerived>::maxPaymentFlow(ReadView const& sb) const
{
auto const maxFlow = accountFunds(sb, src_, mptIssue_, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
// From a holder to an issuer
if (src_ != mptIssue_.getIssuer())
return {toAmount<MPTAmount>(maxFlow), DebtDirection::redeems};
// From an issuer to a holder
if (auto const sle = sb.read(keylet::mptIssuance(mptIssue_)))
{
// If issuer is the source account, and it is direct payment then
// MPTEndpointStep is the only step. Provide available maxFlow.
if (prevStep_ == nullptr)
return {toAmount<MPTAmount>(maxFlow), DebtDirection::issues};
// MPTEndpointStep is the last step. It's always issuing in
// this case. Can't infer at this point what the maxFlow is, because
// the previous step may issue or redeem. Allow OutstandingAmount
// to temporarily overflow. Let the previous step figure out how
// to limit the flow.
std::int64_t const maxAmount = maxMPTAmount(*sle);
return {MPTAmount{maxAmount}, DebtDirection::issues};
}
return {MPTAmount{0}, DebtDirection::issues};
}
template <class TDerived>
DebtDirection
MPTEndpointStep<TDerived>::debtDirection(ReadView const& sb, StrandDirection dir) const
{
if (dir == StrandDirection::forward && cache_)
return cache_->srcDebtDir;
return (src_ == mptIssue_.getIssuer()) ? DebtDirection::issues : DebtDirection::redeems;
}
template <class TDerived>
std::pair<MPTAmount, MPTAmount>
MPTEndpointStep<TDerived>::revImp(
PaymentSandbox& sb,
ApplyView& /*afView*/,
boost::container::flat_set<uint256>& /*ofrsToRm*/,
MPTAmount const& out)
{
cache_.reset();
auto const [maxSrcToDst, srcDebtDir] = static_cast<TDerived const*>(this)->maxPaymentFlow(sb);
auto const [srcQOut, dstQIn] = qualities(sb, srcDebtDir, StrandDirection::reverse);
(void)dstQIn;
MPTIssue const srcToDstIss(mptIssue_);
JLOG(j_.trace()) << "MPTEndpointStep::rev"
<< " srcRedeems: " << redeems(srcDebtDir) << " outReq: " << to_string(out)
<< " maxSrcToDst: " << to_string(maxSrcToDst) << " srcQOut: " << srcQOut
<< " dstQIn: " << dstQIn;
if (maxSrcToDst.signum() <= 0)
{
JLOG(j_.trace()) << "MPTEndpointStep::rev: dry";
resetCache(srcDebtDir);
return {beast::zero, beast::zero};
}
if (auto const err = static_cast<TDerived*>(this)->checkCreateMPT(sb, srcDebtDir);
!isTesSuccess(err))
return {beast::zero, beast::zero};
// Don't have to factor in dstQIn since it is always QUALITY_ONE
MPTAmount const srcToDst = out;
if (srcToDst <= maxSrcToDst)
{
MPTAmount const in = mulRatio(srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
cache_.emplace(in, srcToDst, srcToDst, srcDebtDir);
auto const ter = directSendNoFee(
sb,
src_,
dst_,
toSTAmount(srcToDst, srcToDstIss),
/*checkIssuer*/ false,
j_);
if (!isTesSuccess(ter))
{
JLOG(j_.trace()) << "MPTEndpointStep::rev: error " << ter;
resetCache(srcDebtDir);
return {beast::zero, beast::zero};
}
JLOG(j_.trace()) << "MPTEndpointStep::rev: Non-limiting"
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
return {in, out};
}
// limiting node
MPTAmount const in = mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
// Don't have to factor in dsqQIn since it's always QUALITY_ONE
MPTAmount const actualOut = maxSrcToDst;
cache_.emplace(in, maxSrcToDst, actualOut, srcDebtDir);
auto const ter = directSendNoFee(
sb,
src_,
dst_,
toSTAmount(maxSrcToDst, srcToDstIss),
/*checkIssuer*/ false,
j_);
if (!isTesSuccess(ter))
{
JLOG(j_.trace()) << "MPTEndpointStep::rev: error " << ter;
resetCache(srcDebtDir);
return {beast::zero, beast::zero};
}
JLOG(j_.trace()) << "MPTEndpointStep::rev: Limiting"
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
<< " srcToDst: " << to_string(maxSrcToDst) << " out: " << to_string(out);
return {in, actualOut};
}
// The forward pass should never have more liquidity than the reverse
// pass. But sometimes rounding differences cause the forward pass to
// deliver more liquidity. Use the cached values from the reverse pass
// to prevent this.
template <class TDerived>
void
MPTEndpointStep<TDerived>::setCacheLimiting(
MPTAmount const& fwdIn,
MPTAmount const& fwdSrcToDst,
MPTAmount const& fwdOut,
DebtDirection srcDebtDir)
{
if (cache_->in < fwdIn)
{
MPTAmount const smallDiff(1);
auto const diff = fwdIn - cache_->in;
if (diff > smallDiff)
{
if (!cache_->in.value() ||
(Number(fwdIn.value()) / Number(cache_->in.value())) > Number(101, -2))
{
// Detect large diffs on forward pass so they may be
// investigated
JLOG(j_.warn()) << "MPTEndpointStep::fwd: setCacheLimiting"
<< " fwdIn: " << to_string(fwdIn)
<< " cacheIn: " << to_string(cache_->in)
<< " fwdSrcToDst: " << to_string(fwdSrcToDst)
<< " cacheSrcToDst: " << to_string(cache_->srcToDst)
<< " fwdOut: " << to_string(fwdOut)
<< " cacheOut: " << to_string(cache_->out);
cache_.emplace(fwdIn, fwdSrcToDst, fwdOut, srcDebtDir);
return;
}
}
}
cache_->in = fwdIn;
if (fwdSrcToDst < cache_->srcToDst)
cache_->srcToDst = fwdSrcToDst;
if (fwdOut < cache_->out)
cache_->out = fwdOut;
cache_->srcDebtDir = srcDebtDir;
};
template <class TDerived>
std::pair<MPTAmount, MPTAmount>
MPTEndpointStep<TDerived>::fwdImp(
PaymentSandbox& sb,
ApplyView& /*afView*/,
boost::container::flat_set<uint256>& /*ofrsToRm*/,
MPTAmount const& in)
{
XRPL_ASSERT(cache_, "MPTEndpointStep<TDerived>::fwdImp : valid cache");
auto const [maxSrcToDst, srcDebtDir] = static_cast<TDerived const*>(this)->maxPaymentFlow(sb);
auto const [srcQOut, dstQIn] = qualities(sb, srcDebtDir, StrandDirection::forward);
(void)dstQIn;
MPTIssue const srcToDstIss(mptIssue_);
JLOG(j_.trace()) << "MPTEndpointStep::fwd"
<< " srcRedeems: " << redeems(srcDebtDir) << " inReq: " << to_string(in)
<< " maxSrcToDst: " << to_string(maxSrcToDst) << " srcQOut: " << srcQOut
<< " dstQIn: " << dstQIn;
if (maxSrcToDst.signum() <= 0)
{
JLOG(j_.trace()) << "MPTEndpointStep::fwd: dry";
resetCache(srcDebtDir);
return {beast::zero, beast::zero};
}
if (auto const err = static_cast<TDerived*>(this)->checkCreateMPT(sb, srcDebtDir);
!isTesSuccess(err))
return {beast::zero, beast::zero};
MPTAmount const srcToDst = mulRatio(in, QUALITY_ONE, srcQOut, /*roundUp*/ false);
if (srcToDst <= maxSrcToDst)
{
// Don't have to factor in dstQIn since it's always QUALITY_ONE
MPTAmount const out = srcToDst;
setCacheLimiting(in, srcToDst, out, srcDebtDir);
auto const ter = directSendNoFee(
sb,
src_,
dst_,
toSTAmount(cache_->srcToDst, srcToDstIss),
/*checkIssuer*/ false,
j_);
if (!isTesSuccess(ter))
{
JLOG(j_.trace()) << "MPTEndpointStep::fwd: error " << ter;
resetCache(srcDebtDir);
return {beast::zero, beast::zero};
}
JLOG(j_.trace()) << "MPTEndpointStep::fwd: Non-limiting"
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
}
else
{
// limiting node
MPTAmount const actualIn = mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
// Don't have to factor in dstQIn since it's always QUALITY_ONE
MPTAmount const out = maxSrcToDst;
setCacheLimiting(actualIn, maxSrcToDst, out, srcDebtDir);
auto const ter = directSendNoFee(
sb,
src_,
dst_,
toSTAmount(cache_->srcToDst, srcToDstIss),
/*checkIssuer*/ false,
j_);
if (!isTesSuccess(ter))
{
JLOG(j_.trace()) << "MPTEndpointStep::fwd: error " << ter;
resetCache(srcDebtDir);
return {beast::zero, beast::zero};
}
JLOG(j_.trace()) << "MPTEndpointStep::fwd: Limiting"
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(actualIn)
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
}
return {cache_->in, cache_->out};
}
template <class TDerived>
std::pair<bool, EitherAmount>
MPTEndpointStep<TDerived>::validFwd(PaymentSandbox& sb, ApplyView& afView, EitherAmount const& in)
{
if (!cache_)
{
JLOG(j_.trace()) << "Expected valid cache in validFwd";
return {false, EitherAmount(MPTAmount(beast::zero))};
}
auto const savCache = *cache_;
XRPL_ASSERT(in.holds<MPTAmount>(), "MPTEndpoint<TDerived>::validFwd : is MPT");
auto const [maxSrcToDst, srcDebtDir] = static_cast<TDerived const*>(this)->maxPaymentFlow(sb);
(void)srcDebtDir;
try
{
boost::container::flat_set<uint256> dummy;
fwdImp(sb, afView, dummy, in.get<MPTAmount>()); // changes cache
}
catch (FlowException const&)
{
return {false, EitherAmount(MPTAmount(beast::zero))};
}
if (maxSrcToDst < cache_->srcToDst)
{
JLOG(j_.warn()) << "MPTEndpointStep: Strand re-execute check failed."
<< " Exceeded max src->dst limit"
<< " max src->dst: " << to_string(maxSrcToDst)
<< " actual src->dst: " << to_string(cache_->srcToDst);
return {false, EitherAmount(cache_->out)};
}
if (!(checkNear(savCache.in, cache_->in) && checkNear(savCache.out, cache_->out)))
{
JLOG(j_.warn()) << "MPTEndpointStep: Strand re-execute check failed."
<< " ExpectedIn: " << to_string(savCache.in)
<< " CachedIn: " << to_string(cache_->in)
<< " ExpectedOut: " << to_string(savCache.out)
<< " CachedOut: " << to_string(cache_->out);
return {false, EitherAmount(cache_->out)};
}
return {true, EitherAmount(cache_->out)};
}
// Returns srcQOut, dstQIn
template <class TDerived>
std::pair<std::uint32_t, std::uint32_t>
MPTEndpointStep<TDerived>::qualitiesSrcRedeems(ReadView const& sb) const
{
if (prevStep_ == nullptr)
return {QUALITY_ONE, QUALITY_ONE};
auto const prevStepQIn = prevStep_->lineQualityIn(sb);
// Unlike trustline MPT doesn't have line quality field
auto srcQOut = QUALITY_ONE;
srcQOut = std::max<std::uint32_t>(prevStepQIn, srcQOut);
return {srcQOut, QUALITY_ONE};
}
// Returns srcQOut, dstQIn
template <class TDerived>
std::pair<std::uint32_t, std::uint32_t>
MPTEndpointStep<TDerived>::qualitiesSrcIssues(
ReadView const& sb,
DebtDirection prevStepDebtDirection) const
{
// Charge a transfer rate when issuing and previous step redeems
XRPL_ASSERT(
static_cast<TDerived const*>(this)->verifyPrevStepDebtDirection(prevStepDebtDirection),
"MPTEndpointStep<TDerived>::qualitiesSrcIssues : verify prev step debt "
"direction");
std::uint32_t const srcQOut =
redeems(prevStepDebtDirection) ? transferRate(sb, mptIssue_.getMptID()).value : QUALITY_ONE;
// Unlike trustline, MPT doesn't have line quality field
return {srcQOut, QUALITY_ONE};
}
// Returns srcQOut, dstQIn
template <class TDerived>
std::pair<std::uint32_t, std::uint32_t>
MPTEndpointStep<TDerived>::qualities(
ReadView const& sb,
DebtDirection srcDebtDir,
StrandDirection strandDir) const
{
if (redeems(srcDebtDir))
{
return qualitiesSrcRedeems(sb);
}
auto const prevStepDebtDirection = [&] {
if (prevStep_ != nullptr)
return prevStep_->debtDirection(sb, strandDir);
return DebtDirection::issues;
}();
return qualitiesSrcIssues(sb, prevStepDebtDirection);
}
template <class TDerived>
std::uint32_t
MPTEndpointStep<TDerived>::lineQualityIn(ReadView const& v) const
{
// dst quality in
return QUALITY_ONE;
}
template <class TDerived>
std::pair<std::optional<Quality>, DebtDirection>
MPTEndpointStep<TDerived>::qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const
{
auto const dir = this->debtDirection(v, StrandDirection::forward);
auto const [srcQOut, dstQIn] =
redeems(dir) ? qualitiesSrcRedeems(v) : qualitiesSrcIssues(v, prevStepDir);
(void)dstQIn;
MPTIssue const iss{mptIssue_};
// Be careful not to switch the parameters to `getRate`. The
// `getRate(offerOut, offerIn)` function is usually used for offers. It
// returns offerIn/offerOut. For a direct step, the rate is srcQOut/dstQIn
// (Input*dstQIn/srcQOut = Output; So rate = srcQOut/dstQIn). Although the
// first parameter is called `offerOut`, it should take the `dstQIn`
// variable.
return {Quality(getRate(STAmount(iss, QUALITY_ONE), STAmount(iss, srcQOut))), dir};
}
template <class TDerived>
TER
MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
{
// The following checks apply for both payments and offer crossing.
if (!src_ || !dst_)
{
JLOG(j_.debug()) << "MPTEndpointStep: specified bad account.";
return temBAD_PATH;
}
if (src_ == dst_)
{
JLOG(j_.debug()) << "MPTEndpointStep: same src and dst.";
return temBAD_PATH;
}
auto const sleSrc = ctx.view.read(keylet::account(src_));
if (!sleSrc)
{
JLOG(j_.warn()) << "MPTEndpointStep: can't receive MPT from non-existent issuer: " << src_;
return terNO_ACCOUNT;
}
// pure issue/redeem can't be frozen (issuer/holder)
if (!(ctx.isLast && ctx.isFirst))
{
auto const& account = ctx.isFirst ? src_ : dst_;
if (isFrozen(ctx.view, account, mptIssue_))
return terLOCKED;
}
if (ctx.seenBookOuts.count(mptIssue_) > 0)
{
if (ctx.prevStep == nullptr)
{
UNREACHABLE(
"xrpl::MPTEndpointStep::check : prev seen book without a "
"prev step");
return temBAD_PATH_LOOP;
}
// This is OK if the previous step is a book step that outputs this
// issue
if (auto book = ctx.prevStep->bookStepBook())
{
if (book->out.get<MPTIssue>() != mptIssue_)
return temBAD_PATH_LOOP;
}
}
if ((ctx.isFirst && !ctx.seenDirectAssets[0].insert(mptIssue_).second) ||
(ctx.isLast && !ctx.seenDirectAssets[1].insert(mptIssue_).second))
{
JLOG(j_.debug()) << "MPTEndpointStep: loop detected: Index: " << ctx.strandSize << ' '
<< *this;
return temBAD_PATH_LOOP;
}
// MPT can only be an endpoint
if (!ctx.isLast && !ctx.isFirst)
{
JLOG(j_.warn()) << "MPTEndpointStep: MPT can only be an endpoint";
return temBAD_PATH;
}
auto const& issuer = mptIssue_.getIssuer();
if ((src_ != issuer && dst_ != issuer) || (src_ == issuer && dst_ == issuer))
{
JLOG(j_.warn()) << "MPTEndpointStep: invalid src/dst";
return temBAD_PATH;
}
return static_cast<TDerived const*>(this)->check(ctx, sleSrc);
}
template <class TDerived>
void
MPTEndpointStep<TDerived>::resetCache(xrpl::DebtDirection dir)
{
cache_.emplace(MPTAmount(beast::zero), MPTAmount(beast::zero), MPTAmount(beast::zero), dir);
}
//------------------------------------------------------------------------------
std::pair<TER, std::unique_ptr<Step>>
make_MPTEndpointStep(
StrandContext const& ctx,
AccountID const& src,
AccountID const& dst,
MPTID const& mpt)
{
TER ter = tefINTERNAL;
std::unique_ptr<Step> r;
if (ctx.offerCrossing != OfferCrossing::no)
{
auto offerCrossingStep = std::make_unique<MPTEndpointOfferCrossingStep>(ctx, src, dst, mpt);
ter = offerCrossingStep->check(ctx);
r = std::move(offerCrossingStep);
}
else // payment
{
auto paymentStep = std::make_unique<MPTEndpointPaymentStep>(ctx, src, dst, mpt);
ter = paymentStep->check(ctx);
r = std::move(paymentStep);
}
if (!isTesSuccess(ter))
return {ter, nullptr};
return {tesSUCCESS, std::move(r)};
}
namespace test {
// Needed for testing
bool
mptEndpointStepEqual(
Step const& step,
AccountID const& src,
AccountID const& dst,
MPTID const& mptid)
{
if (auto ds = dynamic_cast<MPTEndpointStep<MPTEndpointPaymentStep> const*>(&step))
{
return ds->src() == src && ds->dst() == dst && ds->mptID() == mptid;
}
return false;
}
} // namespace test
} // namespace xrpl

View File

@@ -1,5 +1,6 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/OfferHelpers.h>
#include <xrpl/ledger/helpers/PermissionedDEXHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -13,14 +14,15 @@ namespace {
bool
checkIssuers(ReadView const& view, Book const& book)
{
auto issuerExists = [](ReadView const& view, Issue const& iss) -> bool {
return isXRP(iss.account) || view.read(keylet::account(iss.account));
auto issuerExists = [](ReadView const& view, Asset const& asset) -> bool {
auto const& issuer = asset.getIssuer();
return isXRP(issuer) || view.exists(keylet::account(issuer));
};
return issuerExists(view, book.in) && issuerExists(view, book.out);
}
} // namespace
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
TOfferStreamBase<TIn, TOut>::TOfferStreamBase(
ApplyView& view,
ApplyView& cancelView,
@@ -42,7 +44,7 @@ TOfferStreamBase<TIn, TOut>::TOfferStreamBase(
// Handle the case where a directory item with no corresponding ledger entry
// is found. This shouldn't happen but if it does we clean it up.
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
void
TOfferStreamBase<TIn, TOut>::erase(ApplyView& view)
{
@@ -75,67 +77,42 @@ TOfferStreamBase<TIn, TOut>::erase(ApplyView& view)
<< tip_.dir();
}
static STAmount
template <StepAmount T>
static T
accountFundsHelper(
ReadView const& view,
AccountID const& id,
STAmount const& saDefault,
Issue const&,
T const& amtDefault,
Asset const& asset,
FreezeHandling freezeHandling,
AuthHandling authHandling,
beast::Journal j)
{
return accountFunds(view, id, saDefault, freezeHandling, j);
}
static IOUAmount
accountFundsHelper(
ReadView const& view,
AccountID const& id,
IOUAmount const& amtDefault,
Issue const& issue,
FreezeHandling freezeHandling,
beast::Journal j)
{
if (issue.account == id)
if constexpr (std::is_same_v<T, IOUAmount>)
{
// self funded
return amtDefault;
if (id == asset.getIssuer())
{
// self funded
return amtDefault;
}
}
else if constexpr (std::is_same_v<T, MPTAmount>)
{
if (id == asset.getIssuer())
{
return toAmount<T>(issuerFundsToSelfIssue(view, asset.get<MPTIssue>()));
}
}
return toAmount<IOUAmount>(
accountHolds(view, id, issue.currency, issue.account, freezeHandling, j));
return toAmount<T>(accountHolds(view, id, asset, freezeHandling, authHandling, j));
}
static XRPAmount
accountFundsHelper(
ReadView const& view,
AccountID const& id,
XRPAmount const& amtDefault,
Issue const& issue,
FreezeHandling freezeHandling,
beast::Journal j)
{
return toAmount<XRPAmount>(
accountHolds(view, id, issue.currency, issue.account, freezeHandling, j));
}
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
template <class TTakerPays, class TTakerGets>
requires ValidTaker<TTakerPays, TTakerGets>
bool
TOfferStreamBase<TIn, TOut>::shouldRmSmallIncreasedQOffer() const
{
static_assert(
std::is_same_v<TTakerPays, IOUAmount> || std::is_same_v<TTakerPays, XRPAmount>,
"STAmount is not supported");
static_assert(
std::is_same_v<TTakerGets, IOUAmount> || std::is_same_v<TTakerGets, XRPAmount>,
"STAmount is not supported");
static_assert(
!std::is_same_v<TTakerPays, XRPAmount> || !std::is_same_v<TTakerGets, XRPAmount>,
"Cannot have XRP/XRP offers");
// Consider removing the offer if:
// o `TakerPays` is XRP (because of XRP drops granularity) or
// o `TakerPays` and `TakerGets` are both IOU and `TakerPays`<`TakerGets`
@@ -156,14 +133,14 @@ TOfferStreamBase<TIn, TOut>::shouldRmSmallIncreasedQOffer() const
if constexpr (!inIsXRP && !outIsXRP)
{
if (ofrAmts.in >= ofrAmts.out)
if (Number(ofrAmts.in) >= Number(ofrAmts.out))
return false;
}
TTakerGets const ownerFunds = toAmount<TTakerGets>(*ownerFunds_);
auto const effectiveAmounts = [&] {
if (offer_.owner() != offer_.issueOut().account && ownerFunds < ofrAmts.out)
if (offer_.owner() != offer_.assetOut().getIssuer() && ownerFunds < ofrAmts.out)
{
// adjust the amounts by owner funds.
//
@@ -185,7 +162,7 @@ TOfferStreamBase<TIn, TOut>::shouldRmSmallIncreasedQOffer() const
return effectiveQuality < offer_.quality();
}
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
bool
TOfferStreamBase<TIn, TOut>::step()
{
@@ -240,9 +217,7 @@ TOfferStreamBase<TIn, TOut>::step()
continue;
}
bool const deepFrozen = isDeepFrozen(
view_, offer_.owner(), offer_.issueIn().currency, offer_.issueIn().account);
if (deepFrozen)
if (isDeepFrozen(view_, offer_.owner(), offer_.assetIn()))
{
JLOG(j_.trace()) << "Removing deep frozen unfunded offer " << entry->key();
permRmOffer(entry->key());
@@ -262,7 +237,13 @@ TOfferStreamBase<TIn, TOut>::step()
// Calculate owner funds
ownerFunds_ = accountFundsHelper(
view_, offer_.owner(), amount.out, offer_.issueOut(), fhZERO_IF_FROZEN, j_);
view_,
offer_.owner(),
amount.out,
offer_.assetOut(),
fhZERO_IF_FROZEN,
ahZERO_IF_UNAUTHORIZED,
j_);
// Check for unfunded offer
if (*ownerFunds_ <= beast::zero)
@@ -271,7 +252,13 @@ TOfferStreamBase<TIn, TOut>::step()
// we haven't modified the balance and therefore the
// offer is "found unfunded" versus "became unfunded"
auto const original_funds = accountFundsHelper(
cancelView_, offer_.owner(), amount.out, offer_.issueOut(), fhZERO_IF_FROZEN, j_);
cancelView_,
offer_.owner(),
amount.out,
offer_.assetOut(),
fhZERO_IF_FROZEN,
ahZERO_IF_UNAUTHORIZED,
j_);
if (original_funds == *ownerFunds_)
{
@@ -287,44 +274,16 @@ TOfferStreamBase<TIn, TOut>::step()
continue;
}
bool const rmSmallIncreasedQOffer = [&] {
bool const inIsXRP = isXRP(offer_.issueIn());
bool const outIsXRP = isXRP(offer_.issueOut());
if (inIsXRP && !outIsXRP)
{
// Without the `if constexpr`, the
// `shouldRmSmallIncreasedQOffer` template will be instantiated
// even if it is never used. This can cause compiler errors in
// some cases, hence the `if constexpr` guard.
// Note that TIn can be XRPAmount or STAmount, and TOut can be
// IOUAmount or STAmount.
if constexpr (!(std::is_same_v<TIn, IOUAmount> || std::is_same_v<TOut, XRPAmount>))
return shouldRmSmallIncreasedQOffer<XRPAmount, IOUAmount>();
}
if (!inIsXRP && outIsXRP)
{
// See comment above for `if constexpr` rationale
if constexpr (!(std::is_same_v<TIn, XRPAmount> || std::is_same_v<TOut, IOUAmount>))
return shouldRmSmallIncreasedQOffer<IOUAmount, XRPAmount>();
}
if (!inIsXRP && !outIsXRP)
{
// See comment above for `if constexpr` rationale
if constexpr (!(std::is_same_v<TIn, XRPAmount> || std::is_same_v<TOut, XRPAmount>))
return shouldRmSmallIncreasedQOffer<IOUAmount, IOUAmount>();
}
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::TOfferStreamBase::step::rmSmallIncreasedQOffer : XRP "
"vs XRP offer");
return false;
// LCOV_EXCL_STOP
}();
if (rmSmallIncreasedQOffer)
if (shouldRmSmallIncreasedQOffer<TIn, TOut>())
{
auto const original_funds = accountFundsHelper(
cancelView_, offer_.owner(), amount.out, offer_.issueOut(), fhZERO_IF_FROZEN, j_);
cancelView_,
offer_.owner(),
amount.out,
offer_.assetOut(),
fhZERO_IF_FROZEN,
ahZERO_IF_UNAUTHORIZED,
j_);
if (original_funds == *ownerFunds_)
{
@@ -348,26 +307,28 @@ TOfferStreamBase<TIn, TOut>::step()
return true;
}
void
OfferStream::permRmOffer(uint256 const& offerIndex)
{
offerDelete(cancelView_, cancelView_.peek(keylet::offer(offerIndex)), j_);
}
template <class TIn, class TOut>
template <StepAmount TIn, StepAmount TOut>
void
FlowOfferStream<TIn, TOut>::permRmOffer(uint256 const& offerIndex)
{
permToRemove_.insert(offerIndex);
}
template class FlowOfferStream<STAmount, STAmount>;
template class FlowOfferStream<IOUAmount, IOUAmount>;
template class FlowOfferStream<XRPAmount, IOUAmount>;
template class FlowOfferStream<IOUAmount, XRPAmount>;
template class FlowOfferStream<MPTAmount, MPTAmount>;
template class FlowOfferStream<XRPAmount, MPTAmount>;
template class FlowOfferStream<MPTAmount, XRPAmount>;
template class FlowOfferStream<IOUAmount, MPTAmount>;
template class FlowOfferStream<MPTAmount, IOUAmount>;
template class TOfferStreamBase<STAmount, STAmount>;
template class TOfferStreamBase<IOUAmount, IOUAmount>;
template class TOfferStreamBase<XRPAmount, IOUAmount>;
template class TOfferStreamBase<IOUAmount, XRPAmount>;
template class TOfferStreamBase<MPTAmount, MPTAmount>;
template class TOfferStreamBase<XRPAmount, MPTAmount>;
template class TOfferStreamBase<MPTAmount, XRPAmount>;
template class TOfferStreamBase<IOUAmount, MPTAmount>;
template class TOfferStreamBase<MPTAmount, IOUAmount>;
} // namespace xrpl

View File

@@ -32,12 +32,6 @@ checkNear(IOUAmount const& expected, IOUAmount const& actual)
return r <= ratTol;
};
bool
checkNear(XRPAmount const& expected, XRPAmount const& actual)
{
return expected == actual;
};
static bool
isXRPAccount(STPathElement const& pe)
{
@@ -51,12 +45,12 @@ toStep(
StrandContext const& ctx,
STPathElement const* e1,
STPathElement const* e2,
Issue const& curIssue)
Asset const& curAsset)
{
auto& j = ctx.j;
if (ctx.isFirst && e1->isAccount() &&
((e1->getNodeType() & STPathElement::typeCurrency) != 0u) && isXRP(e1->getCurrency()))
((e1->getNodeType() & STPathElement::typeCurrency) != 0u) && e1->getPathAsset().isXRP())
{
return make_XRPEndpointStep(ctx, e1->getAccountID());
}
@@ -64,9 +58,32 @@ toStep(
if (ctx.isLast && isXRPAccount(*e1) && e2->isAccount())
return make_XRPEndpointStep(ctx, e2->getAccountID());
// MPTEndpointStep is created in following cases:
// 1 Direct payment between an issuer and a holder
// e1 is issuer and e2 is holder or vise versa
// There is only one step in this case: holder->issuer or
// issuer->holder
// 2 Direct payment between the holders
// e1 is issuer and e2 is holder or vise versa
// There are two steps in this case: holder->issuer->holder1
// 3 Cross-token payment with Amount or SendMax or both MPT
// If destination is an issuer then the last step is BookStep,
// otherwise the last step is MPTEndpointStep where e1 is
// the issuer and e2 is the holder.
// In all cases MPTEndpointStep is always first or last step,
// e1/e2 are always account types, and curAsset is always MPT.
if (e1->isAccount() && e2->isAccount())
{
return make_DirectStepI(ctx, e1->getAccountID(), e2->getAccountID(), curIssue.currency);
return curAsset.visit(
[&](MPTIssue const& issue) {
return make_MPTEndpointStep(
ctx, e1->getAccountID(), e2->getAccountID(), issue.getMptID());
},
[&](Issue const& issue) {
return make_DirectStepI(
ctx, e1->getAccountID(), e2->getAccountID(), issue.currency);
});
}
if (e1->isOffer() && e2->isAccount())
@@ -80,17 +97,16 @@ toStep(
}
XRPL_ASSERT(
(e2->getNodeType() & STPathElement::typeCurrency) ||
(e2->getNodeType() & STPathElement::typeAsset) ||
(e2->getNodeType() & STPathElement::typeIssuer),
"xrpl::toStep : currency or issuer");
auto const outCurrency = ((e2->getNodeType() & STPathElement::typeCurrency) != 0u)
? e2->getCurrency()
: curIssue.currency;
PathAsset const outAsset =
((e2->getNodeType() & STPathElement::typeAsset) != 0u) ? e2->getPathAsset() : curAsset;
auto const outIssuer = ((e2->getNodeType() & STPathElement::typeIssuer) != 0u)
? e2->getIssuerID()
: curIssue.account;
: curAsset.getIssuer();
if (isXRP(curIssue.currency) && isXRP(outCurrency))
if (isXRP(curAsset) && outAsset.isXRP())
{
JLOG(j.info()) << "Found xrp/xrp offer payment step";
return {temBAD_PATH, std::unique_ptr<Step>{}};
@@ -98,13 +114,35 @@ toStep(
XRPL_ASSERT(e2->isOffer(), "xrpl::toStep : is offer");
if (isXRP(outCurrency))
return make_BookStepIX(ctx, curIssue);
if (outAsset.isXRP())
{
return curAsset.visit(
[&](MPTIssue const& issue) { return make_BookStepMX(ctx, issue); },
[&](Issue const& issue) { return make_BookStepIX(ctx, issue); });
}
if (isXRP(curIssue.currency))
return make_BookStepXI(ctx, {outCurrency, outIssuer});
if (isXRP(curAsset))
{
return outAsset.visit(
[&](MPTID const& mpt) { return make_BookStepXM(ctx, mpt); },
[&](Currency const& currency) { return make_BookStepXI(ctx, {currency, outIssuer}); });
}
return make_BookStepII(ctx, curIssue, {outCurrency, outIssuer});
return curAsset.visit(
[&](MPTIssue const& issue) {
return outAsset.visit(
[&](Currency const& currency) {
return make_BookStepMI(ctx, issue, {currency, outIssuer});
},
[&](MPTID const& mpt) { return make_BookStepMM(ctx, issue, mpt); });
},
[&](Issue const& issue) {
return outAsset.visit(
[&](MPTID const& mpt) { return make_BookStepIM(ctx, issue, mpt); },
[&](Currency const& currency) {
return make_BookStepII(ctx, issue, {currency, outIssuer});
});
});
}
std::pair<TER, Strand>
@@ -112,9 +150,9 @@ toStrand(
ReadView const& view,
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
Asset const& deliver,
std::optional<Quality> const& limitQuality,
std::optional<Issue> const& sendMaxIssue,
std::optional<Asset> const& sendMaxAsset,
STPath const& path,
bool ownerPaysTransferFee,
OfferCrossing offerCrossing,
@@ -123,15 +161,21 @@ toStrand(
beast::Journal j)
{
if (isXRP(src) || isXRP(dst) || !isConsistent(deliver) ||
(sendMaxIssue && !isConsistent(*sendMaxIssue)))
(sendMaxAsset && !isConsistent(*sendMaxAsset)))
return {temBAD_PATH, Strand{}};
if ((sendMaxIssue && sendMaxIssue->account == noAccount()) || (src == noAccount()) ||
(dst == noAccount()) || (deliver.account == noAccount()))
if ((sendMaxAsset && sendMaxAsset->getIssuer() == noAccount()) || (src == noAccount()) ||
(dst == noAccount()) || (deliver.getIssuer() == noAccount()))
return {temBAD_PATH, Strand{}};
for (auto const& pe : path)
if ((deliver.holds<MPTIssue>() && deliver.getIssuer() == beast::zero) ||
(sendMaxAsset && sendMaxAsset->holds<MPTIssue>() &&
sendMaxAsset->getIssuer() == beast::zero))
return {temBAD_PATH, Strand{}};
for (std::size_t i = 0; i < path.size(); ++i)
{
auto const& pe = path[i];
auto const t = pe.getNodeType();
if (((t & ~STPathElement::typeAll) != 0u) || (t == 0u))
@@ -140,6 +184,8 @@ toStrand(
bool const hasAccount = (t & STPathElement::typeAccount) != 0u;
bool const hasIssuer = (t & STPathElement::typeIssuer) != 0u;
bool const hasCurrency = (t & STPathElement::typeCurrency) != 0u;
bool const hasMPT = (t & STPathElement::typeMPT) != 0u;
bool const hasAsset = (t & STPathElement::typeAsset) != 0u;
if (hasAccount && (hasIssuer || hasCurrency))
return {temBAD_PATH, Strand{}};
@@ -158,17 +204,33 @@ toStrand(
if (hasAccount && (pe.getAccountID() == noAccount()))
return {temBAD_PATH, Strand{}};
if (hasMPT && (hasCurrency || hasAccount))
return {temBAD_PATH, Strand{}};
if (hasMPT && hasIssuer && (pe.getIssuerID() != getMPTIssuer(pe.getMPTID())))
return {temBAD_PATH, Strand{}};
// No rippling if MPT
if (i > 0 && path[i - 1].hasMPT() && (hasAccount || (hasIssuer && !hasAsset)))
return {temBAD_PATH, Strand{}};
}
Issue curIssue = [&] {
auto const& currency = sendMaxIssue ? sendMaxIssue->currency : deliver.currency;
if (isXRP(currency))
return xrpIssue();
return Issue{currency, src};
Asset curAsset = [&]() -> Asset {
auto const& asset = sendMaxAsset ? *sendMaxAsset : deliver;
return asset.visit(
[&](MPTIssue const& issue) -> Asset { return asset; },
[&](Issue const& issue) -> Asset {
if (isXRP(asset))
return xrpIssue();
// First step ripples from the source to the issuer.
return Issue{issue.currency, src};
});
}();
auto hasCurrency = [](STPathElement const pe) {
return pe.getNodeType() & STPathElement::typeCurrency;
// Currency or MPT
auto hasAsset = [](STPathElement const pe) {
return pe.getNodeType() & STPathElement::typeAsset;
};
std::vector<STPathElement> normPath;
@@ -176,13 +238,28 @@ toStrand(
// sendmax and deliver.
normPath.reserve(4 + path.size());
{
normPath.emplace_back(STPathElement::typeAll, src, curIssue.currency, curIssue.account);
// The first step of a path is always implied to be the sender of the
// transaction, as defined by the transaction's Account field. The Asset
// is either SendMax or Deliver.
auto const t = [&]() {
auto const t = STPathElement::typeAccount | STPathElement::typeIssuer;
return curAsset.visit(
[&](MPTIssue const&) { return t | STPathElement::typeMPT; },
[&](Issue const&) { return t | STPathElement::typeCurrency; });
}();
// If MPT then the issuer is the actual issuer, it is never the source
// account.
normPath.emplace_back(t, src, curAsset, curAsset.getIssuer());
if (sendMaxIssue && sendMaxIssue->account != src &&
// If transaction includes SendMax with the issuer, which is not
// the sender of the transaction, that issuer is implied to be
// the second step of the path. Unless the path starts at an address,
// which is the issuer of SendMax.
if (sendMaxAsset && sendMaxAsset->getIssuer() != src &&
(path.empty() || !path[0].isAccount() ||
path[0].getAccountID() != sendMaxIssue->account))
path[0].getAccountID() != sendMaxAsset->getIssuer()))
{
normPath.emplace_back(sendMaxIssue->account, std::nullopt, std::nullopt);
normPath.emplace_back(sendMaxAsset->getIssuer(), std::nullopt, std::nullopt);
}
for (auto const& i : path)
@@ -190,22 +267,32 @@ toStrand(
{
// Note that for offer crossing (only) we do use an offer book
// even if all that is changing is the Issue.account.
STPathElement const& lastCurrency =
*std::find_if(normPath.rbegin(), normPath.rend(), hasCurrency);
if ((lastCurrency.getCurrency() != deliver.currency) ||
((offerCrossing != 0u) && lastCurrency.getIssuerID() != deliver.account))
// even if all that is changing is the Issue.account. Note
// that MPTIssue can't change the account.
STPathElement const& lastAsset =
*std::find_if(normPath.rbegin(), normPath.rend(), hasAsset);
if (lastAsset.getPathAsset() != deliver ||
(offerCrossing != OfferCrossing::no &&
lastAsset.getIssuerID() != deliver.getIssuer()))
{
normPath.emplace_back(std::nullopt, deliver.currency, deliver.account);
normPath.emplace_back(std::nullopt, deliver, deliver.getIssuer());
}
}
if (!((normPath.back().isAccount() && normPath.back().getAccountID() == deliver.account) ||
(dst == deliver.account)))
// If the Amount field of the transaction includes an issuer that is not
// the same as the Destination of the transaction, that issuer is
// implied to be the second-to-last step of the path. If normPath.back
// is an offer, which sells MPT then the added path element account is
// the MPT's issuer.
if (!((normPath.back().isAccount() &&
normPath.back().getAccountID() == deliver.getIssuer()) ||
(dst == deliver.getIssuer())))
{
normPath.emplace_back(deliver.account, std::nullopt, std::nullopt);
normPath.emplace_back(deliver.getIssuer(), std::nullopt, std::nullopt);
}
// Last step of a path is always implied to be the receiver of a
// transaction, as defined by the transaction's Destination field.
if (!normPath.back().isAccount() || normPath.back().getAccountID() != dst)
{
normPath.emplace_back(dst, std::nullopt, std::nullopt);
@@ -227,11 +314,11 @@ toStrand(
at most twice: once as a src and once as a dst (hence the two element
array). The strandSrc and strandDst will only show up once each.
*/
std::array<boost::container::flat_set<Issue>, 2> seenDirectIssues;
std::array<boost::container::flat_set<Asset>, 2> seenDirectAssets;
// A strand may not include the same offer book more than once
boost::container::flat_set<Issue> seenBookOuts;
seenDirectIssues[0].reserve(normPath.size());
seenDirectIssues[1].reserve(normPath.size());
boost::container::flat_set<Asset> seenBookOuts;
seenDirectAssets[0].reserve(normPath.size());
seenDirectAssets[1].reserve(normPath.size());
seenBookOuts.reserve(normPath.size());
auto ctx = [&](bool isLast = false) {
return StrandContext{
@@ -245,7 +332,7 @@ toStrand(
ownerPaysTransferFee,
offerCrossing,
isDefaultPath,
seenDirectIssues,
seenDirectAssets,
seenBookOuts,
ammContext,
domainID,
@@ -265,58 +352,96 @@ toStrand(
auto cur = &normPath[i];
auto const next = &normPath[i + 1];
if (cur->isAccount())
// Switch over from MPT to Currency. In this case curAsset account
// can be different from the issuer. If cur is MPT then curAsset
// is just set to MPTID.
if (curAsset.holds<MPTIssue>() && cur->hasCurrency())
{
curIssue.account = cur->getAccountID();
}
else if (cur->hasIssuer())
{
curIssue.account = cur->getIssuerID();
curAsset = Issue{};
}
// Can only update the account for Issue since MPTIssue's account
// is immutable as it is part of MPTID.
curAsset.visit(
[&](Issue const&) {
if (cur->isAccount())
{
curAsset.get<Issue>().account = cur->getAccountID();
}
else if (cur->hasIssuer())
{
curAsset.get<Issue>().account = cur->getIssuerID();
}
},
[](MPTIssue const&) {});
if (cur->hasCurrency())
{
curIssue.currency = cur->getCurrency();
if (isXRP(curIssue.currency))
curIssue.account = xrpAccount();
curAsset = Issue{cur->getCurrency(), curAsset.getIssuer()};
if (isXRP(curAsset))
curAsset.get<Issue>().account = xrpAccount();
}
else if (cur->hasMPT())
{
curAsset = cur->getPathAsset().get<MPTID>();
}
using ImpliedStepRet = std::pair<TER, std::unique_ptr<Step>>;
auto getImpliedStep = [&](AccountID const& src_,
AccountID const& dst_,
Asset const& asset_) -> ImpliedStepRet {
return asset_.visit(
[&](MPTIssue const&) -> ImpliedStepRet {
JLOG(j.error()) << "MPT is invalid with rippling";
return {temBAD_PATH, nullptr};
},
[&](Issue const& issue) -> ImpliedStepRet {
return make_DirectStepI(ctx(), src_, dst_, issue.currency);
});
};
if (cur->isAccount() && next->isAccount())
{
if (!isXRP(curIssue.currency) && curIssue.account != cur->getAccountID() &&
curIssue.account != next->getAccountID())
// This block doesn't execute
// since curAsset's account is set to cur's account above.
// It should not execute for MPT either because MPT rippling
// is invalid. Should this block be removed/amendment excluded?
if (!isXRP(curAsset) && curAsset.getIssuer() != cur->getAccountID() &&
curAsset.getIssuer() != next->getAccountID())
{
JLOG(j.trace()) << "Inserting implied account";
auto msr = make_DirectStepI(
ctx(), cur->getAccountID(), curIssue.account, curIssue.currency);
auto msr = getImpliedStep(cur->getAccountID(), curAsset.getIssuer(), curAsset);
if (!isTesSuccess(msr.first))
return {msr.first, Strand{}};
result.push_back(std::move(msr.second));
impliedPE.emplace(
STPathElement::typeAccount, curIssue.account, xrpCurrency(), xrpAccount());
STPathElement::typeAccount, curAsset.getIssuer(), xrpCurrency(), xrpAccount());
cur = &*impliedPE;
}
}
else if (cur->isAccount() && next->isOffer())
{
if (curIssue.account != cur->getAccountID())
// Same as above, this block doesn't execute.
if (curAsset.getIssuer() != cur->getAccountID())
{
JLOG(j.trace()) << "Inserting implied account before offer";
auto msr = make_DirectStepI(
ctx(), cur->getAccountID(), curIssue.account, curIssue.currency);
auto msr = getImpliedStep(cur->getAccountID(), curAsset.getIssuer(), curAsset);
if (!isTesSuccess(msr.first))
return {msr.first, Strand{}};
result.push_back(std::move(msr.second));
impliedPE.emplace(
STPathElement::typeAccount, curIssue.account, xrpCurrency(), xrpAccount());
STPathElement::typeAccount, curAsset.getIssuer(), xrpCurrency(), xrpAccount());
cur = &*impliedPE;
}
}
else if (cur->isOffer() && next->isAccount())
{
if (curIssue.account != next->getAccountID() && !isXRP(next->getAccountID()))
// If the offer sells MPT, then next's account is always the issuer.
// See how normPath step is added for second-to-last or last
// step. Therefore, this block never executes if MPT.
if (curAsset.getIssuer() != next->getAccountID() && !isXRP(next->getAccountID()))
{
if (isXRP(curIssue))
if (isXRP(curAsset))
{
if (i != normPath.size() - 2)
return {temBAD_PATH, Strand{}};
@@ -330,8 +455,7 @@ toStrand(
else
{
JLOG(j.trace()) << "Inserting implied account after offer";
auto msr = make_DirectStepI(
ctx(), curIssue.account, next->getAccountID(), curIssue.currency);
auto msr = getImpliedStep(curAsset.getIssuer(), next->getAccountID(), curAsset);
if (!isTesSuccess(msr.first))
return {msr.first, Strand{}};
result.push_back(std::move(msr.second));
@@ -340,7 +464,7 @@ toStrand(
continue;
}
if (!next->isOffer() && next->hasCurrency() && next->getCurrency() != curIssue.currency)
if (!next->isOffer() && next->hasAsset() && next->getPathAsset() != curAsset)
{
// Should never happen
// LCOV_EXCL_START
@@ -349,7 +473,7 @@ toStrand(
// LCOV_EXCL_STOP
}
auto s = toStep(ctx(/*isLast*/ i == normPath.size() - 2), cur, next, curIssue);
auto s = toStep(ctx(/*isLast*/ i == normPath.size() - 2), cur, next, curAsset);
if (isTesSuccess(s.first))
{
result.emplace_back(std::move(s.second));
@@ -366,17 +490,21 @@ toStrand(
if (auto r = s.directStepAccts())
return *r;
if (auto const r = s.bookStepBook())
return std::make_pair(r->in.account, r->out.account);
return std::make_pair(r->in.getIssuer(), r->out.getIssuer());
Throw<FlowException>(tefEXCEPTION, "Step should be either a direct or book step");
return std::make_pair(xrpAccount(), xrpAccount());
};
auto curAcc = src;
auto curIss = [&] {
auto& currency = sendMaxIssue ? sendMaxIssue->currency : deliver.currency;
if (isXRP(currency))
return xrpIssue();
return Issue{currency, src};
auto curAsset = [&]() -> Asset {
auto const& asset = sendMaxAsset ? *sendMaxAsset : deliver;
return asset.visit(
[&](MPTIssue const&) -> Asset { return asset; },
[&](Issue const& issue) -> Asset {
if (isXRP(asset))
return xrpIssue();
return Issue{issue.currency, src};
});
}();
for (auto const& s : result)
@@ -387,22 +515,25 @@ toStrand(
if (auto const b = s->bookStepBook())
{
if (curIss != b->in)
if (curAsset != b->in)
return false;
curIss = b->out;
curAsset = b->out;
}
else
else if (curAsset.holds<Issue>())
{
curIss.account = accts.second;
curAsset.get<Issue>().account = accts.second;
}
curAcc = accts.second;
}
if (curAcc != dst)
return false;
if (curIss.currency != deliver.currency)
if (curAsset.holds<Issue>() != deliver.holds<Issue>() ||
(curAsset.holds<Issue>() &&
curAsset.get<Issue>().currency != deliver.get<Issue>().currency) ||
(curAsset.holds<MPTIssue>() && curAsset.get<MPTIssue>() != deliver.get<MPTIssue>()))
return false;
if (curIss.account != deliver.account && curIss.account != dst)
if (curAsset.getIssuer() != deliver.getIssuer() && curAsset.getIssuer() != dst)
return false;
return true;
};
@@ -424,9 +555,9 @@ toStrands(
ReadView const& view,
AccountID const& src,
AccountID const& dst,
Issue const& deliver,
Asset const& deliver,
std::optional<Quality> const& limitQuality,
std::optional<Issue> const& sendMax,
std::optional<Asset> const& sendMax,
STPathSet const& paths,
bool addDefaultPath,
bool ownerPaysTransferFee,
@@ -539,14 +670,14 @@ StrandContext::StrandContext(
// replicates the source or destination.
AccountID const& strandSrc_,
AccountID const& strandDst_,
Issue const& strandDeliver_,
Asset const& strandDeliver_,
std::optional<Quality> const& limitQuality_,
bool isLast_,
bool ownerPaysTransferFee_,
OfferCrossing offerCrossing_,
bool isDefaultPath_,
std::array<boost::container::flat_set<Issue>, 2>& seenDirectIssues_,
boost::container::flat_set<Issue>& seenBookOuts_,
std::array<boost::container::flat_set<Asset>, 2>& seenDirectAssets_,
boost::container::flat_set<Asset>& seenBookOuts_,
AMMContext& ammContext_,
std::optional<uint256> const& domainID_,
beast::Journal j_)
@@ -562,7 +693,7 @@ StrandContext::StrandContext(
, isDefaultPath(isDefaultPath_)
, strandSize(strand_.size())
, prevStep(!strand_.empty() ? strand_.back().get() : nullptr)
, seenDirectIssues(seenDirectIssues_)
, seenDirectAssets(seenDirectAssets_)
, seenBookOuts(seenBookOuts_)
, ammContext(ammContext_)
, domainID(domainID_)
@@ -570,25 +701,4 @@ StrandContext::StrandContext(
{
}
template <class InAmt, class OutAmt>
bool
isDirectXrpToXrp(Strand const& strand)
{
return false;
}
template <>
bool
isDirectXrpToXrp<XRPAmount, XRPAmount>(Strand const& strand)
{
return (strand.size() == 2);
}
template bool
isDirectXrpToXrp<XRPAmount, IOUAmount>(Strand const& strand);
template bool
isDirectXrpToXrp<IOUAmount, XRPAmount>(Strand const& strand);
template bool
isDirectXrpToXrp<IOUAmount, IOUAmount>(Strand const& strand);
} // namespace xrpl

View File

@@ -54,7 +54,7 @@ RippleCalc::rippleCalculate(
auto const sendMax = [&]() -> std::optional<STAmount> {
if (saMaxAmountReq >= beast::zero ||
saMaxAmountReq.getCurrency() != saDstAmountReq.getCurrency() ||
!equalTokens(saMaxAmountReq.asset(), saDstAmountReq.asset()) ||
saMaxAmountReq.getIssuer() != uSrcAccountID)
{
return saMaxAmountReq;

View File

@@ -179,13 +179,25 @@ private:
// because the trust line was created after the XRP was removed.)
// Return how much the reserve should be reduced.
//
// Note that reduced reserve only happens if the trust line does not
// Note that reduced reserve only happens if the trust line or MPT does not
// currently exist.
static std::int32_t
computeReserveReduction(StrandContext const& ctx, AccountID const& acc)
{
if (ctx.isFirst && !ctx.view.read(keylet::line(acc, ctx.strandDeliver)))
return -1;
if (ctx.isFirst)
{
return ctx.strandDeliver.visit(
[&](Issue const& issue) {
if (!ctx.view.exists(keylet::line(acc, issue)))
return -1;
return 0;
},
[&](MPTIssue const& issue) {
if (!ctx.view.exists(keylet::mptoken(issue.getMptID(), acc)))
return -1;
return 0;
});
}
return 0;
}
@@ -283,9 +295,9 @@ XRPEndpointStep<TDerived>::validFwd(PaymentSandbox& sb, ApplyView& afView, Eithe
return {false, EitherAmount(XRPAmount(beast::zero))};
}
XRPL_ASSERT(in.native, "xrpl::XRPEndpointStep::validFwd : input is XRP");
XRPL_ASSERT(in.holds<XRPAmount>(), "xrpl::XRPEndpointStep::validFwd : input is XRP");
auto const& xrpIn = in.xrp;
auto const& xrpIn = in.get<XRPAmount>();
auto const balance = static_cast<TDerived const*>(this)->xrpLiquid(sb);
if (!isLast_ && balance < xrpIn)
@@ -336,7 +348,7 @@ XRPEndpointStep<TDerived>::check(StrandContext const& ctx) const
return ter;
auto const issuesIndex = isLast_ ? 0 : 1;
if (!ctx.seenDirectIssues[issuesIndex].insert(xrpIssue()).second)
if (!ctx.seenDirectAssets[issuesIndex].insert(xrpIssue()).second)
{
JLOG(j_.debug()) << "XRPEndpointStep: loop detected: Index: " << ctx.strandSize << ' '
<< *this;

View File

@@ -642,7 +642,7 @@ finalizeClaimHelper(
saveNumberRoundMode const _{Number::setround(round_mode)};
STAmount const den{rewardAccounts.size()};
return divide(rewardPool, den, rewardPool.issue());
return divide(rewardPool, den, rewardPool.asset());
}();
STAmount distributed = rewardPool.zeroed();
for (auto const& rewardAccount : rewardAccounts)
@@ -1167,7 +1167,7 @@ attestationPreflight(PreflightContext const& ctx)
if (att->sendingAmount.signum() <= 0)
return temXCHAIN_BAD_PROOF;
auto const expectedIssue = bridgeSpec.issue(STXChainBridge::srcChain(att->wasLockingChainSend));
if (att->sendingAmount.issue() != expectedIssue)
if (att->sendingAmount.asset() != expectedIssue)
return temXCHAIN_BAD_PROOF;
return tesSUCCESS;
@@ -1578,8 +1578,8 @@ XChainClaim::preflight(PreflightContext const& ctx)
auto const amount = ctx.tx[sfAmount];
if (amount.signum() <= 0 ||
(amount.issue() != bridgeSpec.lockingChainIssue() &&
amount.issue() != bridgeSpec.issuingChainIssue()))
(amount.asset() != bridgeSpec.lockingChainIssue() &&
amount.asset() != bridgeSpec.issuingChainIssue()))
{
return temBAD_AMOUNT;
}
@@ -1628,12 +1628,12 @@ XChainClaim::preclaim(PreclaimContext const& ctx)
if (isLockingChain)
{
if (bridgeSpec.lockingChainIssue() != thisChainAmount.issue())
if (bridgeSpec.lockingChainIssue() != thisChainAmount.asset())
return tecXCHAIN_BAD_TRANSFER_ISSUE;
}
else
{
if (bridgeSpec.issuingChainIssue() != thisChainAmount.issue())
if (bridgeSpec.issuingChainIssue() != thisChainAmount.asset())
return tecXCHAIN_BAD_TRANSFER_ISSUE;
}
}
@@ -1820,8 +1820,8 @@ XChainCommit::preflight(PreflightContext const& ctx)
if (amount.signum() <= 0 || !isLegalNet(amount))
return temBAD_AMOUNT;
if (amount.issue() != bridgeSpec.lockingChainIssue() &&
amount.issue() != bridgeSpec.issuingChainIssue())
if (amount.asset() != bridgeSpec.lockingChainIssue() &&
amount.asset() != bridgeSpec.issuingChainIssue())
return temBAD_ISSUER;
return tesSUCCESS;
@@ -1866,12 +1866,12 @@ XChainCommit::preclaim(PreclaimContext const& ctx)
if (isLockingChain)
{
if (bridgeSpec.lockingChainIssue() != ctx.tx[sfAmount].issue())
if (bridgeSpec.lockingChainIssue() != ctx.tx[sfAmount].asset())
return tecXCHAIN_BAD_TRANSFER_ISSUE;
}
else
{
if (bridgeSpec.issuingChainIssue() != ctx.tx[sfAmount].issue())
if (bridgeSpec.issuingChainIssue() != ctx.tx[sfAmount].asset())
return tecXCHAIN_BAD_TRANSFER_ISSUE;
}
@@ -2083,7 +2083,7 @@ XChainCreateAccountCommit::preflight(PreflightContext const& ctx)
if (reward.signum() < 0 || !reward.native())
return temBAD_AMOUNT;
if (reward.issue() != amount.issue())
if (reward.asset() != amount.asset())
return temBAD_AMOUNT;
return tesSUCCESS;
@@ -2115,7 +2115,7 @@ XChainCreateAccountCommit::preclaim(PreclaimContext const& ctx)
if (amount < *minCreateAmount)
return tecXCHAIN_INSUFF_CREATE_AMOUNT;
if (minCreateAmount->issue() != amount.issue())
if (minCreateAmount->asset() != amount.asset())
return tecXCHAIN_BAD_TRANSFER_ISSUE;
AccountID const thisDoor = (*sleBridge)[sfAccount];
@@ -2143,7 +2143,7 @@ XChainCreateAccountCommit::preclaim(PreclaimContext const& ctx)
}
STXChainBridge::ChainType const dstChain = STXChainBridge::otherChain(srcChain);
if (bridgeSpec.issue(srcChain) != ctx.tx[sfAmount].issue())
if (bridgeSpec.issue(srcChain) != ctx.tx[sfAmount].asset())
return tecXCHAIN_BAD_TRANSFER_ISSUE;
if (!isXRP(bridgeSpec.issue(dstChain)))

View File

@@ -1,18 +1,30 @@
#include <xrpl/basics/scope.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/paths/Flow.h>
#include <xrpl/tx/transactors/check/CheckCash.h>
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
#include <algorithm>
namespace xrpl {
bool
CheckCash::checkExtraFeatures(xrpl::PreflightContext const& ctx)
{
auto const optAmount = ctx.tx[~sfAmount];
auto const optDeliverMin = ctx.tx[~sfDeliverMin];
return ctx.rules.enabled(featureMPTokensV2) ||
(!(optAmount && optAmount->holds<MPTIssue>()) &&
!(optDeliverMin && optDeliverMin->holds<MPTIssue>()));
}
NotTEC
CheckCash::preflight(PreflightContext const& ctx)
{
@@ -35,7 +47,7 @@ CheckCash::preflight(PreflightContext const& ctx)
return temBAD_AMOUNT;
}
if (badCurrency() == value.getCurrency())
if (badAsset() == value.asset())
{
JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency.";
return temBAD_CURRENCY;
@@ -106,8 +118,7 @@ CheckCash::preclaim(PreclaimContext const& ctx)
}(ctx.tx)};
STAmount const sendMax = sleCheck->at(sfSendMax);
Currency const currency{value.getCurrency()};
if (currency != sendMax.getCurrency())
if (!equalTokens(value.asset(), sendMax.asset()))
{
JLOG(ctx.j.warn()) << "Check cash does not match check currency.";
return temMALFORMED;
@@ -127,8 +138,13 @@ CheckCash::preclaim(PreclaimContext const& ctx)
// Make sure the check owner holds at least value. If they have
// less than value the check cannot be cashed.
{
STAmount availableFunds{
accountFunds(ctx.view, sleCheck->at(sfAccount), value, fhZERO_IF_FROZEN, ctx.j)};
STAmount availableFunds{accountFunds(
ctx.view,
sleCheck->at(sfAccount),
value,
fhZERO_IF_FROZEN,
ahZERO_IF_UNAUTHORIZED,
ctx.j)};
// Note that src will have one reserve's worth of additional XRP
// once the check is cashed, since the check's reserve will no
@@ -147,51 +163,95 @@ CheckCash::preclaim(PreclaimContext const& ctx)
// An issuer can always accept their own currency.
if (!value.native() && (value.getIssuer() != dstId))
{
auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
if (!sleIssuer)
{
JLOG(ctx.j.warn())
<< "Can't receive IOUs from non-existent issuer: " << to_string(issuerId);
return tecNO_ISSUER;
}
return value.asset().visit(
[&](Issue const& issue) -> TER {
Currency const currency{issue.currency};
auto const sleTrustLine =
ctx.view.read(keylet::line(dstId, issuerId, currency));
if ((sleIssuer->at(sfFlags) & lsfRequireAuth) != 0u)
{
auto const sleTrustLine = ctx.view.read(keylet::line(dstId, issuerId, currency));
auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
if (!sleIssuer)
{
JLOG(ctx.j.warn()) << "Can't receive IOUs from "
"non-existent issuer: "
<< to_string(issuerId);
return tecNO_ISSUER;
}
if (!sleTrustLine)
{
// We can only create a trust line if the issuer does not
// have lsfRequireAuth set.
return tecNO_AUTH;
}
if ((sleIssuer->at(sfFlags) & lsfRequireAuth) != 0u)
{
if (!sleTrustLine)
{
// We can only create a trust line if the issuer
// does not have lsfRequireAuth set.
return tecNO_AUTH;
}
// Entries have a canonical representation, determined by a
// lexicographical "greater than" comparison employing strict
// weak ordering. Determine which entry we need to access.
bool const canonical_gt(dstId > issuerId);
// Entries have a canonical representation,
// determined by a lexicographical "greater than"
// comparison employing strict weak ordering.
// Determine which entry we need to access.
bool const canonical_gt(dstId > issuerId);
bool const is_authorized(
(sleTrustLine->at(sfFlags) & (canonical_gt ? lsfLowAuth : lsfHighAuth)) != 0u);
bool const is_authorized(
(sleTrustLine->at(sfFlags) &
(canonical_gt ? lsfLowAuth : lsfHighAuth)) != 0u);
if (!is_authorized)
{
JLOG(ctx.j.warn()) << "Can't receive IOUs from issuer without auth.";
return tecNO_AUTH;
}
}
if (!is_authorized)
{
JLOG(ctx.j.warn()) << "Can't receive IOUs from "
"issuer without auth.";
return tecNO_AUTH;
}
}
// The trustline from source to issuer does not need to
// be checked for freezing, since we already verified that the
// source has sufficient non-frozen funds available.
// The trustline from source to issuer does not need to
// be checked for freezing, since we already verified
// that the source has sufficient non-frozen funds
// available.
// However, the trustline from destination to issuer may not
// be frozen.
if (isFrozen(ctx.view, dstId, currency, issuerId))
{
JLOG(ctx.j.warn()) << "Cashing a check to a frozen trustline.";
return tecFROZEN;
}
// However, the trustline from destination to issuer may
// not be frozen.
if (isFrozen(ctx.view, dstId, currency, issuerId))
{
JLOG(ctx.j.warn()) << "Cashing a check to a frozen trustline.";
return tecFROZEN;
}
return tesSUCCESS;
},
[&](MPTIssue const& issue) -> TER {
auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
if (!sleIssuer)
{
JLOG(ctx.j.warn()) << "Can't receive MPTs from "
"non-existent issuer: "
<< to_string(issuerId);
return tecNO_ISSUER;
}
if (auto const err = requireAuth(ctx.view, issue, dstId, AuthType::WeakAuth);
!isTesSuccess(err))
{
JLOG(ctx.j.warn()) << "Cashing a check to a MPT requiring auth.";
return err;
}
if (isFrozen(ctx.view, dstId, issue))
{
JLOG(ctx.j.warn()) << "Cashing a check to a frozen MPT.";
return tecLOCKED;
}
if (auto const err = canTransfer(ctx.view, issue, srcId, dstId);
!isTesSuccess(err))
{
JLOG(ctx.j.warn()) << "MPT transfer is disabled.";
return err;
}
return tesSUCCESS;
});
}
}
return tesSUCCESS;
@@ -287,94 +347,152 @@ CheckCash::doApply()
// maximum possible currency because there might be a gateway
// transfer rate to account for. Since the transfer rate cannot
// exceed 200%, we use 1/2 maxValue as our limit.
auto const maxDeliverMin = [&]() {
return optDeliverMin->asset().visit(
[&](Issue const&) {
return STAmount(
optDeliverMin->asset(), STAmount::cMaxValue / 2, STAmount::cMaxOffset);
},
[&](MPTIssue const&) {
return STAmount(optDeliverMin->asset(), maxMPTokenAmount / 2);
});
};
STAmount const flowDeliver{
optDeliverMin
? STAmount(
optDeliverMin->issue(), STAmount::cMaxValue / 2, STAmount::cMaxOffset)
: ctx_.tx.getFieldAmount(sfAmount)};
optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)};
// If a trust line does not exist yet create one.
Issue const& trustLineIssue = flowDeliver.issue();
AccountID const issuer = flowDeliver.getIssuer();
AccountID const truster = issuer == account_ ? srcId : account_;
Keylet const trustLineKey = keylet::line(truster, trustLineIssue);
bool const destLow = issuer > account_;
// Check reserve. Return destination account SLE if enough reserve,
// otherwise return nullptr.
auto checkReserve = [&]() -> std::shared_ptr<SLE> {
auto sleDst = psb.peek(keylet::account(account_));
if (!psb.exists(trustLineKey))
{
// 1. Can the check casher meet the reserve for the trust line?
// 2. Create trust line between destination (this) account
// and the issuer.
// 3. Apply correct noRipple settings on trust line. Use...
// a. this (destination) account and
// b. issuing account (not sending account).
auto const sleDst = psb.peek(keylet::account(account_));
// Can the account cover the trust line's reserve?
// Can the account cover the trust line's or MPT reserve?
if (std::uint32_t const ownerCount = {sleDst->at(sfOwnerCount)};
preFeeBalance_ < psb.fees().accountReserve(ownerCount + 1))
{
JLOG(j_.trace()) << "Trust line does not exist. "
"Insufficent reserve to create line.";
"Insufficient reserve to create line.";
return tecNO_LINE_INSUF_RESERVE;
return nullptr;
}
return sleDst;
};
Currency const currency = flowDeliver.getCurrency();
STAmount initialBalance(flowDeliver.issue());
initialBalance.setIssuer(noAccount());
std::optional<Keylet> trustLineKey;
STAmount savedLimit;
bool destLow = false;
AccountID const& deliverIssuer = flowDeliver.getIssuer();
auto const err = flowDeliver.asset().visit(
[&](Issue const& issue) -> std::optional<TER> {
// If a trust line does not exist yet create one.
Issue const& trustLineIssue = issue;
AccountID const truster = deliverIssuer == account_ ? srcId : account_;
trustLineKey = keylet::line(truster, trustLineIssue);
destLow = deliverIssuer > account_;
if (TER const ter = trustCreate(
psb, // payment sandbox
destLow, // is dest low?
issuer, // source
account_, // destination
trustLineKey.key, // ledger index
sleDst, // Account to add to
false, // authorize account
(sleDst->getFlags() & lsfDefaultRipple) == 0, //
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, account_), // limit of zero
0, // quality in
0, // quality out
viewJ); // journal
!isTesSuccess(ter))
if (!psb.exists(*trustLineKey))
{
// 1. Can the check casher meet the reserve for the
// trust line?
// 2. Create trust line between destination (this)
// account
// and the issuer.
// 3. Apply correct noRipple settings on trust line.
// Use...
// a. this (destination) account and
// b. issuing account (not sending account).
auto const sleDst = checkReserve();
if (sleDst == nullptr)
return tecNO_LINE_INSUF_RESERVE;
Currency const& currency = issue.currency;
STAmount initialBalance(flowDeliver.asset());
initialBalance.get<Issue>().account = noAccount();
if (TER const ter = trustCreate(
psb, // payment sandbox
destLow, // is dest low?
deliverIssuer, // source
account_, // destination
trustLineKey->key, // ledger index
sleDst, // Account to add to
false, // authorize account
(sleDst->getFlags() & lsfDefaultRipple) == 0, //
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, account_), // limit of zero
0, // quality in
0, // quality out
viewJ); // journal
!isTesSuccess(ter))
{
return ter;
}
psb.update(sleDst);
// Note that we _don't_ need to be careful about
// destroying the trust line if the check cashing
// fails. The transaction machinery will
// automatically clean it up.
}
// Since the destination is signing the check, they
// clearly want the funds even if their new total funds
// would exceed the limit on their trust line. So we
// tweak the trust line limits before calling flow and
// then restore the trust line limits afterwards.
auto const sleTrustLine = psb.peek(*trustLineKey);
if (!sleTrustLine)
return tecNO_LINE;
SF_AMOUNT const& tweakedLimit = destLow ? sfLowLimit : sfHighLimit;
savedLimit = sleTrustLine->at(tweakedLimit);
// Set the trust line limit to the highest possible
// value while flow runs.
STAmount const bigAmount(
trustLineIssue, STAmount::cMaxValue, STAmount::cMaxOffset);
sleTrustLine->at(tweakedLimit) = bigAmount;
return std::nullopt;
},
[&](MPTIssue const& issue) -> std::optional<TER> {
if (account_ != deliverIssuer)
{
auto const& mptID = issue.getMptID();
// Create MPT if it doesn't exist
auto const mptokenKey = keylet::mptoken(mptID, account_);
if (!psb.exists(mptokenKey))
{
auto sleDst = checkReserve();
if (sleDst == nullptr)
return tecINSUFFICIENT_RESERVE;
if (auto const err = checkCreateMPT(psb, mptID, account_, j_);
!isTesSuccess(err))
{
return err;
}
}
}
return std::nullopt;
});
if (err)
return *err;
// Make sure the tweaked limits are restored when we leave
// scope.
scope_exit const fixup([&psb, &trustLineKey, destLow, &savedLimit]() {
if (trustLineKey)
{
return ter;
SF_AMOUNT const& tweakedLimit = destLow ? sfLowLimit : sfHighLimit;
if (auto const sleTrustLine = psb.peek(*trustLineKey))
sleTrustLine->at(tweakedLimit) = savedLimit;
}
psb.update(sleDst);
// Note that we _don't_ need to be careful about destroying
// the trust line if the check cashing fails. The transaction
// machinery will automatically clean it up.
}
// Since the destination is signing the check, they clearly want
// the funds even if their new total funds would exceed the limit
// on their trust line. So we tweak the trust line limits before
// calling flow and then restore the trust line limits afterwards.
auto const sleTrustLine = psb.peek(trustLineKey);
if (!sleTrustLine)
return tecNO_LINE;
SF_AMOUNT const& tweakedLimit = destLow ? sfLowLimit : sfHighLimit;
STAmount const savedLimit = sleTrustLine->at(tweakedLimit);
// Make sure the tweaked limits are restored when we leave scope.
scope_exit const fixup([&psb, &trustLineKey, &tweakedLimit, &savedLimit]() {
if (auto const sleTrustLine = psb.peek(trustLineKey))
sleTrustLine->at(tweakedLimit) = savedLimit;
});
// Set the trust line limit to the highest possible value
// while flow runs.
STAmount const bigAmount(trustLineIssue, STAmount::cMaxValue, STAmount::cMaxOffset);
sleTrustLine->at(tweakedLimit) = bigAmount;
// Let flow() do the heavy lifting on a check for an IOU.
auto const result = flow(
psb,
@@ -405,6 +523,7 @@ CheckCash::doApply()
JLOG(ctx_.journal.warn()) << "flow did not produce DeliverMin.";
return tecPATH_PARTIAL;
}
ctx_.deliver(result.actualAmountOut);
}
// Set the delivered amount metadata in all cases, not just

Some files were not shown because too many files have changed in this diff Show More