xrpld
Loading...
Searching...
No Matches
BookStep.cpp
1#include <xrpl/basics/Log.h>
2#include <xrpl/basics/base_uint.h>
3#include <xrpl/basics/contract.h>
4#include <xrpl/beast/utility/Journal.h>
5#include <xrpl/beast/utility/Zero.h>
6#include <xrpl/beast/utility/instrumentation.h>
7#include <xrpl/ledger/ApplyView.h>
8#include <xrpl/ledger/PaymentSandbox.h>
9#include <xrpl/ledger/Sandbox.h>
10#include <xrpl/ledger/helpers/AMMHelpers.h>
11#include <xrpl/ledger/helpers/AccountRootHelpers.h>
12#include <xrpl/ledger/helpers/MPTokenHelpers.h>
13#include <xrpl/ledger/helpers/TokenHelpers.h>
14#include <xrpl/protocol/AccountID.h>
15#include <xrpl/protocol/Asset.h>
16#include <xrpl/protocol/Book.h>
17#include <xrpl/protocol/Concepts.h>
18#include <xrpl/protocol/Feature.h>
19#include <xrpl/protocol/IOUAmount.h>
20#include <xrpl/protocol/Indexes.h>
21#include <xrpl/protocol/Issue.h>
22#include <xrpl/protocol/LedgerFormats.h>
23#include <xrpl/protocol/MPTAmount.h>
24#include <xrpl/protocol/MPTIssue.h>
25#include <xrpl/protocol/Quality.h>
26#include <xrpl/protocol/Rate.h>
27#include <xrpl/protocol/Rules.h>
28#include <xrpl/protocol/SField.h>
29#include <xrpl/protocol/STAmount.h>
30#include <xrpl/protocol/TER.h>
31#include <xrpl/protocol/XRPAmount.h>
32#include <xrpl/tx/paths/AMMLiquidity.h>
33#include <xrpl/tx/paths/AMMOffer.h>
34#include <xrpl/tx/paths/BookTip.h>
35#include <xrpl/tx/paths/OfferStream.h>
36#include <xrpl/tx/paths/detail/EitherAmount.h>
37#include <xrpl/tx/paths/detail/FlatSets.h>
38#include <xrpl/tx/paths/detail/Steps.h>
39
40#include <boost/container/flat_set.hpp>
41
42#include <cstdint>
43#include <memory>
44#include <numeric>
45#include <optional>
46#include <sstream>
47#include <stdexcept>
48#include <string>
49#include <string_view>
50#include <type_traits>
51#include <utility>
52#include <variant>
53
54namespace xrpl {
55
56template <class TIn, class TOut, class TDerived>
57class BookStep : public StepImp<TIn, TOut, BookStep<TIn, TOut, TDerived>>
58{
59protected:
60 enum class OfferType { Amm, Clob };
61
62 static constexpr uint32_t kMaxOffersToConsume{1000};
66 // Charge transfer fees when the prev step redeems
67 Step const* const prevStep_ = nullptr;
69 // Mark as inactive (dry) if too many offers are consumed
70 bool inactive_ = false;
80 // If set, AMM liquidity might be available
81 // if AMM offer quality is better than CLOB offer
82 // quality or there is no CLOB offer.
86
87 struct Cache
88 {
89 TIn in;
90 TOut out;
91
92 Cache(TIn const& in, TOut const& out) : in(in), out(out)
93 {
94 }
95 };
96
98
99private:
100 BookStep(StrandContext const& ctx, Asset const& in, Asset const& out)
101 : book_(in, out, ctx.domainID)
102 , strandSrc_(ctx.strandSrc)
103 , strandDst_(ctx.strandDst)
104 , prevStep_(ctx.prevStep)
105 , ownerPaysTransferFee_(ctx.ownerPaysTransferFee)
106 , j_(ctx.j)
107 , strandDeliver_(ctx.strandDeliver)
108 {
109 if (auto const ammSle = ctx.view.read(keylet::amm(in, out));
110 ammSle && ammSle->getFieldAmount(sfLPTokenBalance) != beast::kZero)
111 {
112 ammLiquidity_.emplace(
113 ctx.view,
114 (*ammSle)[sfAccount],
115 getTradingFee(ctx.view, *ammSle, ctx.ammContext.account()),
116 in,
117 out,
118 ctx.ammContext,
119 ctx.j);
120 }
121 }
122
123public:
124 [[nodiscard]] Book const&
125 book() const
126 {
127 return book_;
128 }
129
130 [[nodiscard]] std::optional<EitherAmount>
131 cachedIn() const override
132 {
133 if (!cache_)
134 return std::nullopt;
135 return EitherAmount(cache_->in);
136 }
137
138 [[nodiscard]] std::optional<EitherAmount>
139 cachedOut() const override
140 {
141 if (!cache_)
142 return std::nullopt;
143 return EitherAmount(cache_->out);
144 }
145
146 [[nodiscard]] DebtDirection
151
152 [[nodiscard]] std::optional<Book>
153 bookStepBook() const override
154 {
155 return book_;
156 }
157
159 qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const override;
160
162 getQualityFunc(ReadView const& v, DebtDirection prevStepDir) const override;
163
164 [[nodiscard]] std::uint32_t
165 offersUsed() const override;
166
169 PaymentSandbox& sb,
170 ApplyView& afView,
171 boost::container::flat_set<uint256>& ofrsToRm,
172 TOut const& out);
173
176 PaymentSandbox& sb,
177 ApplyView& afView,
178 boost::container::flat_set<uint256>& ofrsToRm,
179 TIn const& in);
180
182 validFwd(PaymentSandbox& sb, ApplyView& afView, EitherAmount const& in) override;
183
184 // Check for errors frozen constraints.
185 [[nodiscard]] TER
186 check(StrandContext const& ctx) const;
187
188 [[nodiscard]] bool
189 inactive() const override
190 {
191 return inactive_;
192 }
193
194protected:
196 logStringImpl(char const* name) const
197 {
199 ostr << name << ": "
200 << "\ninIss: " << book_.in.getIssuer() << "\noutIss: " << book_.out.getIssuer()
201 << "\ninCur: " << to_string(book_.in) << "\noutCur: " << to_string(book_.out);
202 return ostr.str();
203 }
204
205 [[nodiscard]] Rate
206 rate(ReadView const& view, Asset const& asset, AccountID const& dstAccount) const;
207
208private:
209 friend bool
210 operator==(BookStep const& lhs, BookStep const& rhs)
211 {
212 return lhs.book_ == rhs.book_;
213 }
214
215 friend bool
216 operator!=(BookStep const& lhs, BookStep const& rhs)
217 {
218 return !(lhs == rhs);
219 }
220
221 [[nodiscard]] bool
222 equal(Step const& rhs) const override;
223
224 // Iterate through the offers at the best quality in a book.
225 // Unfunded offers and bad offers are skipped (and returned).
226 // callback is called with the offer SLE, taker pays, taker gets.
227 // If callback returns false, don't process any more offers.
228 // Return the unfunded, bad offers and the number of offers consumed.
229 template <class Callback>
232 PaymentSandbox& sb,
233 ApplyView& afView,
234 DebtDirection prevStepDebtDir,
235 Callback& callback) const;
236
237 // Offer is either TOffer or AMMOffer
238 template <template <typename, typename> typename Offer>
239 void
241 PaymentSandbox& sb,
242 Offer<TIn, TOut>& offer,
243 TAmounts<TIn, TOut> const& ofrAmt,
244 TAmounts<TIn, TOut> const& stepAmt,
245 TOut const& ownerGives) const;
246
247 // If clobQuality is available and has a better quality then return nullopt,
248 // otherwise if amm liquidity is available return AMM offer adjusted based
249 // on clobQuality.
251 getAMMOffer(ReadView const& view, std::optional<Quality> const& clobQuality) const;
252
253 // If seated then it is either order book tip quality or AMMOffer,
254 // whichever is a better quality.
256 tip(ReadView const& view) const;
257 // If seated then it is either AMM or CLOB quality,
258 // whichever is a better quality. OfferType is AMM
259 // if AMM quality is better.
261 tipOfferQuality(ReadView const& view) const;
262 // If seated then it is either AMM or CLOB quality function,
263 // whichever is a better quality.
265 tipOfferQualityF(ReadView const& view) const;
266
267 // Check that takerPays/takerGets can be transferred/traded.
268 // Applies to MPT assets.
269 [[nodiscard]] bool
270 checkMPTDEX(ReadView const& view, AccountID const& owner) const;
271
272 friend TDerived;
273};
274
275//------------------------------------------------------------------------------
276
277// Flow is used in two different circumstances for transferring funds:
278// o Payments, and
279// o Offer crossing.
280// The rules for handling funds in these two cases are almost, but not
281// quite, the same.
282
283// Payment BookStep template class (not offer crossing).
284template <class TIn, class TOut>
285class BookPaymentStep : public BookStep<TIn, TOut, BookPaymentStep<TIn, TOut>>
286{
287public:
288 explicit BookPaymentStep() = default;
289
290 BookPaymentStep(StrandContext const& ctx, Asset const& in, Asset const& out)
291 : BookStep<TIn, TOut, BookPaymentStep<TIn, TOut>>(ctx, in, out)
292 {
293 }
294
297
298 // Never limit self cross quality on a payment.
299 template <template <typename, typename> typename Offer>
300 bool
302 AccountID const&,
303 AccountID const&,
304 Offer<TIn, TOut> const& offer,
307 bool) const
308 {
309 return false;
310 }
311
312 // A payment can look at offers of any quality
313 [[nodiscard]] bool
314 checkQualityThreshold(Quality const& quality) const
315 {
316 return true;
317 }
318
319 // A payment doesn't use quality threshold (limitQuality)
320 // since the strand's quality doesn't directly relate to the step's quality.
321 [[nodiscard]] std::optional<Quality>
322 qualityThreshold(Quality const& lobQuality) const
323 {
324 return lobQuality;
325 }
326
327 // For a payment ofrInRate is always the same as trIn.
329 getOfrInRate(Step const*, AccountID const&, std::uint32_t trIn) const
330 {
331 return trIn;
332 }
333
334 // For a payment ofrOutRate is always the same as trOut.
336 getOfrOutRate(Step const*, AccountID const&, AccountID const&, std::uint32_t trOut) const
337 {
338 return trOut;
339 }
340
341 [[nodiscard]] Quality
343 ReadView const& v,
344 Quality const& ofrQ,
345 DebtDirection prevStepDir,
346 WaiveTransferFee waiveFee,
347 OfferType,
348 Rules const&) const
349 {
350 // Charge the offer owner, not the sender
351 // Charge a fee even if the owner is the same as the issuer
352 // (the old code does not charge a fee)
353 // Calculate amount that goes to the taker and the amount charged the
354 // offer owner
355 auto const trIn =
356 redeems(prevStepDir) ? this->rate(v, this->book_.in, this->strandDst_) : kParityRate;
357 // Always charge the transfer fee, even if the owner is the issuer,
358 // unless the fee is waived
359 auto const trOut = (this->ownerPaysTransferFee_ && waiveFee == WaiveTransferFee::No)
360 ? this->rate(v, this->book_.out, this->strandDst_)
361 : kParityRate;
362
363 Quality const q1{getRate(STAmount(trOut.value), STAmount(trIn.value))};
364 return composedQuality(q1, ofrQ);
365 }
366
367 [[nodiscard]] std::string
368 logString() const override
369 {
370 return this->logStringImpl("BookPaymentStep");
371 }
372};
373
374// Offer crossing BookStep template class (not a payment).
375template <class TIn, class TOut>
376class BookOfferCrossingStep : public BookStep<TIn, TOut, BookOfferCrossingStep<TIn, TOut>>
377{
380
381private:
382 // Helper function that throws if the optional passed to the constructor
383 // is none.
384 static Quality
386 {
387 // It's really a programming error if the quality is missing.
388 XRPL_ASSERT(limitQuality, "xrpl::BookOfferCrossingStep::getQuality : nonzero quality");
389 if (!limitQuality)
390 Throw<FlowException>(tefINTERNAL, "Offer requires quality.");
391 return *limitQuality;
392 }
393
394public:
395 BookOfferCrossingStep(StrandContext const& ctx, Asset const& in, Asset const& out)
396 : BookStep<TIn, TOut, BookOfferCrossingStep<TIn, TOut>>(ctx, in, out)
398 , qualityThreshold_(getQuality(ctx.limitQuality))
399 {
400 }
401
402 template <template <typename, typename> typename Offer>
403 bool
405 AccountID const& strandSrc,
406 AccountID const& strandDst,
407 Offer<TIn, TOut> const& offer,
410 bool const offerAttempted) const
411 {
412 // This method supports some correct but slightly surprising
413 // behavior in offer crossing. The scenario:
414 //
415 // o alice has already created one or more offers.
416 // o alice creates another offer that can be directly crossed (not
417 // autobridged) by one or more of her previously created offer(s).
418 //
419 // What does the offer crossing do?
420 //
421 // o The offer crossing could go ahead and cross the offers leaving
422 // either one reduced offer (partial crossing) or zero offers
423 // (exact crossing) in the ledger. We don't do this. And, really,
424 // the offer creator probably didn't want us to.
425 //
426 // o We could skip over the self offer in the book and only cross
427 // offers that are not our own. This would make a lot of sense,
428 // but we don't do it. Part of the rationale is that we can only
429 // operate on the tip of the order book. We can't leave an offer
430 // behind -- it would sit on the tip and block access to other
431 // offers.
432 //
433 // o We could delete the self-crossable offer(s) off the tip of the
434 // book and continue with offer crossing. That's what we do.
435 //
436 // To support this scenario offer crossing has a special rule. If:
437 // a. We're offer crossing using default path (no autobridging), and
438 // b. The offer's quality is at least as good as our quality, and
439 // c. We're about to cross one of our own offers, then
440 // d. Delete the old offer from the ledger.
441 if (defaultPath_ && offer.quality() >= qualityThreshold_ && strandSrc == offer.owner() &&
442 strandDst == offer.owner())
443 {
444 // Remove this offer even if no crossing occurs.
445 if (auto const key = offer.key())
446 offers.permRmOffer(*key);
447
448 // If no offers have been attempted yet then it's okay to move to
449 // a different quality.
450 if (!offerAttempted)
451 ofrQ = std::nullopt;
452
453 // Return true so the current offer will be deleted.
454 return true;
455 }
456 return false;
457 }
458
459 // Offer crossing can prune the offers it needs to look at with a
460 // quality threshold.
461 [[nodiscard]] bool
462 checkQualityThreshold(Quality const& quality) const
463 {
464 return !defaultPath_ || quality >= qualityThreshold_;
465 }
466
467 // Return quality threshold or nullopt to use when generating AMM offer.
468 // AMM synthetic offer is generated to match LOB offer quality.
469 // If LOB tip offer quality is less than qualityThreshold
470 // then generated AMM offer quality is also less than qualityThreshold and
471 // the offer is not crossed even though AMM might generate a better quality
472 // offer. To address this, if qualityThreshold is greater than lobQuality
473 // then don't use quality to generate the AMM offer. The limit out value
474 // generates the maximum AMM offer in this case, which matches
475 // the quality threshold. This only applies to single path scenario.
476 // Multi-path AMM offers work the same as LOB offers.
477 [[nodiscard]] std::optional<Quality>
478 qualityThreshold(Quality const& lobQuality) const
479 {
480 if (this->ammLiquidity_ && !this->ammLiquidity_->multiPath() &&
481 qualityThreshold_ > lobQuality)
482 return std::nullopt;
483 return lobQuality;
484 }
485
486 // For offer crossing don't pay the transfer fee if alice is paying alice.
487 // A regular (non-offer-crossing) payment does not apply this rule.
489 getOfrInRate(Step const* prevStep, AccountID const& owner, std::uint32_t trIn) const
490 {
491 auto const srcAcct = (prevStep != nullptr) ? prevStep->directStepSrcAcct() : std::nullopt;
492
493 return owner == srcAcct // If offer crossing && prevStep is DirectI
494 ? QUALITY_ONE // or MPTEndpoint && src is offer owner
495 : trIn; // then rate = QUALITY_ONE
496 }
497
498 // See comment on getOfrInRate().
501 Step const* prevStep,
502 AccountID const& owner,
503 AccountID const& strandDst,
504 std::uint32_t trOut) const
505 {
506 return // If offer crossing
507 (prevStep != nullptr) && prevStep->bookStepBook() && // && prevStep is BookStep
508 owner == strandDst // && dest is offer owner
509 ? QUALITY_ONE
510 : trOut; // then rate = QUALITY_ONE
511 }
512
513 [[nodiscard]] Quality
515 ReadView const& v,
516 Quality const& ofrQ,
517 DebtDirection prevStepDir,
518 WaiveTransferFee waiveFee,
519 OfferType offerType,
520 Rules const& rules) const
521 {
522 // Offer x-ing does not charge a transfer fee when the offer's owner
523 // is the same as the strand dst. It is important that
524 // `qualityUpperBound` is an upper bound on the quality (it is used to
525 // ignore strands whose quality cannot meet a minimum threshold). When
526 // calculating quality assume no fee is charged, or the estimate will no
527 // longer be an upper bound.
528
529 // Single path AMM offer has to factor in the transfer in rate
530 // when calculating the upper bound quality and the quality function
531 // because single path AMM's offer quality is not constant.
532 if (!rules.enabled(fixAMMv1_1))
533 {
534 return ofrQ;
535 }
536 if (offerType == OfferType::Clob ||
537 (this->ammLiquidity_ && this->ammLiquidity_->multiPath()))
538 {
539 return ofrQ;
540 }
541
542 auto const trIn =
543 redeems(prevStepDir) ? this->rate(v, this->book_.in, this->strandDst_) : kParityRate;
544 // AMM doesn't pay the transfer fee on the out amount
545 auto const trOut = kParityRate;
546
547 Quality const q1{getRate(STAmount(trOut.value), STAmount(trIn.value))};
548 return composedQuality(q1, ofrQ);
549 }
550
551 [[nodiscard]] std::string
552 logString() const override
553 {
554 return this->logStringImpl("BookOfferCrossingStep");
555 }
556
557private:
558 bool const defaultPath_;
560};
561
562//------------------------------------------------------------------------------
563
564template <class TIn, class TOut, class TDerived>
565bool
567{
568 if (auto bs = dynamic_cast<BookStep<TIn, TOut, TDerived> const*>(&rhs))
569 return book_ == bs->book_;
570 return false;
571}
572
573template <class TIn, class TOut, class TDerived>
576{
577 auto const dir = this->debtDirection(v, StrandDirection::Forward);
578
580 if (!res)
581 return {std::nullopt, dir};
582
583 auto const waiveFee = (std::get<OfferType>(*res) == OfferType::Amm) ? WaiveTransferFee::Yes
585
586 Quality const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
587 v, std::get<Quality>(*res), prevStepDir, waiveFee, std::get<OfferType>(*res), v.rules());
588 return {q, dir};
589}
590
591template <class TIn, class TOut, class TDerived>
594{
595 auto const dir = this->debtDirection(v, StrandDirection::Forward);
596
598 if (!res)
599 return {std::nullopt, dir};
600
601 // AMM
602 if (!res->isConst())
603 {
604 auto static const kQOne = Quality{STAmount::kURateOne};
605 auto const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
606 v, kQOne, prevStepDir, WaiveTransferFee::Yes, OfferType::Amm, v.rules());
607 if (q == kQOne)
608 return {res, dir};
610 qf.combine(*res);
611 return {qf, dir};
612 }
613
614 // CLOB
615 Quality const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
616 v,
617 *(res->quality()), // NOLINT(bugprone-unchecked-optional-access) CLOB QualityFunction
618 // always has quality set
619 prevStepDir,
622 v.rules());
624}
625
626template <class TIn, class TOut, class TDerived>
632
633// Adjust the offer amount and step amount subject to the given input limit
634template <class TIn, class TOut, class Offer>
635static void
637 Offer const& offer,
638 TAmounts<TIn, TOut>& ofrAmt,
639 TAmounts<TIn, TOut>& stpAmt,
640 TOut& ownerGives,
641 std::uint32_t transferRateIn,
642 std::uint32_t transferRateOut,
643 TIn const& limit)
644{
645 if (limit < stpAmt.in)
646 {
647 stpAmt.in = limit;
648 auto const inLmt = mulRatio(stpAmt.in, QUALITY_ONE, transferRateIn, /*roundUp*/ false);
649 // It turns out we can prevent order book blocking by (strictly)
650 // rounding down the ceil_in() result. By rounding down we guarantee
651 // that the quality of an offer left in the ledger is as good or
652 // better than the quality of the containing order book page.
653 //
654 // This adjustment changes transaction outcomes, so it must be made
655 // under an amendment.
656 ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false);
657 stpAmt.out = ofrAmt.out;
658 // Round up for MPT output so the offer owner pays the full
659 // ceil(amount × rate) fee, matching direct Payment semantics. IOU uses
660 // floating-point arithmetic so the floor/ceil distinction is sub-epsilon
661 // there; preserve the historical false to avoid changing IOU behavior.
662 ownerGives = mulRatio(
663 ofrAmt.out,
664 transferRateOut,
665 QUALITY_ONE,
667 }
668}
669
670// Adjust the offer amount and step amount subject to the given output limit
671template <class TIn, class TOut, class Offer>
672static void
674 Offer const& offer,
675 TAmounts<TIn, TOut>& ofrAmt,
676 TAmounts<TIn, TOut>& stpAmt,
677 TOut& ownerGives,
678 std::uint32_t transferRateIn,
679 std::uint32_t transferRateOut,
680 TOut const& limit)
681{
682 if (limit < stpAmt.out)
683 {
684 stpAmt.out = limit;
685 ownerGives = mulRatio(
686 stpAmt.out,
687 transferRateOut,
688 QUALITY_ONE,
690 ofrAmt = offer.limitOut(
691 ofrAmt,
692 stpAmt.out,
693 /*roundUp*/ true);
694 stpAmt.in = mulRatio(ofrAmt.in, transferRateIn, QUALITY_ONE, /*roundUp*/ true);
695 }
696}
697
698template <class TIn, class TOut, class TDerived>
699template <class Callback>
702 PaymentSandbox& sb,
703 ApplyView& afView,
704 DebtDirection prevStepDir,
705 Callback& callback) const
706{
707 // Charge the offer owner, not the sender
708 // Charge a fee even if the owner is the same as the issuer
709 // (the old code does not charge a fee)
710 // Calculate amount that goes to the taker and the amount charged the offer
711 // owner
712 std::uint32_t const trIn =
713 redeems(prevStepDir) ? rate(sb, book_.in, this->strandDst_).value : QUALITY_ONE;
714 // Always charge the transfer fee, even if the owner is the issuer
715 std::uint32_t const trOut =
716 ownerPaysTransferFee_ ? rate(sb, book_.out, this->strandDst_).value : QUALITY_ONE;
717
719
720 FlowOfferStream<TIn, TOut> offers(sb, afView, book_, sb.parentCloseTime(), counter, j_);
721
722 bool offerAttempted = false;
724 auto execOffer = [&](auto& offer) {
725 // Note that offer.quality() returns a (non-optional) Quality. So
726 // ofrQ is always safe to use below this point in the lambda.
727 if (!ofrQ)
728 {
729 ofrQ = offer.quality();
730 }
731 else if (*ofrQ != offer.quality())
732 {
733 return false;
734 }
735
736 if (static_cast<TDerived const*>(this)->limitSelfCrossQuality(
737 strandSrc_, strandDst_, offer, ofrQ, offers, offerAttempted))
738 return true;
739
740 Asset const& assetIn = offer.assetIn();
741 bool const isAssetInMPT = assetIn.holds<MPTIssue>();
742 auto const& owner = offer.owner();
743
744 auto removeOffer = [&](std::string_view logMessage = {}) {
745 auto const key = offer.key();
746 if (!logMessage.empty())
747 {
748 JLOG(j_.trace()) << logMessage << (key ? " " + to_string(*key) : "");
749 }
750 if (key)
751 offers.permRmOffer(*key);
752 if (!offerAttempted)
753 {
754 // Change quality only if no previous offers were tried.
755 ofrQ = std::nullopt;
756 }
757 };
758
759 // It shouldn't matter from auth point of view whether it's sb
760 // or afView. Amendment guard this change just in case.
761 auto& applyView = sb.rules().enabled(featureMPTokensV2) ? sb : afView;
762 // Make sure offer owner has authorization to own Assets from issuer
763 // and MPT assets can be traded/transferred.
764 // An account can always own XRP or their own Assets.
765 // Missing MPTokens are allowed during offer discovery; they are
766 // created later if the offer is actually consumed.
767 auto const authType = isAssetInMPT ? AuthType::WeakAuth : AuthType::Legacy;
768 if (!isTesSuccess(requireAuth(applyView, assetIn, owner, authType)) ||
769 !checkMPTDEX(sb, owner))
770 {
771 // Offer owner not authorized to hold IOU/MPT from issuer.
772 // Remove this offer even if no crossing occurs.
773 removeOffer();
774 // Returning true causes offers.step() to delete the offer.
775 return true;
776 }
777
778 if (!static_cast<TDerived const*>(this)->checkQualityThreshold(offer.quality()))
779 return false;
780
781 auto const [ofrInRate, ofrOutRate] = offer.adjustRates(
782 static_cast<TDerived const*>(this)->getOfrInRate(prevStep_, owner, trIn),
783 static_cast<TDerived const*>(this)->getOfrOutRate(prevStep_, owner, strandDst_, trOut));
784
785 auto ofrAmt = offer.amount();
786 TAmounts stpAmt{ofrAmt.in, ofrAmt.out};
787 auto ownerGives = ofrAmt.out;
788 try
789 {
790 // All arithmetic in this block runs before the offer is consumed.
791 // A crafted MPTokensV2 offer can overflow while transfer rates or
792 // crossing limits are applied; remove that unusable offer instead
793 // of letting it persist as a tecINTERNAL source.
794 stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
795
796 // owner pays the transfer fee.
797 ownerGives = mulRatio(
798 ofrAmt.out,
799 ofrOutRate,
800 QUALITY_ONE,
802
803 auto const funds = offer.isFunded()
804 ? ownerGives // Offer owner is issuer; they have unlimited funds
805 : offers.ownerFunds();
806
807 // Only if CLOB offer
808 if (funds < ownerGives)
809 {
810 // We already know offer.owner()!=offer.issueOut().account
811 ownerGives = funds;
812 stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false);
813
814 // It turns out we can prevent order book blocking by (strictly)
815 // rounding down the ceil_out() result. This adjustment changes
816 // transaction outcomes, so it must be made under an amendment.
817 ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false);
818
819 stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
820 }
821
822 // Limit offer's input if MPT, BookStep is the first step (an issuer
823 // is making a cross-currency payment), and this offer is not owned
824 // by the issuer. Otherwise, OutstandingAmount may overflow.
825 auto const& issuer = assetIn.getIssuer();
826 if (isAssetInMPT && !prevStep_ && offer.owner() != issuer)
827 {
828 // Funds available to issue
829 auto const available = toAmount<TIn>(accountFunds(
830 sb,
831 issuer,
832 assetIn, // STAmount{0}, but the default is not used
835 j_));
836 if (stpAmt.in > available)
837 {
839 offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available);
840 }
841 }
842
843 offerAttempted = true;
844 return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate);
845 }
846 catch (std::overflow_error const&)
847 {
848 if (sb.rules().enabled(featureMPTokensV2))
849 {
850 SOMETIMES(
851 true,
852 "BookStep::forEachOffer removed MPT offer after "
853 "overflow during crossing");
854 removeOffer("Removing offer with overflowing amount calculation");
855 return true;
856 }
857 // An overflow can only be produced by a crafted MPT offer, and MPT
858 // offers require featureMPTokensV2 (enforced at OfferCreate
859 // preflight). So the amendment is always enabled when we get here
860 // and this legacy re-throw is unreachable in practice.
861 // LCOV_EXCL_START
862 XRPL_ASSERT(
863 sb.rules().enabled(featureMPTokensV2),
864 "xrpl::BookStep::forEachOffer : overflow implies MPTokensV2");
865 throw;
866 // LCOV_EXCL_STOP
867 }
868 };
869
870 // At any payment engine iteration, AMM offer can only be consumed once.
871 auto tryAMM = [&](std::optional<Quality> const& lobQuality) -> bool {
872 // amm doesn't support domain yet
873 if (book_.domain)
874 return true;
875
876 // If offer crossing then use either LOB quality or nullopt
877 // to prevent AMM being blocked by a lower quality LOB.
878 auto const qualityThreshold = [&]() -> std::optional<Quality> {
879 if (sb.rules().enabled(fixAMMv1_1) && lobQuality)
880 return static_cast<TDerived const*>(this)->qualityThreshold(*lobQuality);
881 return lobQuality;
882 }();
883 auto ammOffer = getAMMOffer(sb, qualityThreshold);
884 return !ammOffer || execOffer(*ammOffer);
885 };
886
887 if (offers.step())
888 {
889 if (tryAMM(offers.tip().quality()))
890 {
891 do
892 {
893 if (!execOffer(offers.tip()))
894 break;
895 } while (offers.step());
896 }
897 }
898 else
899 {
900 // Might have AMM offer if there are no LOB offers.
901 tryAMM(std::nullopt);
902 }
903
904 return {offers.permToRemove(), counter.count()};
905}
906
907template <class TIn, class TOut, class TDerived>
908template <template <typename, typename> typename Offer>
909void
911 PaymentSandbox& sb,
912 Offer<TIn, TOut>& offer,
913 TAmounts<TIn, TOut> const& ofrAmt,
914 TAmounts<TIn, TOut> const& stepAmt,
915 TOut const& ownerGives) const
916{
917 if (!offer.checkInvariant(ofrAmt, j_))
918 {
919 // LCOV_EXCL_START
920 Throw<FlowException>(tecINVARIANT_FAILED, "AMM pool product invariant failed.");
921 // LCOV_EXCL_STOP
922 }
923
924 // The offer owner gets the ofrAmt. The difference between ofrAmt and
925 // stepAmt is a transfer fee that goes to book_.in.account
926 {
927 if constexpr (std::is_same_v<TIn, MPTAmount>)
928 {
929 // If the offer's TakerPays asset is an MPT, the offer owner must
930 // hold an MPToken to receive it. Create one here if it doesn't
931 // already exist.
932 if (auto const err = checkCreateMPT(sb, book_.in.get<MPTIssue>(), offer.owner(), j_);
933 !isTesSuccess(err))
934 {
935 // checkCreateMPT only fails on tecDIR_FULL (its source line is
936 // itself LCOV-excluded) or a missing offer-owner account, which
937 // cannot happen since that account owns the offer being
938 // consumed. Defensive and unreachable in practice.
939 Throw<FlowException>(err); // LCOV_EXCL_LINE
940 }
941 }
942
943 auto const dr = offer.send(
944 sb, book_.in.getIssuer(), offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_);
945 if (!isTesSuccess(dr))
947 }
948
949 // The offer owner pays `ownerGives`. The difference between ownerGives and
950 // stepAmt is a transfer fee that goes to book_.out.account
951 {
952 auto const& issuer = book_.out.getIssuer();
953 auto const cr =
954 offer.send(sb, offer.owner(), issuer, toSTAmount(ownerGives, book_.out), j_);
955 if (!isTesSuccess(cr))
958 {
959 if (offer.owner() == issuer)
960 issuerSelfDebitHookMPT(sb, book_.out.get<MPTIssue>(), ofrAmt.out.value());
961 }
962 }
963
964 offer.consume(sb, ofrAmt);
965}
966
967template <class TIn, class TOut, class TDerived>
970 ReadView const& view,
971 std::optional<Quality> const& clobQuality) const
972{
973 // AMM doesn't support domain books. When fixCleanup3_3_0 is enabled, exclude
974 // AMM liquidity so quality estimation matches actual crossing (tryAMM skips
975 // AMM for domain books).
976 if (book_.domain && view.rules().enabled(fixCleanup3_3_0))
977 return std::nullopt;
978 if (ammLiquidity_)
979 return ammLiquidity_->getOffer(view, clobQuality);
980 return std::nullopt;
981}
982
983template <class TIn, class TOut, class TDerived>
986{
987 // This can be simplified (and sped up) if directories are never empty.
988 Sandbox sb(&view, TapNone);
989 BookTip bt(sb, book_);
990 auto const lobQuality = bt.step(j_) ? std::optional<Quality>(bt.quality()) : std::nullopt;
991 // Multi-path offer generates an offer with the quality
992 // calculated from the offer size and the quality is constant in this case.
993 // Single path offer quality changes with the offer size. Spot price quality
994 // (SPQ) can't be used in this case as the upper bound quality because
995 // even if SPQ quality is better than LOB quality, it might not be possible
996 // to generate AMM offer at or better quality than LOB quality. Another
997 // factor to consider is limit quality on offer crossing. If LOB quality
998 // is greater than limit quality then use LOB quality when generating AMM
999 // offer, otherwise don't use quality threshold when generating AMM offer.
1000 // AMM or LOB offer, whether multi-path or single path then can be selected
1001 // based on the best offer quality. Using the quality to generate AMM offer
1002 // in this case also prevents the payment engine from going into multiple
1003 // iterations to cross a LOB offer. This happens when AMM changes
1004 // the out amount at the start of iteration to match the limitQuality
1005 // on offer crossing but AMM can't generate the offer at this quality,
1006 // as the result a LOB offer is partially crossed, and it might take a few
1007 // iterations to fully cross the offer.
1008 auto const qualityThreshold = [&]() -> std::optional<Quality> {
1009 if (view.rules().enabled(fixAMMv1_1) && lobQuality)
1010 return static_cast<TDerived const*>(this)->qualityThreshold(*lobQuality);
1011 return std::nullopt;
1012 }();
1013 // AMM quality is better or no LOB offer
1014 if (auto const ammOffer = getAMMOffer(view, qualityThreshold);
1015 ammOffer && ((lobQuality && ammOffer->quality() > lobQuality) || !lobQuality))
1016 return ammOffer;
1017 // LOB quality is better or nullopt
1018 return lobQuality;
1019}
1020
1021template <class TIn, class TOut, class TDerived>
1022auto
1025{
1026 auto const res = tip(view);
1027 if (!res)
1028 {
1029 return std::nullopt;
1030 }
1031 if (auto const q = std::get_if<Quality>(&(*res)))
1032 {
1033 return std::make_pair(*q, OfferType::Clob);
1034 }
1035
1036 return std::make_pair(std::get<AMMOffer<TIn, TOut>>(*res).quality(), OfferType::Amm);
1037}
1038
1039template <class TIn, class TOut, class TDerived>
1042{
1043 auto const res = tip(view);
1044 if (!res)
1045 {
1046 return std::nullopt;
1047 }
1048 if (auto const q = std::get_if<Quality>(&(*res)))
1049 {
1051 }
1052
1053 return std::get<AMMOffer<TIn, TOut>>(*res).getQualityFunc();
1054}
1055
1056template <class TCollection>
1057static auto
1058sum(TCollection const& col)
1059{
1060 using TResult = std::decay_t<decltype(*col.begin())>;
1061 if (col.empty())
1062 return TResult{beast::kZero};
1063 return std::accumulate(col.begin() + 1, col.end(), *col.begin());
1064};
1065
1066template <class TIn, class TOut, class TDerived>
1069 PaymentSandbox& sb,
1070 ApplyView& afView,
1071 boost::container::flat_set<uint256>& ofrsToRm,
1072 TOut const& out)
1073{
1074 cache_.reset();
1075
1077
1078 auto remainingOut = out;
1079
1080 boost::container::flat_multiset<TIn> savedIns;
1081 savedIns.reserve(64);
1082 boost::container::flat_multiset<TOut> savedOuts;
1083 savedOuts.reserve(64);
1084
1085 /* amt fed will be adjusted by owner funds (and may differ from the offer's
1086 amounts - tho always <=)
1087 Return true to continue to receive offers, false to stop receiving offers.
1088 */
1089 auto eachOffer = [&](auto& offer,
1090 TAmounts<TIn, TOut> const& ofrAmt,
1091 TAmounts<TIn, TOut> const& stpAmt,
1092 TOut const& ownerGives,
1093 std::uint32_t transferRateIn,
1094 std::uint32_t transferRateOut) mutable -> bool {
1095 if (remainingOut <= beast::kZero)
1096 return false;
1097
1098 if (stpAmt.out <= remainingOut)
1099 {
1100 savedIns.insert(stpAmt.in);
1101 savedOuts.insert(stpAmt.out);
1102 result = TAmounts<TIn, TOut>(sum(savedIns), sum(savedOuts));
1103 remainingOut = out - result.out;
1104 this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
1105 // return true b/c even if the payment is satisfied,
1106 // we need to consume the offer
1107 return true;
1108 }
1109
1110 auto ofrAdjAmt = ofrAmt;
1111 auto stpAdjAmt = stpAmt;
1112 auto ownerGivesAdj = ownerGives;
1113 // This reduction can overflow via the transfer-rate mulRatio() on a
1114 // 63-bit MPT amount (IOU rescales instead of throwing, and XRP stays
1115 // under the int64 limit, so only MPT reaches it today), but
1116 // savedIns/savedOuts are not updated until after it succeeds. The outer
1117 // execOffer() catch can therefore remove the offer under
1118 // featureMPTokensV2 (legacy propagate-the-exception behavior otherwise)
1119 // without rolling back local state.
1121 offer,
1122 ofrAdjAmt,
1123 stpAdjAmt,
1124 ownerGivesAdj,
1125 transferRateIn,
1126 transferRateOut,
1127 remainingOut);
1128 remainingOut = beast::kZero;
1129 savedIns.insert(stpAdjAmt.in);
1130 savedOuts.insert(remainingOut);
1131 result.in = sum(savedIns);
1132 result.out = out;
1133 this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
1134
1135 // Explicitly check whether the offer is funded. Given that we have
1136 // (stpAmt.out > remainingOut), it's natural to assume the offer
1137 // will still be funded after consuming remainingOut but that is
1138 // not always the case. If the mantissas of two IOU amounts differ
1139 // by less than ten, then subtracting them leaves a zero.
1140 return offer.fullyConsumed();
1141 };
1142
1143 {
1144 auto const prevStepDebtDir = [&] {
1145 if (prevStep_)
1146 return prevStep_->debtDirection(sb, StrandDirection::Reverse);
1147 return DebtDirection::Issues;
1148 }();
1149 auto const r = forEachOffer(sb, afView, prevStepDebtDir, eachOffer);
1150 boost::container::flat_set<uint256> const toRm = std::move(std::get<0>(r));
1151 std::uint32_t const offersConsumed = std::get<1>(r);
1152 offersUsed_ = offersConsumed;
1153 setUnion(ofrsToRm, toRm);
1154
1155 // Too many iterations, mark this strand as inactive
1156 if (offersConsumed >= kMaxOffersToConsume)
1157 {
1158 inactive_ = true;
1159 }
1160 }
1161
1162 switch (remainingOut.signum())
1163 {
1164 case -1: {
1165 // something went very wrong
1166 // LCOV_EXCL_START
1167 JLOG(j_.error()) << "BookStep remainingOut < 0 " << to_string(remainingOut);
1168 UNREACHABLE("xrpl::BookStep::revImp : remaining less than zero");
1170 return {beast::kZero, beast::kZero};
1171 // LCOV_EXCL_STOP
1172 }
1173 case 0: {
1174 // due to normalization, remainingOut can be zero without
1175 // result.out == out. Force result.out == out for this case
1176 result.out = out;
1177 }
1178 }
1179
1180 cache_.emplace(result.in, result.out);
1181 return {result.in, result.out};
1182}
1183
1184template <class TIn, class TOut, class TDerived>
1187 PaymentSandbox& sb,
1188 ApplyView& afView,
1189 boost::container::flat_set<uint256>& ofrsToRm,
1190 TIn const& in)
1191{
1192 XRPL_ASSERT(cache_, "xrpl::BookStep::fwdImp : cache is set");
1193
1195
1196 auto remainingIn = in;
1197
1198 boost::container::flat_multiset<TIn> savedIns;
1199 savedIns.reserve(64);
1200 boost::container::flat_multiset<TOut> savedOuts;
1201 savedOuts.reserve(64);
1202
1203 // amt fed will be adjusted by owner funds (and may differ from the offer's
1204 // amounts - tho always <=)
1205 auto eachOffer = [&](auto& offer,
1206 TAmounts<TIn, TOut> const& ofrAmt,
1207 TAmounts<TIn, TOut> const& stpAmt,
1208 TOut const& ownerGives,
1209 std::uint32_t transferRateIn,
1210 std::uint32_t transferRateOut) mutable -> bool {
1211 XRPL_ASSERT(cache_, "xrpl::BookStep::fwdImp::eachOffer : cache is set");
1212
1213 if (remainingIn <= beast::kZero)
1214 return false;
1215
1216 bool processMore = true;
1217 auto ofrAdjAmt = ofrAmt;
1218 auto stpAdjAmt = stpAmt;
1219 auto ownerGivesAdj = ownerGives;
1220
1221 // limitStepIn()/limitStepOut() can throw std::overflow_error from the
1222 // transfer-rate mulRatio() on a 63-bit MPT amount. (IOUAmount::mulRatio
1223 // rescales rather than throwing, and XRP amounts/rates stay under the
1224 // int64 limit, so in practice only MPT reaches this today.) execOffer()
1225 // catches it: under featureMPTokensV2 the offending offer is removed;
1226 // otherwise the legacy behavior (propagate the exception) is preserved.
1227 // Keep candidate accumulator changes local until those calls succeed so
1228 // the catch path does not observe partially updated state. Re-sum the
1229 // staged sets to preserve historical flat_multiset summing behavior.
1230 auto savedInsAdj = savedIns;
1231 auto savedOutsAdj = savedOuts;
1232 auto resultAdj = result;
1233 typename boost::container::flat_multiset<TOut>::const_iterator lastOut;
1234
1235 if (stpAmt.in <= remainingIn)
1236 {
1237 savedInsAdj.insert(stpAmt.in);
1238 lastOut = savedOutsAdj.insert(stpAmt.out);
1239 resultAdj = TAmounts<TIn, TOut>(sum(savedInsAdj), sum(savedOutsAdj));
1240 // consume the offer even if stepAmt.in == remainingIn
1241 processMore = true;
1242 }
1243 else
1244 {
1246 offer,
1247 ofrAdjAmt,
1248 stpAdjAmt,
1249 ownerGivesAdj,
1250 transferRateIn,
1251 transferRateOut,
1252 remainingIn);
1253 savedInsAdj.insert(remainingIn);
1254 lastOut = savedOutsAdj.insert(stpAdjAmt.out);
1255 resultAdj.out = sum(savedOutsAdj);
1256 resultAdj.in = in;
1257
1258 processMore = false;
1259 }
1260
1261 if (resultAdj.out > cache_->out && resultAdj.in <= cache_->in)
1262 {
1263 // The step produced more output in the forward pass than the
1264 // reverse pass while consuming the same input (or less). If we
1265 // compute the input required to produce the cached output
1266 // (produced in the reverse step) and the input is equal to
1267 // the input consumed in the forward step, then consume the
1268 // input provided in the forward step and produce the output
1269 // requested from the reverse step.
1270 auto const lastOutAmt = *lastOut;
1271 savedOutsAdj.erase(lastOut);
1272 auto const remainingOut = cache_->out - sum(savedOutsAdj);
1273 auto ofrAdjAmtRev = ofrAmt;
1274 auto stpAdjAmtRev = stpAmt;
1275 auto ownerGivesAdjRev = ownerGives;
1277 offer,
1278 ofrAdjAmtRev,
1279 stpAdjAmtRev,
1280 ownerGivesAdjRev,
1281 transferRateIn,
1282 transferRateOut,
1283 remainingOut);
1284
1285 if (stpAdjAmtRev.in == remainingIn)
1286 {
1287 resultAdj.in = in;
1288 resultAdj.out = cache_->out;
1289
1290 savedInsAdj.clear();
1291 savedInsAdj.insert(resultAdj.in);
1292 savedOutsAdj.clear();
1293 savedOutsAdj.insert(resultAdj.out);
1294
1295 ofrAdjAmt = ofrAdjAmtRev;
1296 stpAdjAmt.in = remainingIn;
1297 stpAdjAmt.out = remainingOut;
1298 ownerGivesAdj = ownerGivesAdjRev;
1299 }
1300 else
1301 {
1302 // This is (likely) a problem case, and will be caught
1303 // with later checks
1304 savedOutsAdj.insert(lastOutAmt);
1305 }
1306 }
1307
1308 // Commit the staged accounting only after limitStepIn()/limitStepOut()
1309 // have succeeded.
1310 savedIns = std::move(savedInsAdj);
1311 savedOuts = std::move(savedOutsAdj);
1312 result = resultAdj;
1313 remainingIn = in - result.in;
1314 this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
1315
1316 // When the mantissas of two iou amounts differ by less than ten, then
1317 // subtracting them leaves a result of zero. This can cause the check
1318 // for (stpAmt.in > remainingIn) to incorrectly think an offer will be
1319 // funded after subtracting remainingIn.
1320 return processMore || offer.fullyConsumed();
1321 };
1322
1323 {
1324 auto const prevStepDebtDir = [&] {
1325 if (prevStep_)
1326 return prevStep_->debtDirection(sb, StrandDirection::Forward);
1327 return DebtDirection::Issues;
1328 }();
1329 auto const r = forEachOffer(sb, afView, prevStepDebtDir, eachOffer);
1330 boost::container::flat_set<uint256> const toRm = std::move(std::get<0>(r));
1331 std::uint32_t const offersConsumed = std::get<1>(r);
1332 offersUsed_ = offersConsumed;
1333 setUnion(ofrsToRm, toRm);
1334
1335 // Too many iterations, mark this strand as inactive (dry)
1336 if (offersConsumed >= kMaxOffersToConsume)
1337 {
1338 inactive_ = true;
1339 }
1340 }
1341
1342 switch (remainingIn.signum())
1343 {
1344 case -1: {
1345 // LCOV_EXCL_START
1346 // something went very wrong
1347 JLOG(j_.error()) << "BookStep remainingIn < 0 " << to_string(remainingIn);
1348 UNREACHABLE("xrpl::BookStep::fwdImp : remaining less than zero");
1350 return {beast::kZero, beast::kZero};
1351 // LCOV_EXCL_STOP
1352 }
1353 case 0: {
1354 // due to normalization, remainingIn can be zero without
1355 // result.in == in. Force result.in == in for this case
1356 result.in = in;
1357 }
1358 }
1359
1360 cache_.emplace(result.in, result.out);
1361 return {result.in, result.out};
1362}
1363
1364template <class TIn, class TOut, class TDerived>
1367 PaymentSandbox& sb,
1368 ApplyView& afView,
1369 EitherAmount const& in)
1370{
1371 if (!cache_)
1372 {
1373 JLOG(j_.trace()) << "Expected valid cache in validFwd";
1374 return {false, EitherAmount(TOut(beast::kZero))};
1375 }
1376
1377 auto const savCache = *cache_;
1378
1379 try
1380 {
1381 boost::container::flat_set<uint256> dummy;
1382 fwdImp(sb, afView, dummy, get<TIn>(in)); // changes cache
1383 }
1384 catch (FlowException const&)
1385 {
1386 return {false, EitherAmount(TOut(beast::kZero))};
1387 }
1388
1389 // NOLINTBEGIN(bugprone-unchecked-optional-access) fwdImp sets cache_ on success
1390 if (!(checkNear(savCache.in, cache_->in) && checkNear(savCache.out, cache_->out)))
1391 {
1392 JLOG(j_.warn()) << "Strand re-execute check failed."
1393 << " ExpectedIn: " << to_string(savCache.in)
1394 << " CachedIn: " << to_string(cache_->in)
1395 << " ExpectedOut: " << to_string(savCache.out)
1396 << " CachedOut: " << to_string(cache_->out);
1397 return {false, EitherAmount(cache_->out)};
1398 }
1399 return {true, EitherAmount(cache_->out)};
1400 // NOLINTEND(bugprone-unchecked-optional-access)
1401}
1402
1403template <class TIn, class TOut, class TDerived>
1404TER
1406{
1407 if (book_.in == book_.out)
1408 {
1409 JLOG(j_.debug()) << "BookStep: Book with same in and out issuer " << *this;
1410 return temBAD_PATH;
1411 }
1412 if (!isConsistent(book_.in) || !isConsistent(book_.out))
1413 {
1414 JLOG(j_.debug()) << "Book: currency is inconsistent with issuer." << *this;
1415 return temBAD_PATH;
1416 }
1417
1418 // Do not allow two books to output the same issue. This may cause offers on
1419 // one step to unfund offers in another step.
1420 if (!ctx.seenBookOuts.insert(book_.out).second ||
1421 (ctx.seenDirectAssets[0].count(book_.out) != 0u))
1422 {
1423 JLOG(j_.debug()) << "BookStep: loop detected: " << *this;
1424 return temBAD_PATH_LOOP;
1425 }
1426
1427 if (ctx.seenDirectAssets[1].count(book_.out) != 0u)
1428 {
1429 JLOG(j_.debug()) << "BookStep: loop detected: " << *this;
1430 return temBAD_PATH_LOOP;
1431 }
1432
1433 auto issuerExists = [](ReadView const& view, Asset const& iss) -> bool {
1434 return isXRP(iss.getIssuer()) || view.exists(keylet::account(iss.getIssuer()));
1435 };
1436
1437 if (!issuerExists(ctx.view, book_.in) || !issuerExists(ctx.view, book_.out))
1438 {
1439 JLOG(j_.debug()) << "BookStep: deleted issuer detected: " << *this;
1440 return tecNO_ISSUER;
1441 }
1442
1443 if (ctx.prevStep != nullptr)
1444 {
1445 if (auto const prev = ctx.prevStep->directStepSrcAcct())
1446 {
1447 auto const& view = ctx.view;
1448 auto const& cur = book_.in.getIssuer();
1449
1450 auto const err = book_.in.visit(
1451 [&](Issue const& issue) -> std::optional<TER> {
1452 auto sle = view.read(keylet::trustLine(*prev, cur, issue.currency));
1453 if (!sle)
1454 return terNO_LINE;
1455 if (sle->isFlag((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple))
1456 return terNO_RIPPLE;
1457 return std::nullopt;
1458 },
1459 [&](MPTIssue const& issue) -> std::optional<TER> { return std::nullopt; });
1460 if (err)
1461 return *err;
1462 }
1463 }
1464
1465 // Check if the offer can be traded on DEX.
1466 if (auto const ter = canTrade(ctx.view, book_.in); !isTesSuccess(ter))
1467 return ter;
1468 if (auto const ter = canTrade(ctx.view, book_.out); !isTesSuccess(ter))
1469 return ter;
1470
1471 return tesSUCCESS;
1472}
1473
1474template <class TIn, class TOut, class TDerived>
1475Rate
1477 ReadView const& view,
1478 Asset const& asset,
1479 AccountID const& dstAccount) const
1480{
1481 return asset.visit(
1482 [&](Issue const& issue) -> Rate {
1483 if (isXRP(issue.account) || issue.account == dstAccount)
1484 return kParityRate;
1485 return transferRate(view, issue.account);
1486 },
1487 [&](MPTIssue const& mptIssue) -> Rate {
1488 // For MPT, parity applies only when this asset is the final strand
1489 // delivery AND the destination is the MPT issuer (holder → issuer,
1490 // which is fee-free). Using strandDst_ alone is wrong because it
1491 // incorrectly suppresses the fee when MPT is an intermediate or
1492 // the in-side of a book that precedes the issuer's XRP receipt.
1493 if (asset == strandDeliver_ && mptIssue.getIssuer() == dstAccount)
1494 return kParityRate;
1495 return transferRate(view, mptIssue.getMptID());
1496 });
1497};
1498
1499template <class TIn, class TOut, class TDerived>
1500bool
1502{
1503 if (!isTesSuccess(canTrade(view, book_.in)) || !isTesSuccess(canTrade(view, book_.out)))
1504 return false;
1505
1506 if (book_.in.holds<MPTIssue>())
1507 {
1508 auto ret = [&]() {
1509 auto const& asset = book_.in;
1510 // Strand's source is an issuer
1511 if (!prevStep_)
1512 return true;
1513 // Offer's owner is an issuer
1514 if (asset.getIssuer() == owner)
1515 return true;
1516 // The previous step could be MPTEndpointStep with non issuer account or
1517 // BookStep. Fail both if in asset is locked. In the former case it is holder
1518 // to locked holder transfer. In the latter case it is not possible to tell if
1519 // it is issuer to holder or holder to holder transfer.
1520 if (isFrozen(view, owner, book_.in.get<MPTIssue>()))
1521 return false;
1522 // Previous step is BookStep. BookStep only sends if CanTransfer is
1523 // set and not locked or the offer is owned by an issuer
1524 if (prevStep_->bookStepBook())
1525 return true;
1526 // Previous step is MPTEndpointStep and offer's owner is not an
1527 // issuer
1528 return isTesSuccess(canTransfer(view, asset, owner, owner));
1529 }();
1530 if (!ret)
1531 return false;
1532 }
1533
1534 if (book_.out.holds<MPTIssue>())
1535 {
1536 auto const& asset = book_.out;
1537 // Last step if the strand's destination is an issuer
1538 if (strandDeliver_ == asset && strandDst_ == asset.getIssuer())
1539 return true;
1540 // Offer's owner is an issuer
1541 if (asset.getIssuer() == owner)
1542 return true;
1543
1544 // Next step is BookStep and offer's owner is not an issuer.
1545 return isTesSuccess(canTransfer(view, asset, owner, owner));
1546 }
1547
1548 return true;
1549}
1550
1551//------------------------------------------------------------------------------
1552
1553namespace test {
1554// Needed for testing
1555
1556template <class TIn, class TOut, class TDerived>
1557static bool
1558equalHelper(Step const& step, xrpl::Book const& book)
1559{
1560 if (auto bs = dynamic_cast<BookStep<TIn, TOut, TDerived> const*>(&step))
1561 return book == bs->book();
1562 return false;
1563}
1564
1565bool
1566bookStepEqual(Step const& step, xrpl::Book const& book)
1567{
1568 return std::visit(
1569 [&]<typename TIn, typename TOut>(TIn const&, TOut const&) {
1570 using TIn_ = TIn::amount_type;
1571 using TOut_ = TOut::amount_type;
1572
1573 if constexpr (ValidTaker<TIn_, TOut_>)
1574 {
1576 }
1577 else
1578 {
1579 // LCOV_EXCL_START
1580 UNREACHABLE("xrpl::bookStepEqual : invalid book step");
1581 return false;
1582 // LCOV_EXCL_STOP
1583 }
1584 },
1585 book.in.getAmountType(),
1586 book.out.getAmountType());
1587}
1588} // namespace test
1589
1590//------------------------------------------------------------------------------
1591
1592template <class TIn, class TOut>
1594makeBookStepHelper(StrandContext const& ctx, Asset const& in, Asset const& out)
1595{
1596 TER ter = tefINTERNAL;
1599 {
1600 auto offerCrossingStep = std::make_unique<BookOfferCrossingStep<TIn, TOut>>(ctx, in, out);
1601 ter = offerCrossingStep->check(ctx);
1602 r = std::move(offerCrossingStep);
1603 }
1604 else // payment
1605 {
1606 auto paymentStep = std::make_unique<BookPaymentStep<TIn, TOut>>(ctx, in, out);
1607 ter = paymentStep->check(ctx);
1608 r = std::move(paymentStep);
1609 }
1610 if (!isTesSuccess(ter))
1611 return {ter, nullptr};
1612
1613 return {tesSUCCESS, std::move(r)};
1614}
1615
1617makeBookStepIi(StrandContext const& ctx, Issue const& in, Issue const& out)
1618{
1619 return makeBookStepHelper<IOUAmount, IOUAmount>(ctx, in, out);
1620}
1621
1623makeBookStepIx(StrandContext const& ctx, Issue const& in)
1624{
1626}
1627
1629makeBookStepXi(StrandContext const& ctx, Issue const& out)
1630{
1632}
1633
1634// MPT's
1636makeBookStepMm(StrandContext const& ctx, MPTIssue const& in, MPTIssue const& out)
1637{
1638 return makeBookStepHelper<MPTAmount, MPTAmount>(ctx, in, out);
1639}
1640
1642makeBookStepMi(StrandContext const& ctx, MPTIssue const& in, Issue const& out)
1643{
1644 return makeBookStepHelper<MPTAmount, IOUAmount>(ctx, in, out);
1645}
1646
1648makeBookStepIm(StrandContext const& ctx, Issue const& in, MPTIssue const& out)
1649{
1650 return makeBookStepHelper<IOUAmount, MPTAmount>(ctx, in, out);
1651}
1652
1655{
1657}
1658
1661{
1663}
1664
1665} // namespace xrpl
T accumulate(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
Represents synthetic AMM offer in BookStep.
Definition AMMOffer.h:30
Quality quality() const noexcept
Definition AMMOffer.h:59
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:134
constexpr auto visit(Visitors &&... visitors) const -> decltype(auto)
Definition Asset.h:117
AccountID const & getIssuer() const
Definition Asset.cpp:21
constexpr bool holds() const
Definition Asset.h:177
std::uint32_t getOfrInRate(Step const *prevStep, AccountID const &owner, std::uint32_t trIn) const
Definition BookStep.cpp:489
std::string logString() const override
Definition BookStep.cpp:552
bool checkQualityThreshold(Quality const &quality) const
Definition BookStep.cpp:462
BookOfferCrossingStep(StrandContext const &ctx, Asset const &in, Asset const &out)
Definition BookStep.cpp:395
Quality adjustQualityWithFees(ReadView const &v, Quality const &ofrQ, DebtDirection prevStepDir, WaiveTransferFee waiveFee, OfferType offerType, Rules const &rules) const
Definition BookStep.cpp:514
bool limitSelfCrossQuality(AccountID const &strandSrc, AccountID const &strandDst, Offer< TIn, TOut > const &offer, std::optional< Quality > &ofrQ, FlowOfferStream< TIn, TOut > &offers, bool const offerAttempted) const
Definition BookStep.cpp:404
static Quality getQuality(std::optional< Quality > const &limitQuality)
Definition BookStep.cpp:385
std::optional< Quality > qualityThreshold(Quality const &lobQuality) const
Definition BookStep.cpp:478
Quality const qualityThreshold_
Definition BookStep.cpp:559
std::uint32_t getOfrOutRate(Step const *prevStep, AccountID const &owner, AccountID const &strandDst, std::uint32_t trOut) const
Definition BookStep.cpp:500
BookPaymentStep(StrandContext const &ctx, Asset const &in, Asset const &out)
Definition BookStep.cpp:290
std::optional< Quality > qualityThreshold(Quality const &lobQuality) const
Definition BookStep.cpp:322
bool limitSelfCrossQuality(AccountID const &, AccountID const &, Offer< TIn, TOut > const &offer, std::optional< Quality > &, FlowOfferStream< TIn, TOut > &, bool) const
Definition BookStep.cpp:301
Quality adjustQualityWithFees(ReadView const &v, Quality const &ofrQ, DebtDirection prevStepDir, WaiveTransferFee waiveFee, OfferType, Rules const &) const
Definition BookStep.cpp:342
std::uint32_t getOfrInRate(Step const *, AccountID const &, std::uint32_t trIn) const
Definition BookStep.cpp:329
std::uint32_t getOfrOutRate(Step const *, AccountID const &, AccountID const &, std::uint32_t trOut) const
Definition BookStep.cpp:336
std::string logString() const override
Definition BookStep.cpp:368
bool checkQualityThreshold(Quality const &quality) const
Definition BookStep.cpp:314
TER check(StrandContext const &ctx) const
AccountID strandSrc_
Definition BookStep.cpp:64
Step const *const prevStep_
Definition BookStep.cpp:67
friend bool operator==(BookStep const &lhs, BookStep const &rhs)
Definition BookStep.cpp:210
std::pair< boost::container::flat_set< uint256 >, std::uint32_t > forEachOffer(PaymentSandbox &sb, ApplyView &afView, DebtDirection prevStepDebtDir, Callback &callback) const
Definition BookStep.cpp:701
std::optional< Book > bookStepBook() const override
Definition BookStep.cpp:153
beast::Journal const j_
Definition BookStep.cpp:84
std::optional< std::variant< Quality, AMMOffer< TIn, TOut > > > tip(ReadView const &view) const
Definition BookStep.cpp:985
std::optional< QualityFunction > tipOfferQualityF(ReadView const &view) const
Asset const strandDeliver_
Definition BookStep.cpp:85
std::optional< EitherAmount > cachedIn() const override
Definition BookStep.cpp:131
std::optional< AMMOffer< TIn, TOut > > getAMMOffer(ReadView const &view, std::optional< Quality > const &clobQuality) const
Definition BookStep.cpp:969
bool const ownerPaysTransferFee_
Definition BookStep.cpp:68
DebtDirection debtDirection(ReadView const &sb, StrandDirection dir) const override
Definition BookStep.cpp:147
BookStep(StrandContext const &ctx, Asset const &in, Asset const &out)
Definition BookStep.cpp:100
void consumeOffer(PaymentSandbox &sb, Offer< TIn, TOut > &offer, TAmounts< TIn, TOut > const &ofrAmt, TAmounts< TIn, TOut > const &stepAmt, TOut const &ownerGives) const
Definition BookStep.cpp:910
std::pair< TIn, TOut > fwdImp(PaymentSandbox &sb, ApplyView &afView, boost::container::flat_set< uint256 > &ofrsToRm, TIn const &in)
bool equal(Step const &rhs) const override
Definition BookStep.cpp:566
std::optional< Cache > cache_
Definition BookStep.cpp:97
std::uint32_t offersUsed_
Number of offers consumed or partially consumed the last time the step ran, including expired and unf...
Definition BookStep.cpp:79
std::optional< EitherAmount > cachedOut() const override
Definition BookStep.cpp:139
std::pair< TIn, TOut > revImp(PaymentSandbox &sb, ApplyView &afView, boost::container::flat_set< uint256 > &ofrsToRm, TOut const &out)
std::pair< std::optional< Quality >, DebtDirection > qualityUpperBound(ReadView const &v, DebtDirection prevStepDir) const override
Definition BookStep.cpp:575
bool inactive() const override
Definition BookStep.cpp:189
friend bool operator!=(BookStep const &lhs, BookStep const &rhs)
Definition BookStep.cpp:216
std::uint32_t offersUsed() const override
Definition BookStep.cpp:628
std::pair< std::optional< QualityFunction >, DebtDirection > getQualityFunc(ReadView const &v, DebtDirection prevStepDir) const override
Definition BookStep.cpp:593
std::optional< AMMLiquidity< TIn, TOut > > ammLiquidity_
Definition BookStep.cpp:83
Rate rate(ReadView const &view, Asset const &asset, AccountID const &dstAccount) const
Book const & book() const
Definition BookStep.cpp:125
static constexpr uint32_t kMaxOffersToConsume
Definition BookStep.cpp:62
std::optional< std::pair< Quality, OfferType > > tipOfferQuality(ReadView const &view) const
AccountID strandDst_
Definition BookStep.cpp:65
std::string logStringImpl(char const *name) const
Definition BookStep.cpp:196
bool checkMPTDEX(ReadView const &view, AccountID const &owner) const
std::pair< bool, EitherAmount > validFwd(PaymentSandbox &sb, ApplyView &afView, EitherAmount const &in) override
Iterates and consumes raw offers in an order book.
Definition BookTip.h:20
bool step(beast::Journal j)
Erases the current offer and advance to the next offer.
Definition BookTip.cpp:19
Quality const & quality() const noexcept
Definition BookTip.h:50
Specifies an order book.
Definition Book.h:28
Presents and consumes the offers in an order book.
A currency issued by an account.
Definition Issue.h:18
Currency currency
Definition Issue.h:20
AccountID account
Definition Issue.h:21
A wrapper which makes credits unavailable to balances.
Average quality of a path as a function of out: q(out) = m * out + b, where m = -1 / poolGets,...
void combine(QualityFunction const &qf)
Combines QF with the next step QF.
Represents the logical ratio of output currency to input currency.
Definition Quality.h:90
A view into a ledger.
Definition ReadView.h:41
virtual Rules const & rules() const =0
Returns the tx processing rules.
NetClock::time_point parentCloseTime() const
Returns the close time of the previous ledger.
Definition ReadView.h:106
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
Rules controlling protocol behavior.
Definition Rules.h:40
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:180
static std::uint64_t const kURateOne
Definition STAmount.h:78
Discardable, editable view to a ledger.
Definition Sandbox.h:18
A step in a payment path.
Definition Steps.h:80
virtual std::optional< AccountID > directStepSrcAcct() const
If this step is DirectStepI (IOU->IOU direct step), return the src account.
Definition Steps.h:139
virtual std::optional< Book > bookStepBook() const
If this step is a BookStep, return the book.
Definition Steps.h:217
Rules const & rules() const override
Returns the tx processing rules.
T empty(T... args)
T get_if(T... args)
T is_same_v
T make_pair(T... args)
T make_unique(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:441
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:253
bool bookStepEqual(Step const &step, xrpl::Book const &book)
static bool equalHelper(Step const &step, xrpl::Book const &book)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terNO_LINE
Definition TER.h:215
@ terNO_RIPPLE
Definition TER.h:220
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
static auto sum(TCollection const &col)
TER checkCreateMPT(xrpl::ApplyView &view, xrpl::MPTIssue const &mptIssue, xrpl::AccountID const &holder, SLE::ref sponsorSle, std::uint32_t flags, beast::Journal j)
std::pair< TER, std::unique_ptr< Step > > makeBookStepIi(StrandContext const &ctx, Issue const &in, Issue const &out)
bool isXRP(AccountID const &c)
Definition AccountID.h:84
static void limitStepOut(Offer const &offer, TAmounts< TIn, TOut > &ofrAmt, TAmounts< TIn, TOut > &stpAmt, TOut &ownerGives, std::uint32_t transferRateIn, std::uint32_t transferRateOut, TOut const &limit)
Definition BookStep.cpp:673
T get(Section const &section, std::string const &name, T const &defaultValue=T{})
Retrieve a key/value pair from a section.
std::pair< TER, std::unique_ptr< Step > > makeBookStepMx(StrandContext const &ctx, MPTIssue const &in)
void setUnion(boost::container::flat_set< T > &dst, boost::container::flat_set< T > const &src)
Given two flat sets dst and src, compute dst = dst union src.
Definition FlatSets.h:16
@ tefINTERNAL
Definition TER.h:165
WaiveTransferFee
std::pair< TER, std::unique_ptr< Step > > makeBookStepMi(StrandContext const &ctx, MPTIssue const &in, Issue const &out)
static void limitStepIn(Offer const &offer, TAmounts< TIn, TOut > &ofrAmt, TAmounts< TIn, TOut > &stpAmt, TOut &ownerGives, std::uint32_t transferRateIn, std::uint32_t transferRateOut, TIn const &limit)
Definition BookStep.cpp:636
TER canTransfer(ReadView const &view, MPTIssue const &mptIssue, AccountID const &from, AccountID const &to, WaiveMPTCanTransfer waive=WaiveMPTCanTransfer::No, std::uint8_t depth=0)
Check whether to may receive the given MPT from from.
DebtDirection
Definition Steps.h:35
std::pair< TER, std::unique_ptr< Step > > makeBookStepIx(StrandContext const &ctx, Issue const &in)
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
TER canTrade(ReadView const &view, Asset const &asset, std::uint8_t depth=0)
Check whether asset may be traded on the DEX.
T toAmount(STAmount const &amt)=delete
StrandDirection
Definition Steps.h:37
STAmount accountFunds(ReadView const &view, AccountID const &id, STAmount const &saDefault, FreezeHandling freezeHandling, beast::Journal j)
static bool isDefaultPath(STPath const &path)
std::pair< TER, std::unique_ptr< Step > > makeBookStepXm(StrandContext const &ctx, MPTIssue const &out)
IOUAmount mulRatio(IOUAmount const &amt, std::uint32_t num, std::uint32_t den, bool roundUp)
Rate transferRate(ReadView const &view, AccountID const &issuer)
Returns IOU issuer transfer fee as Rate.
Rate const kParityRate
A transfer rate signifying a 1:1 exchange.
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition STAmount.cpp:422
std::pair< TER, std::unique_ptr< Step > > makeBookStepXi(StrandContext const &ctx, Issue const &out)
bool checkNear(IOUAmount const &expected, IOUAmount const &actual)
Definition PaySteps.cpp:36
bool isFrozen(ReadView const &view, AccountID const &account, MPTIssue const &mptIssue, std::uint8_t depth=0)
@ TapNone
Definition ApplyView.h:28
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
std::pair< TER, std::unique_ptr< Step > > makeBookStepIm(StrandContext const &ctx, Issue const &in, MPTIssue const &out)
@ temBAD_PATH
Definition TER.h:84
@ temBAD_PATH_LOOP
Definition TER.h:85
bool isTesSuccess(TER x) noexcept
Definition TER.h:676
TERSubset< CanCvtToTER > TER
Definition TER.h:647
bool isConsistent(Asset const &asset)
Definition Asset.h:323
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, std::uint8_t depth=0)
Check if the account lacks required authorization for MPT.
@ tecINVARIANT_FAILED
Definition TER.h:316
@ tecNO_ISSUER
Definition TER.h:302
void issuerSelfDebitHookMPT(ApplyView &view, MPTIssue const &issue, std::uint64_t amount)
Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
bool redeems(DebtDirection dir)
Definition Steps.h:41
static std::pair< TER, std::unique_ptr< Step > > makeBookStepHelper(StrandContext const &ctx, Asset const &in, Asset const &out)
std::pair< TER, std::unique_ptr< Step > > makeBookStepMm(StrandContext const &ctx, MPTIssue const &in, MPTIssue const &out)
Quality composedQuality(Quality const &lhs, Quality const &rhs)
Calculate the quality of a two-hop path given the two hops.
Definition Quality.cpp:114
@ tesSUCCESS
Definition TER.h:245
XRPL_NO_SANITIZE_ADDRESS void Throw(Args &&... args)
Definition contract.h:52
STAmount toSTAmount(IOUAmount const &iou, Asset const &asset)
T str(T... args)
Cache(TIn const &in, TOut const &out)
Definition BookStep.cpp:92
uint256 key
Definition Keylet.h:21
Represents a transfer rate.
Definition Rate.h:21
Context needed to build Strand Steps and for error checking.
Definition Steps.h:525
ReadView const & view
Current ReadView.
Definition Steps.h:526
std::array< boost::container::flat_set< Asset >, 2 > & seenDirectAssets
A strand may not include the same account node more than once in the same currency.
Definition Steps.h:548
boost::container::flat_set< Asset > & seenBookOuts
A strand may not include an offer that output the same issue more than once.
Definition Steps.h:553
Step const *const prevStep
The previous step in the strand.
Definition Steps.h:541
OfferCrossing const offerCrossing
Yes/Sell if offer crossing, not payment.
Definition Steps.h:534
Represents a pair of input and output currencies.
Definition Quality.h:29
T visit(T... args)