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 return tecEXPIRED;
181 }
182
183 // Make sure that we are authorized to hold what the taker will pay us.
184 if (!saTakerPays.native())
185 {
186 auto result = checkAcceptAsset(
187 ctx.view,
188 ctx.flags,
189 id,
190 ctx.j,
191 Issue(uPaysCurrency, uPaysIssuerID));
192 if (result != tesSUCCESS)
193 return result;
194 }
195
196 // if domain is specified, make sure that domain exists and the offer create
197 // is part of the domain
198 if (ctx.tx.isFieldPresent(sfDomainID))
199 {
201 ctx.view, id, ctx.tx[sfDomainID]))
202 return tecNO_PERMISSION;
203 }
204
205 return tesSUCCESS;
206}
207
208TER
210 ReadView const& view,
211 ApplyFlags const flags,
212 AccountID const id,
213 beast::Journal const j,
214 Issue const& issue)
215{
216 // Only valid for custom currencies
217 XRPL_ASSERT(
218 !isXRP(issue.currency),
219 "ripple::CreateOffer::checkAcceptAsset : input is not XRP");
220
221 auto const issuerAccount = view.read(keylet::account(issue.account));
222
223 if (!issuerAccount)
224 {
225 JLOG(j.debug())
226 << "delay: can't receive IOUs from non-existent issuer: "
227 << to_string(issue.account);
228
229 return (flags & tapRETRY) ? TER{terNO_ACCOUNT} : TER{tecNO_ISSUER};
230 }
231
232 if (issue.account == id)
233 // An account can always accept its own issuance.
234 return tesSUCCESS;
235
236 if ((*issuerAccount)[sfFlags] & lsfRequireAuth)
237 {
238 auto const trustLine =
239 view.read(keylet::line(id, issue.account, issue.currency));
240
241 if (!trustLine)
242 {
243 return (flags & tapRETRY) ? TER{terNO_LINE} : TER{tecNO_LINE};
244 }
245
246 // Entries have a canonical representation, determined by a
247 // lexicographical "greater than" comparison employing strict weak
248 // ordering. Determine which entry we need to access.
249 bool const canonical_gt(id > issue.account);
250
251 bool const is_authorized(
252 (*trustLine)[sfFlags] & (canonical_gt ? lsfLowAuth : lsfHighAuth));
253
254 if (!is_authorized)
255 {
256 JLOG(j.debug())
257 << "delay: can't receive IOUs from issuer without auth.";
258
259 return (flags & tapRETRY) ? TER{terNO_AUTH} : TER{tecNO_AUTH};
260 }
261 }
262
263 // An account can not create a trustline to itself, so no line can exist
264 // to be frozen. Additionally, an issuer can always accept its own
265 // issuance.
266 if (issue.account == id)
267 {
268 return tesSUCCESS;
269 }
270
271 auto const trustLine =
272 view.read(keylet::line(id, issue.account, issue.currency));
273
274 if (!trustLine)
275 {
276 return tesSUCCESS;
277 }
278
279 // There's no difference which side enacted deep freeze, accepting
280 // tokens shouldn't be possible.
281 bool const deepFrozen =
282 (*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze);
283
284 if (deepFrozen)
285 {
286 return tecFROZEN;
287 }
288
289 return tesSUCCESS;
290}
291
294 PaymentSandbox& psb,
295 PaymentSandbox& psbCancel,
296 Amounts const& takerAmount,
297 std::optional<uint256> const& domainID)
298{
299 try
300 {
301 // If the taker is unfunded before we begin crossing there's nothing
302 // to do - just return an error.
303 //
304 // We check this in preclaim, but when selling XRP charged fees can
305 // cause a user's available balance to go to 0 (by causing it to dip
306 // below the reserve) so we check this case again.
307 STAmount const inStartBalance =
308 accountFunds(psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_);
309 if (inStartBalance <= beast::zero)
310 {
311 // The account balance can't cover even part of the offer.
312 JLOG(j_.debug()) << "Not crossing: taker is unfunded.";
313 return {tecUNFUNDED_OFFER, takerAmount};
314 }
315
316 // If the gateway has a transfer rate, accommodate that. The
317 // gateway takes its cut without any special consent from the
318 // offer taker. Set sendMax to allow for the gateway's cut.
319 Rate gatewayXferRate{QUALITY_ONE};
320 STAmount sendMax = takerAmount.in;
321 if (!sendMax.native() && (account_ != sendMax.getIssuer()))
322 {
323 gatewayXferRate = transferRate(psb, sendMax.getIssuer());
324 if (gatewayXferRate.value != QUALITY_ONE)
325 {
326 sendMax = multiplyRound(
327 takerAmount.in,
328 gatewayXferRate,
329 takerAmount.in.issue(),
330 true);
331 }
332 }
333
334 // Payment flow code compares quality after the transfer rate is
335 // included. Since transfer rate is incorporated compute threshold.
336 Quality threshold{takerAmount.out, sendMax};
337
338 // If we're creating a passive offer adjust the threshold so we only
339 // cross offers that have a better quality than this one.
340 std::uint32_t const txFlags = ctx_.tx.getFlags();
341 if (txFlags & tfPassive)
342 ++threshold;
343
344 // Don't send more than our balance.
345 if (sendMax > inStartBalance)
346 sendMax = inStartBalance;
347
348 // Always invoke flow() with the default path. However if neither
349 // of the takerAmount currencies are XRP then we cross through an
350 // additional path with XRP as the intermediate between two books.
351 // This second path we have to build ourselves.
352 STPathSet paths;
353 if (!takerAmount.in.native() && !takerAmount.out.native())
354 {
355 STPath path;
356 path.emplace_back(std::nullopt, xrpCurrency(), std::nullopt);
357 paths.emplace_back(std::move(path));
358 }
359 // Special handling for the tfSell flag.
360 STAmount deliver = takerAmount.out;
361 OfferCrossing offerCrossing = OfferCrossing::yes;
362 if (txFlags & tfSell)
363 {
364 offerCrossing = OfferCrossing::sell;
365 // We are selling, so we will accept *more* than the offer
366 // specified. Since we don't know how much they might offer,
367 // we allow delivery of the largest possible amount.
368 if (deliver.native())
370 else
371 // We can't use the maximum possible currency here because
372 // there might be a gateway transfer rate to account for.
373 // Since the transfer rate cannot exceed 200%, we use 1/2
374 // maxValue for our limit.
375 deliver = STAmount{
376 takerAmount.out.issue(),
379 }
380
381 // Call the payment engine's flow() to do the actual work.
382 auto const result = flow(
383 psb,
384 deliver,
385 account_,
386 account_,
387 paths,
388 true, // default path
389 !(txFlags & tfFillOrKill), // partial payment
390 true, // owner pays transfer fee
391 offerCrossing,
392 threshold,
393 sendMax,
394 domainID,
395 j_);
396
397 // If stale offers were found remove them.
398 for (auto const& toRemove : result.removableOffers)
399 {
400 if (auto otr = psb.peek(keylet::offer(toRemove)))
401 offerDelete(psb, otr, j_);
402 if (auto otr = psbCancel.peek(keylet::offer(toRemove)))
403 offerDelete(psbCancel, otr, j_);
404 }
405
406 // Determine the size of the final offer after crossing.
407 auto afterCross = takerAmount; // If !tesSUCCESS offer unchanged
408 if (isTesSuccess(result.result()))
409 {
410 STAmount const takerInBalance = accountFunds(
411 psb, account_, takerAmount.in, fhZERO_IF_FROZEN, j_);
412
413 if (takerInBalance <= beast::zero)
414 {
415 // If offer crossing exhausted the account's funds don't
416 // create the offer.
417 afterCross.in.clear();
418 afterCross.out.clear();
419 }
420 else
421 {
422 STAmount const rate{
423 Quality{takerAmount.out, takerAmount.in}.rate()};
424
425 if (txFlags & tfSell)
426 {
427 // If selling then scale the new out amount based on how
428 // much we sold during crossing. This preserves the offer
429 // Quality,
430
431 // Reduce the offer that is placed by the crossed amount.
432 // Note that we must ignore the portion of the
433 // actualAmountIn that may have been consumed by a
434 // gateway's transfer rate.
435 STAmount nonGatewayAmountIn = result.actualAmountIn;
436 if (gatewayXferRate.value != QUALITY_ONE)
437 nonGatewayAmountIn = divideRound(
438 result.actualAmountIn,
439 gatewayXferRate,
440 takerAmount.in.issue(),
441 true);
442
443 afterCross.in -= nonGatewayAmountIn;
444
445 // It's possible that the divRound will cause our subtract
446 // to go slightly negative. So limit afterCross.in to zero.
447 if (afterCross.in < beast::zero)
448 // We should verify that the difference *is* small, but
449 // what is a good threshold to check?
450 afterCross.in.clear();
451
452 afterCross.out = divRoundStrict(
453 afterCross.in, rate, takerAmount.out.issue(), false);
454 }
455 else
456 {
457 // If not selling, we scale the input based on the
458 // remaining output. This too preserves the offer
459 // Quality.
460 afterCross.out -= result.actualAmountOut;
461 XRPL_ASSERT(
462 afterCross.out >= beast::zero,
463 "ripple::CreateOffer::flowCross : minimum offer");
464 if (afterCross.out < beast::zero)
465 afterCross.out.clear();
466 afterCross.in = mulRound(
467 afterCross.out, rate, takerAmount.in.issue(), true);
468 }
469 }
470 }
471
472 // Return how much of the offer is left.
473 return {tesSUCCESS, afterCross};
474 }
475 catch (std::exception const& e)
476 {
477 JLOG(j_.error()) << "Exception during offer crossing: " << e.what();
478 }
479 return {tecINTERNAL, takerAmount};
480}
481
484{
485 std::string txt = amount.getText();
486 txt += "/";
487 txt += to_string(amount.issue().currency);
488 return txt;
489}
490
491TER
493 Sandbox& sb,
495 Keylet const& offerKey,
496 STAmount const& saTakerPays,
497 STAmount const& saTakerGets,
498 std::function<void(SLE::ref, std::optional<uint256>)> const& setDir)
499{
500 if (!sleOffer->isFieldPresent(sfDomainID))
501 return tecINTERNAL; // LCOV_EXCL_LINE
502
503 // set hybrid flag
504 sleOffer->setFlag(lsfHybrid);
505
506 // if offer is hybrid, need to also place into open offer dir
507 Book const book{saTakerPays.issue(), saTakerGets.issue(), std::nullopt};
508
509 auto dir =
510 keylet::quality(keylet::book(book), getRate(saTakerGets, saTakerPays));
511 bool const bookExists = sb.exists(dir);
512
513 auto const bookNode = sb.dirAppend(dir, offerKey, [&](SLE::ref sle) {
514 // don't set domainID on the directory object since this directory is
515 // for open book
516 setDir(sle, std::nullopt);
517 });
518
519 if (!bookNode)
520 {
521 JLOG(j_.debug())
522 << "final result: failed to add hybrid offer to open book";
523 return tecDIR_FULL; // LCOV_EXCL_LINE
524 }
525
526 STArray bookArr(sfAdditionalBooks, 1);
527 auto bookInfo = STObject::makeInnerObject(sfBook);
528 bookInfo.setFieldH256(sfBookDirectory, dir.key);
529 bookInfo.setFieldU64(sfBookNode, *bookNode);
530 bookArr.push_back(std::move(bookInfo));
531
532 if (!bookExists)
534
535 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
536 return tesSUCCESS;
537}
538
541{
542 using beast::zero;
543
544 std::uint32_t const uTxFlags = ctx_.tx.getFlags();
545
546 bool const bPassive(uTxFlags & tfPassive);
547 bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel);
548 bool const bFillOrKill(uTxFlags & tfFillOrKill);
549 bool const bSell(uTxFlags & tfSell);
550 bool const bHybrid(uTxFlags & tfHybrid);
551
552 auto saTakerPays = ctx_.tx[sfTakerPays];
553 auto saTakerGets = ctx_.tx[sfTakerGets];
554 auto const domainID = ctx_.tx[~sfDomainID];
555
556 auto const cancelSequence = ctx_.tx[~sfOfferSequence];
557
558 // Note that we we use the value from the sequence or ticket as the
559 // offer sequence. For more explanation see comments in SeqProxy.h.
560 auto const offerSequence = ctx_.tx.getSeqValue();
561
562 // This is the original rate of the offer, and is the rate at which
563 // it will be placed, even if crossing offers change the amounts that
564 // end up on the books.
565 auto uRate = getRate(saTakerGets, saTakerPays);
566
567 auto viewJ = ctx_.app.journal("View");
568
569 TER result = tesSUCCESS;
570
571 // Process a cancellation request that's passed along with an offer.
572 if (cancelSequence)
573 {
574 auto const sleCancel =
575 sb.peek(keylet::offer(account_, *cancelSequence));
576
577 // It's not an error to not find the offer to cancel: it might have
578 // been consumed or removed. If it is found, however, it's an error
579 // to fail to delete it.
580 if (sleCancel)
581 {
582 JLOG(j_.debug()) << "Create cancels order " << *cancelSequence;
583 result = offerDelete(sb, sleCancel, viewJ);
584 }
585 }
586
587 auto const expiration = ctx_.tx[~sfExpiration];
588
589 if (hasExpired(sb, expiration))
590 {
591 // If the offer has expired, the transaction has successfully
592 // done nothing, so short circuit from here.
593 return {tecEXPIRED, true};
594 }
595
596 bool const bOpenLedger = sb.open();
597 bool crossed = false;
598
599 if (result == tesSUCCESS)
600 {
601 // If a tick size applies, round the offer to the tick size
602 auto const& uPaysIssuerID = saTakerPays.getIssuer();
603 auto const& uGetsIssuerID = saTakerGets.getIssuer();
604
605 std::uint8_t uTickSize = Quality::maxTickSize;
606 if (!isXRP(uPaysIssuerID))
607 {
608 auto const sle = sb.read(keylet::account(uPaysIssuerID));
609 if (sle && sle->isFieldPresent(sfTickSize))
610 uTickSize = std::min(uTickSize, (*sle)[sfTickSize]);
611 }
612 if (!isXRP(uGetsIssuerID))
613 {
614 auto const sle = sb.read(keylet::account(uGetsIssuerID));
615 if (sle && sle->isFieldPresent(sfTickSize))
616 uTickSize = std::min(uTickSize, (*sle)[sfTickSize]);
617 }
618 if (uTickSize < Quality::maxTickSize)
619 {
620 auto const rate =
621 Quality{saTakerGets, saTakerPays}.round(uTickSize).rate();
622
623 // We round the side that's not exact,
624 // just as if the offer happened to execute
625 // at a slightly better (for the placer) rate
626 if (bSell)
627 {
628 // this is a sell, round taker pays
629 saTakerPays = multiply(saTakerGets, rate, saTakerPays.issue());
630 }
631 else
632 {
633 // this is a buy, round taker gets
634 saTakerGets = divide(saTakerPays, rate, saTakerGets.issue());
635 }
636 if (!saTakerGets || !saTakerPays)
637 {
638 JLOG(j_.debug()) << "Offer rounded to zero";
639 return {result, true};
640 }
641
642 uRate = getRate(saTakerGets, saTakerPays);
643 }
644
645 // We reverse pays and gets because during crossing we are taking.
646 Amounts const takerAmount(saTakerGets, saTakerPays);
647
648 JLOG(j_.debug()) << "Attempting cross: "
649 << to_string(takerAmount.in.issue()) << " -> "
650 << to_string(takerAmount.out.issue());
651
652 if (auto stream = j_.trace())
653 {
654 stream << " mode: " << (bPassive ? "passive " : "")
655 << (bSell ? "sell" : "buy");
656 stream << " in: " << format_amount(takerAmount.in);
657 stream << " out: " << format_amount(takerAmount.out);
658 }
659
660 // The amount of the offer that is unfilled after crossing has been
661 // performed. It may be equal to the original amount (didn't cross),
662 // empty (fully crossed), or something in-between.
663 Amounts place_offer;
664 PaymentSandbox psbFlow{&sb};
665 PaymentSandbox psbCancelFlow{&sbCancel};
666
667 std::tie(result, place_offer) =
668 flowCross(psbFlow, psbCancelFlow, takerAmount, domainID);
669 psbFlow.apply(sb);
670 psbCancelFlow.apply(sbCancel);
671
672 // We expect the implementation of cross to succeed
673 // or give a tec.
674 XRPL_ASSERT(
675 result == tesSUCCESS || isTecClaim(result),
676 "ripple::CreateOffer::applyGuts : result is tesSUCCESS or "
677 "tecCLAIM");
678
679 if (auto stream = j_.trace())
680 {
681 stream << "Cross result: " << transToken(result);
682 stream << " in: " << format_amount(place_offer.in);
683 stream << " out: " << format_amount(place_offer.out);
684 }
685
686 if (result == tecFAILED_PROCESSING && bOpenLedger)
687 result = telFAILED_PROCESSING;
688
689 if (result != tesSUCCESS)
690 {
691 JLOG(j_.debug()) << "final result: " << transToken(result);
692 return {result, true};
693 }
694
695 XRPL_ASSERT(
696 saTakerGets.issue() == place_offer.in.issue(),
697 "ripple::CreateOffer::applyGuts : taker gets issue match");
698 XRPL_ASSERT(
699 saTakerPays.issue() == place_offer.out.issue(),
700 "ripple::CreateOffer::applyGuts : taker pays issue match");
701
702 if (takerAmount != place_offer)
703 crossed = true;
704
705 // The offer that we need to place after offer crossing should
706 // never be negative. If it is, something went very very wrong.
707 if (place_offer.in < zero || place_offer.out < zero)
708 {
709 JLOG(j_.fatal()) << "Cross left offer negative!"
710 << " in: " << format_amount(place_offer.in)
711 << " out: " << format_amount(place_offer.out);
712 return {tefINTERNAL, true};
713 }
714
715 if (place_offer.in == zero || place_offer.out == zero)
716 {
717 JLOG(j_.debug()) << "Offer fully crossed!";
718 return {result, true};
719 }
720
721 // We now need to adjust the offer to reflect the amount left after
722 // crossing. We reverse in and out here, since during crossing we
723 // were the taker.
724 saTakerPays = place_offer.out;
725 saTakerGets = place_offer.in;
726 }
727
728 XRPL_ASSERT(
729 saTakerPays > zero && saTakerGets > zero,
730 "ripple::CreateOffer::applyGuts : taker pays and gets positive");
731
732 if (result != tesSUCCESS)
733 {
734 JLOG(j_.debug()) << "final result: " << transToken(result);
735 return {result, true};
736 }
737
738 if (auto stream = j_.trace())
739 {
740 stream << "Place" << (crossed ? " remaining " : " ") << "offer:";
741 stream << " Pays: " << saTakerPays.getFullText();
742 stream << " Gets: " << saTakerGets.getFullText();
743 }
744
745 // For 'fill or kill' offers, failure to fully cross means that the
746 // entire operation should be aborted, with only fees paid.
747 if (bFillOrKill)
748 {
749 JLOG(j_.trace()) << "Fill or Kill: offer killed";
750 return {tecKILLED, false};
751 }
752
753 // For 'immediate or cancel' offers, the amount remaining doesn't get
754 // placed - it gets canceled and the operation succeeds.
755 if (bImmediateOrCancel)
756 {
757 JLOG(j_.trace()) << "Immediate or cancel: offer canceled";
758 if (!crossed)
759 // Any ImmediateOrCancel offer that transfers absolutely no funds
760 // returns tecKILLED rather than tesSUCCESS. Motivation for the
761 // change is here: https://github.com/ripple/rippled/issues/4115
762 return {tecKILLED, false};
763 return {tesSUCCESS, true};
764 }
765
766 auto const sleCreator = sb.peek(keylet::account(account_));
767 if (!sleCreator)
768 return {tefINTERNAL, false};
769
770 {
771 XRPAmount reserve =
772 sb.fees().accountReserve(sleCreator->getFieldU32(sfOwnerCount) + 1);
773
774 if (mPriorBalance < reserve)
775 {
776 // If we are here, the signing account had an insufficient reserve
777 // *prior* to our processing. If something actually crossed, then
778 // we allow this; otherwise, we just claim a fee.
779 if (!crossed)
780 result = tecINSUF_RESERVE_OFFER;
781
782 if (result != tesSUCCESS)
783 {
784 JLOG(j_.debug()) << "final result: " << transToken(result);
785 }
786
787 return {result, true};
788 }
789 }
790
791 // We need to place the remainder of the offer into its order book.
792 auto const offer_index = keylet::offer(account_, offerSequence);
793
794 // Add offer to owner's directory.
795 auto const ownerNode = sb.dirInsert(
797
798 if (!ownerNode)
799 {
800 // LCOV_EXCL_START
801 JLOG(j_.debug())
802 << "final result: failed to add offer to owner's directory";
803 return {tecDIR_FULL, true};
804 // LCOV_EXCL_STOP
805 }
806
807 // Update owner count.
808 adjustOwnerCount(sb, sleCreator, 1, viewJ);
809
810 JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.issue())
811 << " : " << to_string(saTakerGets.issue())
812 << (domainID ? (" : " + to_string(*domainID)) : "");
813
814 Book const book{saTakerPays.issue(), saTakerGets.issue(), domainID};
815
816 // Add offer to order book, using the original rate
817 // before any crossing occured.
818 //
819 // Regular offer - BookDirectory points to open directory
820 //
821 // Domain offer (w/o hyrbid) - BookDirectory points to domain
822 // directory
823 //
824 // Hybrid domain offer - BookDirectory points to domain directory,
825 // and AdditionalBooks field stores one entry that points to the open
826 // directory
827 auto dir = keylet::quality(keylet::book(book), uRate);
828 bool const bookExisted = static_cast<bool>(sb.peek(dir));
829
830 auto setBookDir = [&](SLE::ref sle,
831 std::optional<uint256> const& maybeDomain) {
832 sle->setFieldH160(sfTakerPaysCurrency, saTakerPays.issue().currency);
833 sle->setFieldH160(sfTakerPaysIssuer, saTakerPays.issue().account);
834 sle->setFieldH160(sfTakerGetsCurrency, saTakerGets.issue().currency);
835 sle->setFieldH160(sfTakerGetsIssuer, saTakerGets.issue().account);
836 sle->setFieldU64(sfExchangeRate, uRate);
837 if (maybeDomain)
838 sle->setFieldH256(sfDomainID, *maybeDomain);
839 };
840
841 auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) {
842 // sets domainID on book directory if it's a domain offer
843 setBookDir(sle, domainID);
844 });
845
846 if (!bookNode)
847 {
848 // LCOV_EXCL_START
849 JLOG(j_.debug()) << "final result: failed to add offer to book";
850 return {tecDIR_FULL, true};
851 // LCOV_EXCL_STOP
852 }
853
854 auto sleOffer = std::make_shared<SLE>(offer_index);
855 sleOffer->setAccountID(sfAccount, account_);
856 sleOffer->setFieldU32(sfSequence, offerSequence);
857 sleOffer->setFieldH256(sfBookDirectory, dir.key);
858 sleOffer->setFieldAmount(sfTakerPays, saTakerPays);
859 sleOffer->setFieldAmount(sfTakerGets, saTakerGets);
860 sleOffer->setFieldU64(sfOwnerNode, *ownerNode);
861 sleOffer->setFieldU64(sfBookNode, *bookNode);
862 if (expiration)
863 sleOffer->setFieldU32(sfExpiration, *expiration);
864 if (bPassive)
865 sleOffer->setFlag(lsfPassive);
866 if (bSell)
867 sleOffer->setFlag(lsfSell);
868 if (domainID)
869 sleOffer->setFieldH256(sfDomainID, *domainID);
870
871 // if it's a hybrid offer, set hybrid flag, and create an open dir
872 if (bHybrid)
873 {
874 auto const res = applyHybrid(
875 sb, sleOffer, offer_index, saTakerPays, saTakerGets, setBookDir);
876 if (res != tesSUCCESS)
877 return {res, true}; // LCOV_EXCL_LINE
878 }
879
880 sb.insert(sleOffer);
881
882 if (!bookExisted)
884
885 JLOG(j_.debug()) << "final result: success";
886
887 return {tesSUCCESS, true};
888}
889
890TER
892{
893 // This is the ledger view that we work against. Transactions are applied
894 // as we go on processing transactions.
895 Sandbox sb(&ctx_.view());
896
897 // This is a ledger with just the fees paid and any unfunded or expired
898 // offers we encounter removed. It's used when handling Fill-or-Kill offers,
899 // if the order isn't going to be placed, to avoid wasting the work we did.
900 Sandbox sbCancel(&ctx_.view());
901
902 auto const result = applyGuts(sb, sbCancel);
903 if (result.second)
904 sb.apply(ctx_.rawView());
905 else
906 sbCancel.apply(ctx_.rawView());
907 return result.first;
908}
909
910} // 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.
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.
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)