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