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