xrpld
Loading...
Searching...
No Matches
OfferCreate.cpp
1#include <xrpl/tx/transactors/dex/OfferCreate.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/base_uint.h>
5#include <xrpl/beast/utility/Zero.h>
6#include <xrpl/beast/utility/instrumentation.h>
7#include <xrpl/core/ServiceRegistry.h>
8#include <xrpl/ledger/ApplyView.h>
9#include <xrpl/ledger/OrderBookDB.h>
10#include <xrpl/ledger/PaymentSandbox.h>
11#include <xrpl/ledger/Sandbox.h>
12#include <xrpl/ledger/View.h>
13#include <xrpl/ledger/helpers/AccountRootHelpers.h>
14#include <xrpl/ledger/helpers/DirectoryHelpers.h>
15#include <xrpl/ledger/helpers/MPTokenHelpers.h>
16#include <xrpl/ledger/helpers/OfferHelpers.h>
17#include <xrpl/ledger/helpers/PermissionedDEXHelpers.h>
18#include <xrpl/ledger/helpers/TokenHelpers.h>
19#include <xrpl/protocol/AccountID.h>
20#include <xrpl/protocol/Asset.h>
21#include <xrpl/protocol/Book.h>
22#include <xrpl/protocol/Feature.h>
23#include <xrpl/protocol/Indexes.h>
24#include <xrpl/protocol/Issue.h>
25#include <xrpl/protocol/Keylet.h>
26#include <xrpl/protocol/LedgerFormats.h>
27#include <xrpl/protocol/MPTIssue.h>
28#include <xrpl/protocol/Protocol.h>
29#include <xrpl/protocol/Quality.h>
30#include <xrpl/protocol/Rate.h>
31#include <xrpl/protocol/SField.h>
32#include <xrpl/protocol/STAmount.h>
33#include <xrpl/protocol/STArray.h>
34#include <xrpl/protocol/STLedgerEntry.h>
35#include <xrpl/protocol/STPathSet.h>
36#include <xrpl/protocol/STTx.h>
37#include <xrpl/protocol/SeqProxy.h>
38#include <xrpl/protocol/TER.h>
39#include <xrpl/protocol/TxFlags.h>
40#include <xrpl/protocol/UintTypes.h>
41#include <xrpl/protocol/XRPAmount.h>
42#include <xrpl/tx/Transactor.h>
43#include <xrpl/tx/applySteps.h>
44#include <xrpl/tx/paths/Flow.h>
45#include <xrpl/tx/paths/detail/Steps.h>
46
47#include <algorithm>
48#include <cstdint>
49#include <exception>
50#include <functional>
51#include <memory>
52#include <optional>
53#include <tuple>
54#include <utility>
55
56namespace xrpl {
59{
60 auto calculateMaxXRPSpend = [](STTx const& tx) -> XRPAmount {
61 auto const& amount{tx[sfTakerGets]};
62 return amount.native() ? amount.xrp() : beast::kZero;
63 };
64
65 return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)};
66}
67
68bool
70{
71 if (ctx.tx.isFieldPresent(sfDomainID) && !ctx.rules.enabled(featurePermissionedDEX))
72 return false;
73
74 return ctx.rules.enabled(featureMPTokensV2) ||
75 (!ctx.tx[sfTakerPays].holds<MPTIssue>() && !ctx.tx[sfTakerGets].holds<MPTIssue>());
76}
77
80{
81 // The tfOfferCreateMask is built assuming that PermissionedDEX is
82 // enabled
83 if (ctx.rules.enabled(featurePermissionedDEX))
84 return tfOfferCreateMask;
85 // If PermissionedDEX is not enabled, add tfHybrid to the mask,
86 // indicating it is not allowed.
87 return tfOfferCreateMask | tfHybrid;
88}
89
92{
93 auto& tx = ctx.tx;
94 auto& j = ctx.j;
95
96 if (tx.isFlag(tfHybrid) && !tx.isFieldPresent(sfDomainID))
97 return temINVALID_FLAG;
98
99 // A zero DomainID is invalid for a PermissionedDomain ledger entry because
100 // keylet::permissionedDomain(uint256) uses the DomainID as the ledger key.
101 if (auto const domainID = tx[~sfDomainID];
102 ctx.rules.enabled(fixCleanup3_2_0) && domainID && *domainID == beast::kZero)
103 return temMALFORMED;
104
105 bool const bImmediateOrCancel(tx.isFlag(tfImmediateOrCancel));
106 bool const bFillOrKill(tx.isFlag(tfFillOrKill));
107
108 if (bImmediateOrCancel && bFillOrKill)
109 {
110 JLOG(j.debug()) << "Malformed transaction: both IoC and FoK set.";
111 return temINVALID_FLAG;
112 }
113
114 bool const bHaveExpiration(tx.isFieldPresent(sfExpiration));
115
116 if (bHaveExpiration && (tx.getFieldU32(sfExpiration) == 0))
117 {
118 JLOG(j.debug()) << "Malformed offer: bad expiration";
119 return temBAD_EXPIRATION;
120 }
121
122 if (auto const cancelSequence = tx[~sfOfferSequence]; cancelSequence && *cancelSequence == 0)
123 {
124 JLOG(j.debug()) << "Malformed offer: bad cancel sequence";
125 return temBAD_SEQUENCE;
126 }
127
128 STAmount const saTakerPays = tx[sfTakerPays];
129 STAmount const saTakerGets = tx[sfTakerGets];
130
131 if (!isLegalNet(saTakerPays) || !isLegalNet(saTakerGets))
132 return temBAD_AMOUNT;
133
134 if (saTakerPays.native() && saTakerGets.native())
135 {
136 JLOG(j.debug()) << "Malformed offer: redundant (XRP for XRP)";
137 return temBAD_OFFER;
138 }
139 if (saTakerPays <= beast::kZero || saTakerGets <= beast::kZero)
140 {
141 JLOG(j.debug()) << "Malformed offer: bad amount";
142 return temBAD_OFFER;
143 }
144
145 auto const& uPaysIssuerID = saTakerPays.getIssuer();
146 auto const& uPaysAsset = saTakerPays.asset();
147
148 auto const& uGetsIssuerID = saTakerGets.getIssuer();
149 auto const& uGetsAsset = saTakerGets.asset();
150
151 if (uPaysAsset == uGetsAsset)
152 {
153 JLOG(j.debug()) << "Malformed offer: redundant (IOU for IOU)";
154 return temREDUNDANT;
155 }
156 // We don't allow a non-native currency to use the currency code XRP.
157 if (badAsset() == uPaysAsset || badAsset() == uGetsAsset)
158 {
159 JLOG(j.debug()) << "Malformed offer: bad currency";
160 return temBAD_CURRENCY;
161 }
162
163 if (saTakerPays.native() != !uPaysIssuerID || saTakerGets.native() != !uGetsIssuerID)
164 {
165 JLOG(j.debug()) << "Malformed offer: bad issuer";
166 return temBAD_ISSUER;
167 }
168
169 return tesSUCCESS;
170}
171
172TER
174{
175 auto const id = ctx.tx[sfAccount];
176
177 auto saTakerPays = ctx.tx[sfTakerPays];
178 auto saTakerGets = ctx.tx[sfTakerGets];
179
180 auto const& uPaysAsset = saTakerPays.asset();
181
182 auto const cancelSequence = ctx.tx[~sfOfferSequence];
183
184 auto const sleCreator = ctx.view.read(keylet::account(id));
185 if (!sleCreator)
186 return terNO_ACCOUNT;
187
188 std::uint32_t const uAccountSequence = sleCreator->getFieldU32(sfSequence);
189
190 auto viewJ = ctx.registry.get().getJournal("View");
191
192 if (auto const ter = checkGlobalFrozen(ctx.view, saTakerPays.asset()); !isTesSuccess(ter))
193 {
194 JLOG(ctx.j.debug()) << "Offer involves frozen or locked asset";
195 return ter;
196 }
197 if (auto const ter = checkGlobalFrozen(ctx.view, saTakerGets.asset()); !isTesSuccess(ter))
198 {
199 JLOG(ctx.j.debug()) << "Offer involves frozen or locked asset";
200 return ter;
201 }
202
203 // Allow unfunded MPT for issuer (OutstandingAmount >= MaximumAmount)
204 if ((!saTakerGets.holds<MPTIssue>() || saTakerGets.getIssuer() != id) &&
206 ctx.view,
207 id,
208 saTakerGets,
211 viewJ) <= beast::kZero)
212 {
213 JLOG(ctx.j.debug()) << "delay: Offers must be at least partially funded.";
214 return tecUNFUNDED_OFFER;
215 }
216
217 // This can probably be simplified to make sure that you cancel sequences
218 // before the transaction sequence number.
219 if (cancelSequence && (uAccountSequence <= *cancelSequence))
220 {
221 JLOG(ctx.j.debug()) << "uAccountSequenceNext=" << uAccountSequence
222 << " uOfferSequence=" << *cancelSequence;
223 return temBAD_SEQUENCE;
224 }
225
226 if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
227 {
228 // Note that this will get checked again in applyGuts, but it saves
229 // us a call to checkAcceptAsset and possible false negative.
230 return tecEXPIRED;
231 }
232
233 // Make sure that we are authorized to hold what the taker will pay us.
234 if (!saTakerPays.native())
235 {
236 auto result = checkAcceptAsset(ctx.view, ctx.flags, id, ctx.j, uPaysAsset);
237 if (!isTesSuccess(result))
238 return result;
239 }
240
241 // if domain is specified, make sure that domain exists and the offer create
242 // is part of the domain
243 if (ctx.tx.isFieldPresent(sfDomainID))
244 {
245 if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
246 return tecNO_PERMISSION;
247 }
248
249 if (auto const ter = canTrade(ctx.view, saTakerPays.asset()); !isTesSuccess(ter))
250 return ter;
251 if (auto const ter = canTrade(ctx.view, saTakerGets.asset()); !isTesSuccess(ter))
252 return ter;
253
254 return tesSUCCESS;
255}
256
257TER
259 ReadView const& view,
260 ApplyFlags const flags,
261 AccountID const id,
262 beast::Journal const j,
263 Asset const& asset)
264{
265 // Only valid for custom currencies
266 XRPL_ASSERT(!isXRP(asset), "xrpl::OfferCreate::checkAcceptAsset : input is not XRP");
267
268 auto const issuerAccount = view.read(keylet::account(asset.getIssuer()));
269
270 if (!issuerAccount)
271 {
272 JLOG(j.debug()) << "delay: can't receive IOUs from non-existent issuer: "
273 << to_string(asset.getIssuer());
274
275 return ((flags & TapRetry) != 0u) ? TER{terNO_ACCOUNT} : TER{tecNO_ISSUER};
276 }
277
278 // An account cannot create a trustline to itself, so no line can exist
279 // to be frozen. Additionally, an issuer can always accept its own
280 // issuance.
281 if (asset.getIssuer() == id)
282 return tesSUCCESS;
283
284 return asset.visit(
285 [&](Issue const& issue) -> TER {
286 auto const& issuer = issue.getIssuer();
287 auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency));
288
289 // Check if the issuer has lsfDisallowIncomingTrustline set.
290 // If so, the account must already have a trustline to receive tokens.
291 if (view.rules().enabled(fixCleanup3_4_0) &&
292 issuerAccount->isFlag(lsfDisallowIncomingTrustline))
293 {
294 if (!trustLine)
295 {
296 JLOG(j.debug()) << "delay: can't receive IOUs from issuer with "
297 "DisallowIncomingTrustline set";
298 return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE};
299 }
300 }
301
302 if (issuerAccount->isFlag(lsfRequireAuth))
303 {
304 if (!trustLine)
305 {
306 return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE};
307 }
308
309 // Entries have a canonical representation, determined by a
310 // lexicographical "greater than" comparison employing
311 // strict weak ordering. Determine which entry we need to
312 // access.
313 bool const canonicalGt(id > issuer);
314
315 bool const isAuthorized(trustLine->isFlag(canonicalGt ? lsfLowAuth : lsfHighAuth));
316
317 if (!isAuthorized)
318 {
319 JLOG(j.debug()) << "delay: can't receive IOUs from "
320 "issuer without auth.";
321
322 return ((flags & TapRetry) != 0u) ? TER{terNO_AUTH} : TER{tecNO_AUTH};
323 }
324 }
325
326 if (!trustLine)
327 {
328 return tesSUCCESS;
329 }
330
331 // There's no difference which side enacted deep freeze, accepting
332 // tokens shouldn't be possible.
333 bool const deepFrozen =
334 ((*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze)) != 0u;
335
336 if (deepFrozen)
337 {
338 return tecFROZEN;
339 }
340
341 return tesSUCCESS;
342 },
343 [&](MPTIssue const& issue) -> TER {
344 // WeakAuth - don't check if MPToken exists since it's created
345 // if needed.
346 if (auto const ter = requireAuth(view, issue, id, AuthType::WeakAuth);
347 !isTesSuccess(ter))
348 {
349 return ter;
350 }
351
352 return checkFrozen(view, id, issue);
353 });
354}
355
358 PaymentSandbox& psb,
359 PaymentSandbox& psbCancel,
360 Amounts const& takerAmount,
361 std::optional<uint256> const& domainID)
362{
363 try
364 {
365 // If the taker is unfunded before we begin crossing there's nothing
366 // to do - just return an error.
367 //
368 // We check this in preclaim, but when selling XRP charged fees can
369 // cause a user's available balance to go to 0 (by causing it to dip
370 // below the reserve) so we check this case again.
371 STAmount const inStartBalance = accountFunds(
372 psb,
374 takerAmount.in,
377 j_);
378 // Allow unfunded MPT issuer
379 auto const disallowUnfunded =
380 !inStartBalance.holds<MPTIssue>() || inStartBalance.getIssuer() != accountID_;
381 if (disallowUnfunded && inStartBalance <= beast::kZero)
382 {
383 // The account balance can't cover even part of the offer.
384 JLOG(j_.debug()) << "Not crossing: taker is unfunded.";
385 return {tecUNFUNDED_OFFER, takerAmount};
386 }
387
388 // If the gateway has a transfer rate, accommodate that. The
389 // gateway takes its cut without any special consent from the
390 // offer taker. Set sendMax to allow for the gateway's cut.
391 Rate gatewayXferRate{QUALITY_ONE};
392 STAmount sendMax = takerAmount.in;
393 if (!sendMax.native() && (accountID_ != sendMax.getIssuer()))
394 {
395 gatewayXferRate = transferRate(psb, sendMax);
396 if (gatewayXferRate.value != QUALITY_ONE)
397 {
398 sendMax =
399 multiplyRound(takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
400 }
401 }
402
403 // Payment flow code compares quality after the transfer rate is
404 // included. Since transfer rate is incorporated compute threshold.
405 Quality threshold{takerAmount.out, sendMax};
406
407 // If we're creating a passive offer adjust the threshold so we only
408 // cross offers that have a better quality than this one.
409 if (ctx_.tx.isFlag(tfPassive))
410 ++threshold;
411
412 // Don't send more than our balance.
413 if (sendMax > inStartBalance)
414 sendMax = inStartBalance;
415
416 // Always invoke flow() with the default path. However if neither
417 // of the takerAmount currencies are XRP then we cross through an
418 // additional path with XRP as the intermediate between two books.
419 // This second path we have to build ourselves.
420 STPathSet paths;
421 if (!takerAmount.in.native() && !takerAmount.out.native())
422 {
423 STPath path;
424 path.emplaceBack(std::nullopt, xrpCurrency(), std::nullopt);
425 paths.emplaceBack(std::move(path));
426 }
427 // Special handling for the tfSell flag.
428 STAmount deliver = takerAmount.out;
429 auto const& deliverAsset = deliver.asset();
430 OfferCrossing offerCrossing = OfferCrossing::Yes;
431 if (ctx_.tx.isFlag(tfSell))
432 {
433 offerCrossing = OfferCrossing::Sell;
434 // We are selling, so we will accept *more* than the offer
435 // specified. Since we don't know how much they might offer,
436 // we allow delivery of the largest possible amount.
437 deliver.asset().visit(
438 [&](Issue const& issue) {
439 if (issue.native())
440 {
442 }
443 // We can't use the maximum possible currency here because
444 // there might be a gateway transfer rate to account for.
445 // Since the transfer rate cannot exceed 200%, we use 1/2
446 // maxValue for our limit.
447 else
448 {
449 deliver =
451 }
452 },
453 [&](MPTIssue const&) { deliver = STAmount{deliverAsset, kMaxMpTokenAmount / 2}; });
454 }
455
456 // Call the payment engine's flow() to do the actual work.
457 auto const result = flow(
458 psb,
459 deliver,
462 paths,
463 true, // default path
464 !ctx_.tx.isFlag(tfFillOrKill), // partial payment
465 true, // owner pays transfer fee
466 offerCrossing,
467 threshold,
468 sendMax,
469 domainID,
470 j_);
471
472 // If stale offers were found remove them.
473 for (auto const& toRemove : result.removableOffers)
474 {
475 if (auto otr = psb.peek(keylet::offer(toRemove)))
476 offerDelete(psb, otr, j_);
477 if (auto otr = psbCancel.peek(keylet::offer(toRemove)))
478 offerDelete(psbCancel, otr, j_);
479 }
480
481 // Determine the size of the final offer after crossing.
482 auto afterCross = takerAmount; // If !tesSUCCESS offer unchanged
483 if (isTesSuccess(result.result()))
484 {
485 STAmount const takerInBalance = accountFunds(
486 psb,
488 takerAmount.in,
491 j_);
492
493 if (disallowUnfunded && takerInBalance <= beast::kZero)
494 {
495 // If offer crossing exhausted the account's funds don't
496 // create the offer.
497 afterCross.in.clear();
498 afterCross.out.clear();
499 }
500 else
501 {
502 STAmount const rate{Quality{takerAmount.out, takerAmount.in}.rate()};
503
504 if (ctx_.tx.isFlag(tfSell))
505 {
506 // If selling then scale the new out amount based on how
507 // much we sold during crossing. This preserves the offer
508 // Quality,
509
510 // Reduce the offer that is placed by the crossed amount.
511 // Note that we must ignore the portion of the
512 // actualAmountIn that may have been consumed by a
513 // gateway's transfer rate.
514 STAmount nonGatewayAmountIn = result.actualAmountIn;
515 if (gatewayXferRate.value != QUALITY_ONE)
516 {
517 nonGatewayAmountIn = divideRound(
518 result.actualAmountIn, gatewayXferRate, takerAmount.in.asset(), true);
519 }
520
521 afterCross.in -= nonGatewayAmountIn;
522
523 // It's possible that the divRound will cause our subtract
524 // to go slightly negative. So limit afterCross.in to beast::kZero.
525 if (afterCross.in < beast::kZero)
526 {
527 // We should verify that the difference *is* small, but
528 // what is a good threshold to check?
529 afterCross.in.clear();
530 }
531
532 afterCross.out =
533 divRoundStrict(afterCross.in, rate, takerAmount.out.asset(), false);
534 }
535 else
536 {
537 // If not selling, we scale the input based on the
538 // remaining output. This too preserves the offer
539 // Quality.
540 afterCross.out -= result.actualAmountOut;
541 XRPL_ASSERT(
542 afterCross.out >= beast::kZero,
543 "xrpl::OfferCreate::flowCross : minimum offer");
544 if (afterCross.out < beast::kZero)
545 afterCross.out.clear();
546 afterCross.in = mulRound(afterCross.out, rate, takerAmount.in.asset(), true);
547 }
548 }
549 }
550
551 // Return how much of the offer is left.
552 return {tesSUCCESS, afterCross};
553 }
554 catch (std::exception const& e)
555 {
556 JLOG(j_.error()) << "Exception during offer crossing: " << e.what();
557 }
558 return {tecINTERNAL, takerAmount};
559}
560
563{
564 std::string txt = amount.getText();
565 txt += "/";
566 amount.asset().visit(
567 [&](Issue const& issue) { txt += to_string(issue.currency); },
568 [&](MPTIssue const& issue) { txt += to_string(issue); });
569 return txt;
570}
571
572TER
574 Sandbox& sb,
575 STLedgerEntry::pointer sleOffer,
576 Keylet const& offerKey,
577 STAmount const& saTakerPays,
578 STAmount const& saTakerGets,
579 std::uint64_t openRate,
580 std::function<void(SLE::ref, std::optional<uint256>)> const& setDir)
581{
582 if (!sleOffer->isFieldPresent(sfDomainID))
583 return tecINTERNAL; // LCOV_EXCL_LINE
584
585 // set hybrid flag
586 sleOffer->setFlag(lsfHybrid);
587
588 // if offer is hybrid, need to also place into open offer dir
589 Book const book{saTakerPays.asset(), saTakerGets.asset(), std::nullopt};
590
591 auto dir = keylet::quality(keylet::book(book), openRate);
592 bool const bookExists = sb.exists(dir);
593
594 auto const bookNode = sb.dirAppend(dir, offerKey, [&](SLE::ref sle) {
595 // don't set domainID on the directory object since this directory is
596 // for open book
597 setDir(sle, std::nullopt);
598 });
599
600 if (!bookNode)
601 {
602 JLOG(j_.debug()) << "final result: failed to add hybrid offer to open book";
603 return tecDIR_FULL; // LCOV_EXCL_LINE
604 }
605
606 STArray bookArr(sfAdditionalBooks, 1);
607 auto bookInfo = STObject::makeInnerObject(sfBook);
608 bookInfo.setFieldH256(sfBookDirectory, dir.key);
609 bookInfo.setFieldU64(sfBookNode, *bookNode);
610 bookArr.pushBack(std::move(bookInfo));
611
612 if (!bookExists)
613 ctx_.registry.get().getOrderBookDB().addOrderBook(book);
614
615 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
616 return tesSUCCESS;
617}
618
621{
622 using beast::kZero;
623
624 bool const bPassive(ctx_.tx.isFlag(tfPassive));
625 bool const bImmediateOrCancel(ctx_.tx.isFlag(tfImmediateOrCancel));
626 bool const bFillOrKill(ctx_.tx.isFlag(tfFillOrKill));
627 bool const bSell(ctx_.tx.isFlag(tfSell));
628 bool const bHybrid(ctx_.tx.isFlag(tfHybrid));
629
630 auto saTakerPays = ctx_.tx[sfTakerPays];
631 auto saTakerGets = ctx_.tx[sfTakerGets];
632 auto const domainID = ctx_.tx[~sfDomainID];
633
634 auto const cancelSequence = ctx_.tx[~sfOfferSequence];
635
636 // Note that we use the value from the sequence or ticket as the
637 // offer sequence. For more explanation see comments in SeqProxy.h.
638 auto const offerSequence = ctx_.tx.getSeqProxy();
639
640 // This is the original rate of the offer, and is the rate at which
641 // it will be placed, even if crossing offers change the amounts that
642 // end up on the books.
643 auto uRate = getRate(saTakerGets, saTakerPays);
644
645 auto viewJ = ctx_.registry.get().getJournal("View");
646
647 TER result = tesSUCCESS;
648
649 // Process a cancellation request that's passed along with an offer.
650 if (cancelSequence)
651 {
652 auto const seqProxy = SeqProxy::rawSequence(*cancelSequence);
653 auto const sleCancel = sb.peek(keylet::offer(accountID_, seqProxy));
654
655 // It's not an error to not find the offer to cancel: it might have
656 // been consumed or removed. If it is found, however, it's an error
657 // to fail to delete it.
658 if (sleCancel)
659 {
660 JLOG(j_.debug()) << "Create cancels order " << *cancelSequence;
661 result = offerDelete(sb, sleCancel, viewJ);
662 }
663 }
664
665 auto const expiration = ctx_.tx[~sfExpiration];
666
667 if (hasExpired(sb, expiration))
668 {
669 // If the offer has expired, the transaction has successfully
670 // done nothing, so short circuit from here.
671 return {tecEXPIRED, true};
672 }
673
674 bool crossed = false;
675
676 if (isTesSuccess(result))
677 {
678 // If a tick size applies, round the offer to the tick size
679 auto const& uPaysIssuerID = saTakerPays.getIssuer();
680 auto const& uGetsIssuerID = saTakerGets.getIssuer();
681
683 // Not XRP or MPT
684 if (!saTakerPays.integral())
685 {
686 auto const sle = sb.read(keylet::account(uPaysIssuerID));
687 if (sle && sle->isFieldPresent(sfTickSize))
688 uTickSize = std::min(uTickSize, (*sle)[sfTickSize]);
689 }
690 // Not XRP or MPT
691 if (!saTakerGets.integral())
692 {
693 auto const sle = sb.read(keylet::account(uGetsIssuerID));
694 if (sle && sle->isFieldPresent(sfTickSize))
695 uTickSize = std::min(uTickSize, (*sle)[sfTickSize]);
696 }
697 if (uTickSize < Quality::kMaxTickSize)
698 {
699 auto const rate = Quality{saTakerGets, saTakerPays}.round(uTickSize).rate();
700
701 // We round the side that's not exact,
702 // just as if the offer happened to execute
703 // at a slightly better (for the placer) rate
704 if (bSell)
705 {
706 // this is a sell, round taker pays
707 if (!saTakerPays.holds<MPTIssue>())
708 saTakerPays = multiply(saTakerGets, rate, saTakerPays.asset());
709 }
710 else if (!saTakerGets.holds<MPTIssue>())
711 {
712 // this is a buy, round taker gets
713 saTakerGets = divide(saTakerPays, rate, saTakerGets.asset());
714 }
715 if (!saTakerGets || !saTakerPays)
716 {
717 JLOG(j_.debug()) << "Offer rounded to zero";
718 return {result, true};
719 }
720
721 uRate = getRate(saTakerGets, saTakerPays);
722 }
723
724 // We reverse pays and gets because during crossing we are taking.
725 Amounts const takerAmount(saTakerGets, saTakerPays);
726
727 JLOG(j_.debug()) << "Attempting cross: " << to_string(takerAmount.in.asset()) << " -> "
728 << to_string(takerAmount.out.asset());
729
730 if (auto stream = j_.trace())
731 {
732 stream << " mode: " << (bPassive ? "passive " : "") << (bSell ? "sell" : "buy");
733 stream << " in: " << formatAmount(takerAmount.in);
734 stream << " out: " << formatAmount(takerAmount.out);
735 }
736
737 // The amount of the offer that is unfilled after crossing has been
738 // performed. It may be equal to the original amount (didn't cross),
739 // empty (fully crossed), or something in-between.
740 Amounts placeOffer;
741 PaymentSandbox psbFlow{&sb};
742 PaymentSandbox psbCancelFlow{&sbCancel};
743
744 std::tie(result, placeOffer) = flowCross(psbFlow, psbCancelFlow, takerAmount, domainID);
745 psbFlow.apply(sb);
746 psbCancelFlow.apply(sbCancel);
747
748 // We expect the implementation of cross to succeed
749 // or give a tec.
750 XRPL_ASSERT(
751 isTesSuccess(result) || isTecClaim(result),
752 "xrpl::OfferCreate::applyGuts : result is tesSUCCESS or "
753 "tecCLAIM");
754
755 if (auto stream = j_.trace())
756 {
757 stream << "Cross result: " << transToken(result);
758 stream << " in: " << formatAmount(placeOffer.in);
759 stream << " out: " << formatAmount(placeOffer.out);
760 }
761
762 if (result == tecFAILED_PROCESSING && sb.open())
763 result = telFAILED_PROCESSING;
764
765 if (!isTesSuccess(result))
766 {
767 JLOG(j_.debug()) << "final result: " << transToken(result);
768 return {result, true};
769 }
770
771 XRPL_ASSERT(
772 saTakerGets.asset() == placeOffer.in.asset(),
773 "xrpl::OfferCreate::applyGuts : taker gets issue match");
774 XRPL_ASSERT(
775 saTakerPays.asset() == placeOffer.out.asset(),
776 "xrpl::OfferCreate::applyGuts : taker pays issue match");
777
778 if (takerAmount != placeOffer)
779 crossed = true;
780
781 // The offer that we need to place after offer crossing should
782 // never be negative. If it is, something went very very wrong.
783 if (placeOffer.in < kZero || placeOffer.out < kZero)
784 {
785 JLOG(j_.fatal()) << "Cross left offer negative!"
786 << " in: " << formatAmount(placeOffer.in)
787 << " out: " << formatAmount(placeOffer.out);
788 return {tefINTERNAL, true};
789 }
790
791 if (placeOffer.in == kZero || placeOffer.out == kZero)
792 {
793 JLOG(j_.debug()) << "Offer fully crossed!";
794 return {result, true};
795 }
796
797 // We now need to adjust the offer to reflect the amount left after
798 // crossing. We reverse in and out here, since during crossing we
799 // were the taker.
800 saTakerPays = placeOffer.out;
801 saTakerGets = placeOffer.in;
802 }
803
804 XRPL_ASSERT(
805 saTakerPays > beast::kZero && saTakerGets > beast::kZero,
806 "xrpl::OfferCreate::applyGuts : taker pays and gets positive");
807
808 if (!isTesSuccess(result))
809 {
810 JLOG(j_.debug()) << "final result: " << transToken(result);
811 return {result, true};
812 }
813
814 if (auto stream = j_.trace())
815 {
816 stream << "Place" << (crossed ? " remaining " : " ") << "offer:";
817 stream << " Pays: " << saTakerPays.getFullText();
818 stream << " Gets: " << saTakerGets.getFullText();
819 }
820
821 // For 'fill or kill' offers, failure to fully cross means that the
822 // entire operation should be aborted, with only fees paid.
823 if (bFillOrKill)
824 {
825 JLOG(j_.trace()) << "Fill or Kill: offer killed";
826 return {tecKILLED, false};
827 }
828
829 // For 'immediate or cancel' offers, the amount remaining doesn't get
830 // placed - it gets canceled and the operation succeeds.
831 if (bImmediateOrCancel)
832 {
833 JLOG(j_.trace()) << "Immediate or cancel: offer canceled";
834 if (!crossed)
835 {
836 // Any ImmediateOrCancel offer that transfers absolutely no funds
837 // returns tecKILLED rather than tesSUCCESS. Motivation for the
838 // change is here: https://github.com/XRPLF/rippled/issues/4115
839 return {tecKILLED, false};
840 }
841 return {tesSUCCESS, true};
842 }
843
844 auto const sleCreator = sb.peek(keylet::account(accountID_));
845 if (!sleCreator)
846 return {tefINTERNAL, false};
847
848 {
849 XRPAmount const reserve = accountReserve(sb, sleCreator, viewJ, {.ownerCountDelta = 1});
850 if (preFeeBalance_ < reserve)
851 {
852 // If we are here, the signing account had an insufficient reserve
853 // *prior* to our processing. If something actually crossed, then
854 // we allow this; otherwise, we just claim a fee.
855 if (!crossed)
856 result = tecINSUF_RESERVE_OFFER;
857
858 if (!isTesSuccess(result))
859 {
860 JLOG(j_.debug()) << "final result: " << transToken(result);
861 }
862
863 return {result, true};
864 }
865 }
866
867 // We need to place the remainder of the offer into its order book.
868 auto const offerIndex = keylet::offer(accountID_, offerSequence);
869
870 // Add offer to owner's directory.
871 auto const ownerNode =
873
874 if (!ownerNode)
875 {
876 // LCOV_EXCL_START
877 JLOG(j_.debug()) << "final result: failed to add offer to owner's directory";
878 return {tecDIR_FULL, true};
879 // LCOV_EXCL_STOP
880 }
881
882 // Update owner count.
883 increaseOwnerCount(sb, sleCreator, {}, 1, viewJ);
884
885 JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) << " : "
886 << to_string(saTakerGets.asset())
887 << (domainID ? (" : " + to_string(*domainID)) : "");
888
889 Book const book{saTakerPays.asset(), saTakerGets.asset(), domainID};
890
891 // Add offer to order book, using the original rate
892 // before any crossing occurred.
893 //
894 // Regular offer - BookDirectory points to open directory
895 //
896 // Domain offer (w/o hybrid) - BookDirectory points to domain
897 // directory
898 //
899 // Hybrid domain offer - BookDirectory points to domain directory,
900 // and AdditionalBooks field stores one entry that points to the open
901 // directory
902 auto dir = keylet::quality(keylet::book(book), uRate);
903 bool const bookExisted = static_cast<bool>(sb.peek(dir));
904
905 auto setBookDir = [&](SLE::ref sle, std::optional<uint256> const& maybeDomain) {
906 saTakerPays.asset().visit(
907 [&](Issue const& issue) {
908 sle->setFieldH160(sfTakerPaysCurrency, issue.currency);
909 sle->setFieldH160(sfTakerPaysIssuer, issue.account);
910 },
911 [&](MPTIssue const& issue) { sle->setFieldH192(sfTakerPaysMPT, issue.getMptID()); });
912 saTakerGets.asset().visit(
913 [&](Issue const& issue) {
914 sle->setFieldH160(sfTakerGetsCurrency, issue.currency);
915 sle->setFieldH160(sfTakerGetsIssuer, issue.account);
916 },
917 [&](MPTIssue const& issue) { sle->setFieldH192(sfTakerGetsMPT, issue.getMptID()); });
918 sle->setFieldU64(sfExchangeRate, uRate);
919 if (maybeDomain)
920 sle->setFieldH256(sfDomainID, *maybeDomain);
921 };
922
923 auto const bookNode = sb.dirAppend(dir, offerIndex, [&](SLE::ref sle) {
924 // sets domainID on book directory if it's a domain offer
925 setBookDir(sle, domainID);
926 });
927
928 if (!bookNode)
929 {
930 // LCOV_EXCL_START
931 JLOG(j_.debug()) << "final result: failed to add offer to book";
932 return {tecDIR_FULL, true};
933 // LCOV_EXCL_STOP
934 }
935
936 auto sleOffer = std::make_shared<SLE>(offerIndex);
937 sleOffer->setAccountID(sfAccount, accountID_);
938 sleOffer->setFieldU32(sfSequence, offerSequence.value());
939 sleOffer->setFieldH256(sfBookDirectory, dir.key);
940 sleOffer->setFieldAmount(sfTakerPays, saTakerPays);
941 sleOffer->setFieldAmount(sfTakerGets, saTakerGets);
942 sleOffer->setFieldU64(sfOwnerNode, *ownerNode);
943 sleOffer->setFieldU64(sfBookNode, *bookNode);
944 if (expiration)
945 sleOffer->setFieldU32(sfExpiration, *expiration);
946 if (bPassive)
947 sleOffer->setFlag(lsfPassive);
948 if (bSell)
949 sleOffer->setFlag(lsfSell);
950 if (domainID)
951 sleOffer->setFieldH256(sfDomainID, *domainID);
952
953 // if it's a hybrid offer, set hybrid flag, and create an open dir
954 if (bHybrid)
955 {
956 // Pre-fixCleanup3_2_0: the open-book directory quality was computed
957 // from post-crossing amounts, which may differ from the original rate
958 // due to rounding in rate preservation. Post-fixCleanup3_2_0: use the
959 // original placement rate so the open-book directory quality matches
960 // the domain-book directory.
961 auto const openRate = ctx_.view().rules().enabled(fixCleanup3_2_0)
962 ? uRate
963 : getRate(saTakerGets, saTakerPays);
964 auto const res =
965 applyHybrid(sb, sleOffer, offerIndex, saTakerPays, saTakerGets, openRate, setBookDir);
966 if (!isTesSuccess(res))
967 return {res, true}; // LCOV_EXCL_LINE
968 }
969
970 sb.insert(sleOffer);
971
972 if (!bookExisted)
973 ctx_.registry.get().getOrderBookDB().addOrderBook(book);
974
975 JLOG(j_.debug()) << "final result: success";
976
977 return {tesSUCCESS, true};
978}
979
980TER
982{
983 // This is the ledger view that we work against. Transactions are applied
984 // as we go on processing transactions.
985 Sandbox sb(&ctx_.view());
986
987 // This is a ledger with just the fees paid and any unfunded or expired
988 // offers we encounter removed. It's used when handling Fill-or-Kill offers,
989 // if the order isn't going to be placed, to avoid wasting the work we did.
990 Sandbox sbCancel(&ctx_.view());
991
992 auto const result = applyGuts(sb, sbCancel);
993 if (result.second)
994 {
995 sb.apply(ctx_.rawView());
996 }
997 else
998 {
999 sbCancel.apply(ctx_.rawView());
1000 }
1001 return result.first;
1002}
1003
1004void
1006{
1007 // No transaction-specific invariants yet (future work).
1008}
1009
1010bool
1012{
1013 // No transaction-specific invariants yet (future work).
1014 return true;
1015}
1016
1017} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
Stream debug() const
Definition Journal.h:344
std::optional< std::uint64_t > dirAppend(Keylet const &directory, Keylet const &key, std::function< void(SLE::ref)> const &describe)
Append an entry to a directory.
Definition ApplyView.h:326
std::optional< std::uint64_t > dirInsert(Keylet const &directory, uint256 const &key, std::function< void(SLE::ref)> const &describe)
Insert an entry to a directory.
Definition ApplyView.h:366
constexpr auto visit(Visitors &&... visitors) const -> decltype(auto)
Definition Asset.h:117
AccountID const & getIssuer() const
Definition Asset.cpp:21
Specifies an order book.
Definition Book.h:28
A currency issued by an account.
Definition Issue.h:18
Currency currency
Definition Issue.h:20
AccountID account
Definition Issue.h:21
bool native() const
Definition Issue.cpp:54
AccountID const & getIssuer() const
Definition Issue.h:30
AccountID const & getIssuer() const
Definition MPTIssue.cpp:29
static TER preclaim(PreclaimContext const &ctx)
Enforce constraints beyond those of the Transactor base class.
static TER checkAcceptAsset(ReadView const &view, ApplyFlags const flags, AccountID const id, beast::Journal const j, Asset const &asset)
bool finalizeInvariants(STTx const &tx, TER result, XRPAmount fee, ReadView const &view, beast::Journal const &j) override
Check transaction-specific post-conditions after all entries have been visited.
TER doApply() override
Precondition: fee collection is likely.
static TxConsequences makeTxConsequences(PreflightContext const &ctx)
std::pair< TER, Amounts > flowCross(PaymentSandbox &psb, PaymentSandbox &psbCancel, Amounts const &takerAmount, std::optional< uint256 > const &domainID)
static bool checkExtraFeatures(PreflightContext const &ctx)
void visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override
Inspect a single ledger entry modified by this transaction.
static NotTEC preflight(PreflightContext const &ctx)
Enforce constraints beyond those of the Transactor base class.
TER applyHybrid(Sandbox &sb, STLedgerEntry::pointer sleOffer, Keylet const &offerIndex, STAmount const &saTakerPays, STAmount const &saTakerGets, std::uint64_t openRate, std::function< void(SLE::ref, std::optional< uint256 >)> const &setDir)
std::pair< TER, bool > applyGuts(Sandbox &view, Sandbox &viewCancel)
static std::string formatAmount(STAmount const &amount)
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
A wrapper which makes credits unavailable to balances.
void apply(RawView &to)
Apply changes to base view.
Represents the logical ratio of output currency to input currency.
Definition Quality.h:90
static int const kMaxTickSize
Definition Quality.h:97
STAmount rate() const
Returns the quality as STAmount.
Definition Quality.h:162
Quality round(int tickSize) const
Returns the quality rounded up to the specified number of decimal digits.
Definition Quality.cpp:134
A view into a ledger.
Definition ReadView.h:41
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:180
constexpr bool holds() const noexcept
Definition STAmount.h:478
std::string getFullText() const override
Definition STAmount.cpp:636
std::string getText() const override
Definition STAmount.cpp:646
bool integral() const noexcept
Definition STAmount.h:465
bool native() const noexcept
Definition STAmount.h:471
Asset const & asset() const
Definition STAmount.h:496
AccountID const & getIssuer() const
Definition STAmount.h:516
static constexpr std::uint64_t kMaxValue
Definition STAmount.h:67
static constexpr std::uint64_t kMaxNative
Definition STAmount.h:69
static constexpr int kMaxOffset
Definition STAmount.h:62
void pushBack(STObject const &object)
Definition STArray.h:212
std::shared_ptr< STLedgerEntry > const & ref
std::shared_ptr< STLedgerEntry > pointer
std::shared_ptr< STLedgerEntry const > const & const_ref
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:464
static STObject makeInnerObject(SField const &name)
Definition STObject.cpp:79
void emplaceBack(Args &&... args)
Definition STPathSet.h:557
Discardable, editable view to a ledger.
Definition Sandbox.h:18
void apply(RawView &to)
Definition Sandbox.h:38
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
beast::Journal const j_
Definition Transactor.h:155
ApplyView & view()
Definition Transactor.h:175
AccountID const accountID_
Definition Transactor.h:157
XRPAmount preFeeBalance_
Definition Transactor.h:158
ApplyContext & ctx_
Definition Transactor.h:153
Class describing the consequences to the account of applying a transaction if the transaction consume...
Definition applySteps.h:52
void insert(SLE::ref sle) override
Insert a new state SLE.
SLE::pointer peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
bool open() const override
Returns true if this reflects an open ledger.
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
bool exists(Keylet const &k) const override
Determine if a state item exists.
T make_shared(T... args)
T min(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet quality(Keylet const &k, std::uint64_t const q) noexcept
The initial directory page for a specific quality.
Definition Indexes.cpp:282
Keylet offer(AccountID const &id, SeqProxy const &seq) noexcept
An offer from an account.
Definition Indexes.cpp:276
Keylet book(Book const &b)
The beginning of an order book.
Definition Indexes.cpp:247
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:373
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 accountInDomain(ReadView const &view, AccountID const &account, Domain const &domainID)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ telFAILED_PROCESSING
Definition TER.h:42
STAmount divide(STAmount const &amount, Rate const &rate)
Definition Rate2.cpp:69
@ terNO_LINE
Definition TER.h:215
@ terNO_AUTH
Definition TER.h:214
@ terNO_ACCOUNT
Definition TER.h:213
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp, ExpiryComparison comparison=ExpiryComparison::Inclusive)
Determines whether the given expiration time has passed.
Definition View.cpp:48
bool isXRP(AccountID const &c)
Definition AccountID.h:84
void increaseOwnerCount(ApplyView &view, SLE::ref accountSle, SLE::ref sponsorSle, std::uint32_t count, beast::Journal j)
Increase owner-count fields when the caller supplies the sponsor.
TAmounts< STAmount, STAmount > Amounts
Definition Quality.h:69
TER checkFrozen(ReadView const &view, AccountID const &account, Issue const &issue)
@ tefINTERNAL
Definition TER.h:165
bool isLegalNet(STAmount const &value)
Definition STAmount.h:616
std::string transToken(TER code)
Definition TER.cpp:251
TER offerDelete(ApplyView &view, SLE::ref sle, beast::Journal j)
Delete an offer.
Currency const & xrpCurrency()
XRP currency.
Definition UintTypes.cpp:99
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.
STAmount accountFunds(ReadView const &view, AccountID const &id, STAmount const &saDefault, FreezeHandling freezeHandling, beast::Journal j)
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:607
StrandResult< TInAmt, TOutAmt > flow(PaymentSandbox const &baseView, Strand const &strand, std::optional< TInAmt > const &maxIn, TOutAmt const &out, beast::Journal j)
Request out amount from a strand.
Definition StrandFlow.h:103
Rate transferRate(ReadView const &view, AccountID const &issuer)
Returns IOU issuer transfer fee as Rate.
STAmount multiplyRound(STAmount const &amount, Rate const &rate, bool roundUp)
Definition Rate2.cpp:45
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition STAmount.cpp:422
std::function< void(SLE::ref)> describeOwnerDir(AccountID const &account)
Returns a function that sets the owner on a directory SLE.
TER checkGlobalFrozen(ReadView const &view, Asset const &asset)
ApplyFlags
Definition ApplyView.h:27
@ TapRetry
Definition ApplyView.h:36
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
STAmount mulRound(STAmount const &v1, STAmount const &v2, Asset const &asset, bool roundUp)
@ temBAD_ISSUER
Definition TER.h:81
@ temBAD_CURRENCY
Definition TER.h:78
@ temBAD_EXPIRATION
Definition TER.h:79
@ temBAD_SEQUENCE
Definition TER.h:92
@ temINVALID_FLAG
Definition TER.h:99
@ temMALFORMED
Definition TER.h:75
@ temBAD_AMOUNT
Definition TER.h:77
@ temBAD_OFFER
Definition TER.h:83
@ temREDUNDANT
Definition TER.h:100
bool isTesSuccess(TER x) noexcept
Definition TER.h:676
STAmount divRoundStrict(STAmount const &v1, STAmount const &v2, Asset const &asset, bool roundUp)
TERSubset< CanCvtToTER > TER
Definition TER.h:647
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.
@ tecINSUF_RESERVE_OFFER
Definition TER.h:292
@ tecDIR_FULL
Definition TER.h:290
@ tecNO_AUTH
Definition TER.h:303
@ tecINTERNAL
Definition TER.h:313
@ tecFAILED_PROCESSING
Definition TER.h:289
@ tecFROZEN
Definition TER.h:306
@ tecUNFUNDED_OFFER
Definition TER.h:287
@ tecEXPIRED
Definition TER.h:317
@ tecNO_LINE
Definition TER.h:304
@ tecKILLED
Definition TER.h:319
@ tecNO_PERMISSION
Definition TER.h:308
@ tecNO_ISSUER
Definition TER.h:302
STAmount multiply(STAmount const &amount, Number const &frac, Number::RoundingMode rm)
bool isTecClaim(TER x) noexcept
Definition TER.h:683
BadAsset const & badAsset()
Definition Asset.h:40
OfferCrossing
Definition Steps.h:38
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:296
STAmount divideRound(STAmount const &amount, Rate const &rate, bool roundUp)
Definition Rate2.cpp:80
XRPAmount accountReserve(ReadView const &view, SLE::const_ref sle, beast::Journal j, Adjustment adj={})
Returns the account reserve, in drops.
@ tesSUCCESS
Definition TER.h:245
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:83
ReadView const & view
Definition Transactor.h:86
std::reference_wrapper< ServiceRegistry > registry
Definition Transactor.h:85
beast::Journal const j
Definition Transactor.h:91
State information when preflighting a tx.
Definition Transactor.h:38
beast::Journal const j
Definition Transactor.h:45
Represents a transfer rate.
Definition Rate.h:21
std::uint32_t value
Definition Rate.h:22
T tie(T... args)
T what(T... args)