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