xrpld
Loading...
Searching...
No Matches
VaultInvariant.cpp
1#include <xrpl/tx/invariants/VaultInvariant.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Number.h>
5#include <xrpl/beast/utility/Journal.h>
6#include <xrpl/beast/utility/Zero.h>
7#include <xrpl/beast/utility/instrumentation.h>
8#include <xrpl/ledger/ReadView.h>
9#include <xrpl/ledger/helpers/AccountRootHelpers.h>
10#include <xrpl/ledger/helpers/VaultHelpers.h>
11#include <xrpl/protocol/Feature.h>
12#include <xrpl/protocol/Indexes.h>
13#include <xrpl/protocol/Issue.h>
14#include <xrpl/protocol/LedgerFormats.h>
15#include <xrpl/protocol/Protocol.h>
16#include <xrpl/protocol/SField.h>
17#include <xrpl/protocol/STAmount.h>
18#include <xrpl/protocol/STLedgerEntry.h>
19#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
20#include <xrpl/protocol/STTx.h>
21#include <xrpl/protocol/TER.h>
22#include <xrpl/protocol/TxFormats.h>
23#include <xrpl/protocol/XRPAmount.h>
24#include <xrpl/tx/Transactor.h>
25#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
26
27#include <algorithm>
28#include <cstdint>
29#include <optional>
30#include <utility>
31#include <variant>
32#include <vector>
33
34namespace xrpl {
35
36namespace {
37
38/*
39 * True iff the recorded sfVaultKind identifies a closed-ended vault.
40 * Centralizes the presence + enum-value check used by the phase-gate
41 * invariants below.
42 */
43[[nodiscard]] bool
44isClosedEnded(std::optional<std::uint8_t> const& vaultKind)
45{
46 return vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded);
47}
48
49} // namespace
50
53{
54 XRPL_ASSERT(from.getType() == ltVAULT, "ValidVault::Vault::make : from Vault object");
55
57 self.key = from.key();
58 self.asset = from.at(sfAsset);
59 self.pseudoId = from.getAccountID(sfAccount);
60 self.owner = from.at(sfOwner);
61 self.shareMPTID = from.getFieldH192(sfShareMPTID);
62 self.assetsTotal = from.at(sfAssetsTotal);
63 self.assetsAvailable = from.at(sfAssetsAvailable);
64 self.assetsMaximum = from.at(sfAssetsMaximum);
65 self.lossUnrealized = from.at(sfLossUnrealized);
66 self.vaultKind = from[~sfVaultKind];
67 self.subscriptionDate = from[~sfSubscriptionDate];
68 self.redemptionDate = from[~sfRedemptionDate];
69 return self;
70}
71
74{
75 XRPL_ASSERT(
76 from.getType() == ltMPTOKEN_ISSUANCE,
77 "ValidVault::Shares::make : from MPTokenIssuance object");
78
80 self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer)));
81 self.sharesTotal = from.at(sfOutstandingAmount);
82 self.sharesMaximum = from[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
83 return self;
84}
85
86void
88{
89 // If `before` is empty, this means an object is being created, in which
90 // case `isDelete` must be false. Otherwise `before` and `after` are set and
91 // `isDelete` indicates whether an object is being deleted or modified.
92 XRPL_ASSERT(
93 after != nullptr && (before != nullptr || !isDelete),
94 "xrpl::ValidVault::visitEntry : some object is available");
95
96 // Number balanceDelta will capture the difference (delta) between "before"
97 // state (zero if created) and "after" state (zero if destroyed), and
98 // preserves value scale (exponent) to round values to the same scale during
99 // validation. It is used to validate that the change in account
100 // balances matches the change in vault balances, stored to deltas_ at the
101 // end of this function.
102 DeltaInfo balanceDelta{.delta = kNumZero, .scale = std::nullopt};
103
104 std::int8_t sign = 0;
105 if (before)
106 {
107 switch (before->getType())
108 {
109 case ltVAULT:
110 beforeVault_.push_back(Vault::make(*before));
111 break;
112 case ltMPTOKEN_ISSUANCE:
113 // At this moment we have no way of telling if this object holds
114 // vault shares or something else. Save it for finalize.
115 beforeMPTs_.push_back(Shares::make(*before));
116 balanceDelta.delta =
117 static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
118 // MPTs are ints, so the scale is always 0.
119 balanceDelta.scale = 0;
120 sign = 1;
121 break;
122 case ltMPTOKEN:
123 balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
124 // MPTs are ints, so the scale is always 0.
125 balanceDelta.scale = 0;
126 sign = -1;
127 break;
128 case ltACCOUNT_ROOT:
129 balanceDelta.delta = before->getFieldAmount(sfBalance);
130 // Account balance is XRP, which is an int, so the scale is
131 // always 0.
132 balanceDelta.scale = 0;
133 sign = -1;
134 break;
135 case ltRIPPLE_STATE: {
136 auto const amount = before->getFieldAmount(sfBalance);
137 balanceDelta.delta = amount;
138 // Trust Line balances are STAmounts, so we can use the exponent
139 // directly to get the scale.
140 balanceDelta.scale = amount.exponent();
141 sign = -1;
142 break;
143 }
144 default:;
145 }
146 }
147
148 if (!isDelete && after)
149 {
150 switch (after->getType())
151 {
152 case ltVAULT:
153 afterVault_.push_back(Vault::make(*after));
154 break;
155 case ltMPTOKEN_ISSUANCE:
156 // At this moment we have no way of telling if this object holds
157 // vault shares or something else. Save it for finalize.
158 afterMPTs_.push_back(Shares::make(*after));
159 balanceDelta.delta -=
160 Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
161 // MPTs are ints, so the scale is always 0.
162 balanceDelta.scale = 0;
163 sign = 1;
164 break;
165 case ltMPTOKEN:
166 balanceDelta.delta -=
167 Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
168 // MPTs are ints, so the scale is always 0.
169 balanceDelta.scale = 0;
170 sign = -1;
171 break;
172 case ltACCOUNT_ROOT:
173 balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
174 // Account balance is XRP, which is an int, so the scale is
175 // always 0.
176 balanceDelta.scale = 0;
177 sign = -1;
178 break;
179 case ltRIPPLE_STATE: {
180 auto const amount = after->getFieldAmount(sfBalance);
181 balanceDelta.delta -= Number(amount);
182 // Trust Line balances are STAmounts, so we can use the exponent
183 // directly to get the scale.
184 if (amount.exponent() > balanceDelta.scale)
185 balanceDelta.scale = amount.exponent();
186 sign = -1;
187 break;
188 }
189 default:;
190 }
191 }
192
193 UInt256 const key = (before ? before->key() : after->key());
194 // Append to deltas if sign is non-zero, i.e. an object of an interesting
195 // type has been updated. A transaction may update an object even when
196 // its balance has not changed, e.g. transaction fee equals the amount
197 // transferred to the account. We intentionally do not compare balanceDelta
198 // against zero, to avoid missing such updates.
199 if (sign != 0)
200 {
201 XRPL_ASSERT_PARTS(balanceDelta.scale, "xrpl::ValidVault::visitEntry", "scale initialized");
202 balanceDelta.delta *= sign;
203 deltas_[key] = balanceDelta;
204 }
205}
206
209{
210 auto const& vaultAsset = afterVault_[0].asset;
211 auto const lookup = [&](UInt256 const& key) -> std::optional<DeltaInfo> {
212 auto const it = deltas_.find(key);
213 if (it == deltas_.end())
214 return std::nullopt;
215 return it->second;
216 };
217
218 return std::visit(
219 [&]<typename TIss>(TIss const& issue) -> std::optional<DeltaInfo> {
220 if constexpr (std::is_same_v<TIss, Issue>)
221 {
222 if (isXRP(issue))
223 return lookup(keylet::account(id).key);
224 auto result = lookup(keylet::trustLine(id, issue).key);
225 // Trust-line balance is stored from the low-account's perspective;
226 // negate if id is the high account so the delta is in id's terms.
227 if (result && id > issue.getIssuer())
228 result->delta = -result->delta;
229 return result;
230 }
231 else if constexpr (std::is_same_v<TIss, MPTIssue>)
232 {
233 return lookup(keylet::mptoken(issue.getMptID(), id).key);
234 }
235 },
236 vaultAsset.value());
237}
238
241{
242 auto const feePayer = Transactor::getFeePayer(view, tx);
243 if (feePayer.type == FeePayerType::SponsorPreFunded)
244 return std::nullopt;
245 return feePayer.id;
246}
247
250 ReadView const& view,
251 AccountID const& id,
252 STTx const& tx,
253 XRPAmount fee,
254 bool fix340Enabled) const
255{
256 auto const& vaultAsset = afterVault_[0].asset;
257 auto ret = deltaAssets(id);
258 if (!ret.has_value() || !vaultAsset.native())
259 return ret;
260
261 if (!fix340Enabled)
262 {
263 // Legacy behaviour: only tx[sfAccount] was ever considered for a fee
264 // correction, and only when STTx::getFeePayerID identified it as the
265 // fee payer (which is never true for a sponsor, since
266 // self-sponsorship is disallowed). After that sender-only correction
267 // a zero delta is collapsed to absence; if the correction does not
268 // apply, a present-zero is returned as-is.
269 if (id != tx[sfAccount] || tx.getFeePayerID() != id)
270 return ret;
271
272 ret->delta += fee.drops();
273 if (ret->delta == kZero)
274 return std::nullopt;
275
276 return ret;
277 }
278
279 // Add the fee back only onto the AccountRoot that actually paid it: an
280 // ordinary sender, a delegate, or a co-signed fee sponsor -- but never a
281 // pre-funded sponsorship, whose fee is drawn from the ltSponsorship
282 // object rather than the sponsor's own XRP balance.
283 if (auto const payer = feePayerAccountRoot(view, tx); payer && *payer == id)
284 ret->delta += fee.drops();
285
286 // Normalize an economically zero delta to absence regardless of who (if
287 // anyone) paid the fee, so a touched-but-unchanged AccountRoot (e.g. the
288 // sender in a third-party withdrawal, touched only for sequence/ticket
289 // processing) is never misread as a second payout recipient.
290 if (ret->delta == kZero)
291 return std::nullopt;
292
293 return ret;
294}
295
298{
299 auto const& afterVault = afterVault_[0];
300 auto const it = [&]() {
301 if (id == afterVault.pseudoId)
302 return deltas_.find(keylet::mptokenIssuance(afterVault.shareMPTID).key);
303 return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
304 }();
305
306 return it != deltas_.end() ? std::optional<DeltaInfo>(it->second) : std::nullopt;
307}
308
309bool
311{
312 return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
313}
314
315bool
317{
318 if (afterVault_.empty())
319 {
320 // LCOV_EXCL_START
321 UNREACHABLE("xrpl::ValidVault::finalizeLoanSet : vault exists");
322 return false;
323 // LCOV_EXCL_STOP
324 }
325
326 auto const& afterVault = afterVault_[0];
327
328 // Loan origination against a closed-ended vault is only permitted while the vault is in the
329 // Investment phase - strictly past SubscriptionDate and before RedemptionDate. Open-ended
330 // vaults have NoPhase and are unaffected.
331 auto const phase = getVaultPhase(
332 view, afterVault.vaultKind, afterVault.subscriptionDate, afterVault.redemptionDate);
333 if (phase == VaultPhase::NoPhase)
334 return true;
335
336 if (phase != VaultPhase::Investment)
337 {
338 JLOG(j.fatal()) << //
339 "Invariant failed: loan origination only allowed in Investment phase";
340 return false;
341 }
342
343 return true;
344}
345
346namespace {
347
348// sfAssetsTotal, sfAssetsAvailable and sfLossUnrealized are STNumber fields
349// with kSmdNeedsAsset, so IOU writes go through associateAsset -> roundToAsset
350// -> STAmount quantization. Since assetsTotal is the largest number, it lands
351// on the coarsest decimal grid, and strict equality on the deltas can fire on
352// a single unit of quantization noise even when the underlying flow is
353// correct. Absorb one unit at the coarsest scale.
354//
355// XRP and MPT are integer-domain assets (Asset::integral() is true) with no
356// sub-ULP quantization; treating a whole drop / MPT unit as "noise" would
357// hide real accounting bugs. Keep the strict comparison there. Note that
358// gating on the sign of `scale` would be wrong: IOU amounts >= 1e15 have a
359// non-negative STAmount exponent but still quantize.
360[[nodiscard]] bool
361agreesWithinOneUnit(Number const& lhs, Number const& rhs, Asset const& asset, std::int32_t scale)
362{
363 if (asset.integral())
364 return lhs == rhs;
365 auto const diff = lhs - rhs;
366 Number const tolerance{1, scale};
367 return (diff < beast::kZero ? -diff : diff) <= tolerance;
368}
369
370// L, T and A are each independently quantized; the strict L <= T - A check
371// can fire on residual noise even when the true relationship holds. Tolerate
372// one unit at scale(assetsTotal) - the coarsest of the three grids. As with
373// the delta check above, the tolerance is meaningful only for IOU
374// (Asset::integral() is false); XRP and MPT keep the strict comparison.
375[[nodiscard]] bool
376lessOrEqualPlusOneUnit(Number const& lhs, Number const& rhs, Asset const& asset, std::int32_t scale)
377{
378 if (asset.integral())
379 return lhs <= rhs;
380 return lhs <= rhs + Number{1, scale};
381}
382
383} // namespace
384
385std::int32_t
386ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const
387{
388 // Returns the posterior `assetsTotal` scale.
389 //
390 // 1. Because STAmounts are normalized, `assetsTotal` (being >= `assetsAvailable`)
391 // safely represents the coarsest exponent needed for both fields.
392 //
393 // 2. The scale may decrease (withdraw/clawback) or increase (deposit). In both cases
394 // we ensure the vault is in a legitimate state in the post-transaction scale.
395 auto const& afterVault = afterVault_[0];
396 auto const& vaultAsset = afterVault.asset;
397 if (rules.enabled(fixCleanup3_2_0))
398 {
400 return scale(afterVault.assetsTotal, vaultAsset);
401 }
402
403 auto const& beforeVault = beforeVault_[0];
404 auto const totalDelta =
405 DeltaInfo::makeDelta(beforeVault.assetsTotal, afterVault.assetsTotal, vaultAsset);
406 auto const availableDelta =
407 DeltaInfo::makeDelta(beforeVault.assetsAvailable, afterVault.assetsAvailable, vaultAsset);
408 return computeCoarsestScale({vaultDelta, totalDelta, availableDelta});
409}
410
411bool
413 STTx const& tx,
414 TER const ret,
415 XRPAmount const fee,
416 ReadView const& view,
417 beast::Journal const& j)
418{
419 bool const enforce = view.rules().enabled(featureSingleAssetVault);
420 bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
421
422 if (!isTesSuccess(ret))
423 return true; // Do not perform checks
424
425 if (afterVault_.empty() && beforeVault_.empty())
426 {
428 {
429 JLOG(j.fatal()) << //
430 "Invariant failed: vault operation succeeded without modifying "
431 "a vault";
432 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault noop invariant");
433 return !enforce;
434 }
435
436 return true; // Not a vault operation
437 }
440 {
441 JLOG(j.fatal()) << //
442 "Invariant failed: vault updated by a wrong transaction type";
443 XRPL_ASSERT(
444 enforce,
445 "xrpl::ValidVault::finalize : illegal vault transaction "
446 "invariant");
447 return !enforce; // Also not a vault operation
448 }
449
450 if (beforeVault_.size() > 1 || afterVault_.size() > 1)
451 {
452 JLOG(j.fatal()) << //
453 "Invariant failed: vault operation updated more than single vault";
454 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : single vault invariant");
455 return !enforce; // That's all we can do here
456 }
457
458 auto const txnType = tx.getTxnType();
459
460 // We do special handling for ttVAULT_DELETE first, because it's the only
461 // vault-modifying transaction without an "after" state of the vault
462 if (afterVault_.empty())
463 {
464 if (txnType != ttVAULT_DELETE)
465 {
466 JLOG(j.fatal()) << //
467 "Invariant failed: vault deleted by a wrong transaction type";
468 XRPL_ASSERT(
469 enforce,
470 "xrpl::ValidVault::finalize : illegal vault deletion "
471 "invariant");
472 return !enforce; // That's all we can do here
473 }
474
475 // Note, if afterVault_ is empty then we know that beforeVault_ is not
476 // empty, as enforced at the top of this function
477 auto const& beforeVault = beforeVault_[0];
478
479 // At this moment we only know a vault is being deleted and there
480 // might be some MPTokenIssuance objects which are deleted in the
481 // same transaction. Find the one matching this vault.
482 auto const deletedShares = [&]() -> std::optional<Shares> {
483 for (auto const& e : beforeMPTs_)
484 {
485 if (e.share.getMptID() == beforeVault.shareMPTID)
486 return e;
487 }
488 return std::nullopt;
489 }();
490
491 if (!deletedShares)
492 {
493 JLOG(j.fatal()) << "Invariant failed: deleted vault must also "
494 "delete shares";
495 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares deletion invariant");
496 return !enforce; // That's all we can do here
497 }
498
499 bool result = true;
500 if (deletedShares->sharesTotal != 0)
501 {
502 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
503 "shares outstanding";
504 result = false;
505 }
506 if (beforeVault.assetsTotal != kZero)
507 {
508 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
509 "assets outstanding";
510 result = false;
511 }
512 if (beforeVault.assetsAvailable != kZero)
513 {
514 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
515 "assets available";
516 result = false;
517 }
518
519 return result;
520 }
521 if (txnType == ttVAULT_DELETE)
522 {
523 JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without "
524 "deleting a vault";
525 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault deletion invariant");
526 return !enforce; // That's all we can do here
527 }
528
529 // Note, `afterVault_.empty()` is handled above
530 auto const& afterVault = afterVault_[0];
531 XRPL_ASSERT(
532 beforeVault_.empty() || beforeVault_[0].key == afterVault.key,
533 "xrpl::ValidVault::finalize : single vault operation");
534
535 auto const updatedShares = [&]() -> std::optional<Shares> {
536 // At this moment we only know that a vault is being updated and there
537 // might be some MPTokenIssuance objects which are also updated in the
538 // same transaction. Find the one matching the shares to this vault.
539 // Note, we expect updatedMPTs collection to be extremely small. For
540 // such collections linear search is faster than lookup.
541 for (auto const& e : afterMPTs_)
542 {
543 if (e.share.getMptID() == afterVault.shareMPTID)
544 return e;
545 }
546
547 auto const sleShares = view.read(keylet::mptokenIssuance(afterVault.shareMPTID));
548
549 return sleShares ? std::optional<Shares>(Shares::make(*sleShares)) : std::nullopt;
550 }();
551
552 bool result = true;
553
554 // Universal transaction checks
555 // From LendingProtocolV1_1 onwards, vault immutability check is moved to InvariantCheck.cpp
556 if (!beforeVault_.empty() && !view.rules().enabled(featureLendingProtocolV1_1))
557 {
558 auto const& beforeVault = beforeVault_[0];
559 if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId ||
560 afterVault.shareMPTID != beforeVault.shareMPTID)
561 {
562 JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data";
563 result = false;
564 }
565 }
566
567 if (!updatedShares)
568 {
569 JLOG(j.fatal()) << "Invariant failed: updated vault must have shares";
570 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault has shares invariant");
571 return !enforce; // That's all we can do here
572 }
573
574 if (updatedShares->sharesTotal == 0)
575 {
576 if (afterVault.assetsTotal != kZero)
577 {
578 JLOG(j.fatal()) << "Invariant failed: updated zero sized "
579 "vault must have no assets outstanding";
580 result = false;
581 }
582 if (afterVault.assetsAvailable != kZero)
583 {
584 JLOG(j.fatal()) << "Invariant failed: updated zero sized "
585 "vault must have no assets available";
586 result = false;
587 }
588 }
589 else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
590 {
591 JLOG(j.fatal()) //
592 << "Invariant failed: updated shares must not exceed maximum "
593 << updatedShares->sharesMaximum;
594 result = false;
595 }
596
597 if (afterVault.assetsAvailable < kZero)
598 {
599 JLOG(j.fatal()) << "Invariant failed: assets available must not be negative";
600 result = false;
601 }
602
603 if (afterVault.assetsAvailable > afterVault.assetsTotal)
604 {
605 JLOG(j.fatal()) << "Invariant failed: assets available must "
606 "not be greater than assets outstanding";
607 result = false;
608 }
609 else
610 {
611 bool const gapExceeded = [&] {
612 if (!fix340Enabled)
613 {
614 return afterVault.lossUnrealized >
615 afterVault.assetsTotal - afterVault.assetsAvailable;
616 }
617
618 auto const s = scale(afterVault.assetsTotal, afterVault.asset);
619 return !lessOrEqualPlusOneUnit(
620 afterVault.lossUnrealized,
621 afterVault.assetsTotal - afterVault.assetsAvailable,
622 afterVault.asset,
623 s);
624 }();
625 if (gapExceeded)
626 {
627 JLOG(j.fatal()) //
628 << "Invariant failed: loss unrealized must not exceed "
629 "the difference between assets outstanding and available";
630 result = false;
631 }
632 }
633
634 if (fix340Enabled && afterVault.lossUnrealized < kZero)
635 {
636 JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative";
637 result = false;
638 }
639
640 if (afterVault.assetsTotal < kZero)
641 {
642 JLOG(j.fatal()) << "Invariant failed: assets outstanding must not be negative";
643 result = false;
644 }
645
646 if (afterVault.assetsMaximum < kZero)
647 {
648 JLOG(j.fatal()) << "Invariant failed: assets maximum must not be negative";
649 result = false;
650 }
651
652 // Thanks to this check we can simply do `assert(!beforeVault_.empty()` when
653 // enforcing invariants on transaction types other than ttVAULT_CREATE
654 if (beforeVault_.empty() && txnType != ttVAULT_CREATE)
655 {
656 JLOG(j.fatal()) << //
657 "Invariant failed: vault created by a wrong transaction type";
658 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault creation invariant");
659 return !enforce; // That's all we can do here
660 }
661
662 if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
663 txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
664 {
665 JLOG(j.fatal()) << //
666 "Invariant failed: vault transaction must not change loss "
667 "unrealized";
668 result = false;
669 }
670
671 // Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced by
672 // NoModifiedUnmodifiableFields in InvariantCheck.cpp.
673
674 auto const beforeShares = [&]() -> std::optional<Shares> {
675 if (beforeVault_.empty())
676 return std::nullopt;
677 auto const& beforeVault = beforeVault_[0];
678
679 for (auto const& e : beforeMPTs_)
680 {
681 if (e.share.getMptID() == beforeVault.shareMPTID)
682 return e;
683 }
684 return std::nullopt;
685 }();
686
687 if (!beforeShares &&
688 (tx.getTxnType() == ttVAULT_DEPOSIT || //
689 tx.getTxnType() == ttVAULT_WITHDRAW || //
690 tx.getTxnType() == ttVAULT_CLAWBACK))
691 {
692 JLOG(j.fatal()) << "Invariant failed: vault operation succeeded "
693 "without updating shares";
694 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares noop invariant");
695 return !enforce; // That's all we can do here
696 }
697
698 auto const& vaultAsset = afterVault.asset;
699
700 // Technically this does not need to be a lambda, but it's more
701 // convenient thanks to early "return false"; the not-so-nice
702 // alternatives are several layers of nested if/else or more complex
703 // (i.e. brittle) if statements.
704 result &= [&]() {
705 switch (txnType)
706 {
707 case ttVAULT_CREATE: {
708 bool result = true;
709
710 if (!beforeVault_.empty())
711 {
712 JLOG(j.fatal()) //
713 << "Invariant failed: create operation must not have "
714 "updated a vault";
715 result = false;
716 }
717
718 if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
719 afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
720 {
721 JLOG(j.fatal()) //
722 << "Invariant failed: created vault must be empty";
723 result = false;
724 }
725
726 if (afterVault.pseudoId != updatedShares->share.getIssuer())
727 {
728 JLOG(j.fatal()) //
729 << "Invariant failed: shares issuer and vault "
730 "pseudo-account must be the same";
731 result = false;
732 }
733
734 auto const sleSharesIssuer =
735 view.read(keylet::account(updatedShares->share.getIssuer()));
736 if (!sleSharesIssuer)
737 {
738 JLOG(j.fatal()) //
739 << "Invariant failed: shares issuer must exist";
740 return false;
741 }
742
743 if (!isPseudoAccount(sleSharesIssuer))
744 {
745 JLOG(j.fatal()) //
746 << "Invariant failed: shares issuer must be a "
747 "pseudo-account";
748 result = false;
749 }
750
751 if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID];
752 !vaultId || *vaultId != afterVault.key)
753 {
754 JLOG(j.fatal()) //
755 << "Invariant failed: shares issuer pseudo-account "
756 "must point back to the vault";
757 result = false;
758 }
759
760 if (isClosedEnded(afterVault.vaultKind))
761 {
762 if (!afterVault.subscriptionDate || !afterVault.redemptionDate)
763 {
764 JLOG(j.fatal()) //
765 << "Invariant failed: closed-ended vault must have SubscriptionDate "
766 "and RedemptionDate";
767 result = false;
768 }
769 else if (!isValidClosedEndedGap(
770 *afterVault.subscriptionDate, *afterVault.redemptionDate))
771 {
772 JLOG(j.fatal()) //
773 << "Invariant failed: closed-ended vault RedemptionDate - "
774 "SubscriptionDate must be within [MIN_INVESTMENT_PERIOD, "
775 "MAX_INVESTMENT_PERIOD)";
776 result = false;
777 }
778 }
779
780 return result;
781 }
782 case ttVAULT_SET: {
783 bool result = true;
784
785 XRPL_ASSERT(
786 !beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault");
787 auto const& beforeVault = beforeVault_[0];
788
789 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
790 if (vaultDeltaAssets)
791 {
792 JLOG(j.fatal()) << //
793 "Invariant failed: set must not change vault balance";
794 result = false;
795 }
796
797 if (beforeVault.assetsTotal != afterVault.assetsTotal)
798 {
799 JLOG(j.fatal()) << //
800 "Invariant failed: set must not change assets "
801 "outstanding";
802 result = false;
803 }
804
805 // AssetsTotal may exceed AssetsMaximum when the excess is interest. After
806 // fixCleanup3_4_0, only reject a VaultSet that supplies sfAssetsMaximum or
807 // otherwise changes the cap to a nonzero value still below AssetsTotal.
808 if (afterVault.assetsMaximum > kZero &&
809 afterVault.assetsTotal > afterVault.assetsMaximum &&
810 (!fix340Enabled || tx.isFieldPresent(sfAssetsMaximum) ||
811 beforeVault.assetsMaximum != afterVault.assetsMaximum))
812 {
813 JLOG(j.fatal()) << //
814 "Invariant failed: set assets outstanding must not "
815 "exceed assets maximum";
816 result = false;
817 }
818
819 if (beforeVault.assetsAvailable != afterVault.assetsAvailable)
820 {
821 JLOG(j.fatal()) << //
822 "Invariant failed: set must not change assets "
823 "available";
824 result = false;
825 }
826
827 if (beforeShares && updatedShares &&
828 beforeShares->sharesTotal != updatedShares->sharesTotal)
829 {
830 JLOG(j.fatal()) << //
831 "Invariant failed: set must not change shares "
832 "outstanding";
833 result = false;
834 }
835
836 return result;
837 }
838 case ttVAULT_DEPOSIT: {
839 bool result = true;
840
841 XRPL_ASSERT(
842 !beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault");
843 auto const& beforeVault = beforeVault_[0];
844
845 // Deposit is only allowed while the vault is in NoPhase or
846 // Subscription.
847 auto const depositPhase = getVaultPhase(
848 view,
849 afterVault.vaultKind,
850 afterVault.subscriptionDate,
851 afterVault.redemptionDate);
852 if (depositPhase != VaultPhase::NoPhase && depositPhase != VaultPhase::Subscription)
853 {
854 JLOG(j.fatal()) << //
855 "Invariant failed: deposit only allowed in "
856 "Subscription or NoPhase";
857 result = false;
858 }
859
860 auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
861 if (!maybeVaultDeltaAssets)
862 {
863 JLOG(j.fatal()) << //
864 "Invariant failed: deposit must change vault balance";
865 return false; // That's all we can do
866 }
867
868 // Get the posterior scale to round calculations to
869 auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
870
871 auto const vaultDeltaAssets =
872 roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
873 auto const txAmount = roundToAsset(vaultAsset, tx[sfAmount], minScale);
874
875 if (vaultDeltaAssets > txAmount)
876 {
877 JLOG(j.fatal()) << //
878 "Invariant failed: deposit must not change vault "
879 "balance by more than deposited amount";
880 result = false;
881 }
882
883 if (vaultDeltaAssets <= kZero)
884 {
885 JLOG(j.fatal()) << //
886 "Invariant failed: deposit must increase vault balance";
887 result = false;
888 }
889
890 // Any payments (including deposits) made by the issuer
891 // do not change their balance, but create funds instead.
892 bool const issuerDeposit = [&]() -> bool {
893 if (vaultAsset.native())
894 return false;
895 return tx[sfAccount] == vaultAsset.getIssuer();
896 }();
897
898 if (!issuerDeposit)
899 {
900 auto const maybeAccDeltaAssets =
901 deltaAssetsForParty(view, tx[sfAccount], tx, fee, fix340Enabled);
902 if (!maybeAccDeltaAssets)
903 {
904 JLOG(j.fatal())
905 << "Invariant failed: deposit must change depositor balance";
906 return false;
907 }
908 auto const localMinScale =
909 std::max(minScale, computeCoarsestScale({*maybeAccDeltaAssets}));
910
911 auto const accountDeltaAssets =
912 roundToAsset(vaultAsset, maybeAccDeltaAssets->delta, localMinScale);
913 auto const localVaultDeltaAssets =
914 roundToAsset(vaultAsset, vaultDeltaAssets, localMinScale);
915
916 // For IOUs, if the deposit amount is not-representable at depositor trustline
917 // scale deposit amount could round to zero, giving depositor shares for no
918 // assets. Unlike withdrawal, we do not allow that.
919 if (accountDeltaAssets >= kZero)
920 {
921 JLOG(j.fatal())
922 << "Invariant failed: deposit must decrease depositor balance";
923 result = false;
924 }
925
926 bool const acctVaultAddsUp = fix340Enabled
927 ? agreesWithinOneUnit(
928 localVaultDeltaAssets * -1,
929 accountDeltaAssets,
930 vaultAsset,
931 localMinScale)
932 : localVaultDeltaAssets * -1 == accountDeltaAssets;
933 if (!acctVaultAddsUp)
934 {
935 JLOG(j.fatal()) << "Invariant failed: " << //
936 "deposit must change vault and depositor balance by equal amount";
937 result = false;
938 }
939 }
940
941 if (afterVault.assetsMaximum > kZero &&
942 afterVault.assetsTotal > afterVault.assetsMaximum)
943 {
944 JLOG(j.fatal()) << "Invariant failed: " << //
945 "deposit assets outstanding must not exceed assets maximum";
946 result = false;
947 }
948
949 auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
950 if (!maybeAccDeltaShares)
951 {
952 JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares";
953 return false; // That's all we can do
954 }
955 // We don't round shares, they are integral MPT
956 auto const& accountDeltaShares = *maybeAccDeltaShares;
957 if (accountDeltaShares.delta <= kZero)
958 {
959 JLOG(j.fatal()) << "Invariant failed: deposit must increase depositor shares";
960 result = false;
961 }
962
963 auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
964 if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == kZero)
965 {
966 JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares";
967 return false; // That's all we can do
968 }
969
970 // We don't round shares, they are integral MPT
971 auto const& vaultDeltaShares = *maybeVaultDeltaShares;
972 if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
973 {
974 JLOG(j.fatal()) << "Invariant failed: " << //
975 "deposit must change depositor and vault shares by equal amount";
976 result = false;
977 }
978
979 auto const assetTotalDelta = roundToAsset(
980 vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
981 bool const totalAddsUp = fix340Enabled
982 ? agreesWithinOneUnit(assetTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
983 : assetTotalDelta == vaultDeltaAssets;
984 if (!totalAddsUp)
985 {
986 JLOG(j.fatal())
987 << "Invariant failed: deposit and assets outstanding must add up";
988 result = false;
989 }
990
991 auto const assetAvailableDelta = roundToAsset(
992 vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
993 bool const availableAddsUp = fix340Enabled
994 ? agreesWithinOneUnit(
995 assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
996 : assetAvailableDelta == vaultDeltaAssets;
997 if (!availableAddsUp)
998 {
999 JLOG(j.fatal()) << "Invariant failed: deposit and assets available must add up";
1000 result = false;
1001 }
1002
1003 return result;
1004 }
1005 case ttVAULT_WITHDRAW: {
1006 bool result = true;
1007
1008 XRPL_ASSERT(
1009 !beforeVault_.empty(),
1010 "xrpl::ValidVault::finalize : withdrawal updated a vault");
1011 auto const& beforeVault = beforeVault_[0];
1012
1013 // Withdrawal from a closed-ended vault is not allowed during the Investment phase
1014 // (strictly past SubscriptionDate, before RedemptionDate).
1015 if (getVaultPhase(
1016 view,
1017 afterVault.vaultKind,
1018 afterVault.subscriptionDate,
1019 afterVault.redemptionDate) == VaultPhase::Investment)
1020 {
1021 JLOG(j.fatal()) << //
1022 "Invariant failed: withdrawal not allowed during "
1023 "Investment phase";
1024 result = false;
1025 }
1026
1027 auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
1028
1029 // Post-fixCleanup3_4_0: a withdrawal that redeems shares from a
1030 // pool with no effective value left to back them (e.g. fully
1031 // impaired/insolvent) legitimately moves zero assets on both
1032 // sides — VaultWithdraw::doApply does not touch either
1033 // balance-holding entry for a zero-value transfer, so no delta
1034 // is recorded. VaultWithdraw::doApply separately rejects
1035 // (tecPRECISION_LOSS) the case where a *positive* per-share
1036 // value merely rounds down to zero, so a missing delta while
1037 // the pool still held positive effective value indicates a
1038 // real accounting bug, not this exception.
1039 bool const zeroDeltaIsLegitimate = fix340Enabled && !maybeVaultDeltaAssets &&
1040 beforeVault.assetsTotal == beforeVault.lossUnrealized;
1041
1042 if (!maybeVaultDeltaAssets && !zeroDeltaIsLegitimate)
1043 {
1044 JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault balance";
1045 return false; // That's all we can do
1046 }
1047
1048 DeltaInfo const vaultDeltaAssets = maybeVaultDeltaAssets.value_or(
1049 DeltaInfo{.delta = kNumZero, .scale = std::nullopt});
1050
1051 // Get the posterior scale to round calculations to
1052 auto const minScale = computeVaultMinScale(vaultDeltaAssets, view.rules());
1053
1054 auto const vaultPseudoDeltaAssets =
1055 roundToAsset(vaultAsset, vaultDeltaAssets.delta, minScale);
1056
1057 if (!zeroDeltaIsLegitimate && vaultPseudoDeltaAssets >= kZero)
1058 {
1059 JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease vault balance";
1060 result = false;
1061 }
1062
1063 // Any payments (including withdrawal) going to the issuer
1064 // do not change their balance, but destroy funds instead.
1065 bool const issuerWithdrawal = [&]() -> bool {
1066 if (vaultAsset.native())
1067 return false;
1068 auto const destination = tx[~sfDestination].value_or(tx[sfAccount]);
1069 return destination == vaultAsset.getIssuer();
1070 }();
1071
1072 if (!issuerWithdrawal)
1073 {
1074 // Identify the intended recipient explicitly from
1075 // sfDestination (falling back to sfAccount for a
1076 // self-withdrawal), rather than inferring it from which
1077 // side happens to show a delta. When a distinct
1078 // destination is named, the sending account must not
1079 // also show a real economic delta -- that would mean two
1080 // accounts were paid, which is always a bug, regardless
1081 // of what (if anything) the named destination received.
1082 auto const destinationField = tx[~sfDestination];
1083 AccountID const recipient = destinationField.value_or(tx[sfAccount]);
1084 bool const distinctDestination =
1085 destinationField.has_value() && *destinationField != tx[sfAccount];
1086
1087 // Intentionally ungated: `fix340Enabled &&` here would let the
1088 // pre-amendment sponsored case succeed and change consensus.
1089 if (distinctDestination &&
1090 deltaAssetsForParty(view, tx[sfAccount], tx, fee, fix340Enabled)
1091 .has_value())
1092 {
1093 JLOG(j.fatal()) << //
1094 "Invariant failed: withdrawal must change one destination balance";
1095 return false;
1096 }
1097
1098 auto const maybeRecipientDelta =
1099 deltaAssetsForParty(view, recipient, tx, fee, fix340Enabled);
1100
1101 if (!maybeRecipientDelta.has_value())
1102 {
1103 // A legitimate zero-value withdrawal moves nothing to
1104 // the recipient either; there is nothing left to
1105 // cross-check.
1106 if (!zeroDeltaIsLegitimate)
1107 {
1108 JLOG(j.fatal()) << //
1109 "Invariant failed: withdrawal must change one destination balance";
1110 return false;
1111 }
1112 }
1113 else
1114 {
1115 // A one-sided change is cross-checked even for a
1116 // legitimate zero vault delta: the destination must
1117 // then have moved by (rounded) zero as well.
1118 auto const destinationDelta = *maybeRecipientDelta;
1119
1120 // the scale of destinationDelta can be coarser than
1121 // minScale, so we take that into account when rounding
1122 auto const destinationScale = computeCoarsestScale({destinationDelta});
1123 auto const localMinScale = std::max(minScale, destinationScale);
1124
1125 auto const roundedDestinationDelta =
1126 roundToAsset(vaultAsset, destinationDelta.delta, localMinScale);
1127
1128 // Post-fixCleanup3_2_0: Tolerate zero-rounded destination deltas for IOUs
1129 // only. If the receiver's trust line sits at a coarser scale, the inflow
1130 // may safely round down to zero.
1131 //
1132 // XRP and MPT remain strict for rounding artifacts.
1133 bool const tolerateZeroDelta =
1134 view.rules().enabled(fixCleanup3_2_0) && !vaultAsset.integral();
1135 auto const invalidBalanceChange = tolerateZeroDelta
1136 ? roundedDestinationDelta < kZero
1137 : roundedDestinationDelta <= kZero;
1138 if (invalidBalanceChange)
1139 {
1140 JLOG(j.fatal()) << //
1141 "Invariant failed: withdrawal must increase destination balance";
1142 result = false;
1143 }
1144
1145 auto const localPseudoDeltaAssets =
1146 roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
1147 // For IOU assets near a precision boundary the destination's STAmount
1148 // exponent can shift, making part of the sent value unrepresentable at
1149 // the receiver's new scale — that portion is irreversibly absorbed by the
1150 // IOU rail. Tolerate the mismatch only when the destroyed amount (vault
1151 // outflow minus destination inflow, in Number space) is itself sub-ULP at
1152 // the destination's scale. Floor rounding is used so that values exactly
1153 // at the step boundary are not mistakenly dismissed. Any representable
1154 // discrepancy indicates a real accounting bug and must be caught.
1155 auto const destroyedIsSubUlp = tolerateZeroDelta &&
1157 vaultAsset,
1158 vaultDeltaAssets.delta * -1 - destinationDelta.delta,
1159 destinationScale,
1161 bool const withdrawAddsUp = fix340Enabled
1162 ? agreesWithinOneUnit(
1163 localPseudoDeltaAssets * -1,
1164 roundedDestinationDelta,
1165 vaultAsset,
1166 localMinScale)
1167 : localPseudoDeltaAssets * -1 == roundedDestinationDelta;
1168 if (!destroyedIsSubUlp && !withdrawAddsUp)
1169 {
1170 JLOG(j.fatal()) << "Invariant failed: " << //
1171 "withdrawal must change vault and destination balance by equal "
1172 "amount";
1173 result = false;
1174 }
1175 }
1176 }
1177
1178 // We don't round shares, they are integral MPT
1179 auto const accountDeltaShares = deltaShares(tx[sfAccount]);
1180 if (!accountDeltaShares)
1181 {
1182 JLOG(j.fatal()) << "Invariant failed: withdrawal must change depositor shares";
1183 return false;
1184 }
1185
1186 if (accountDeltaShares->delta >= kZero)
1187 {
1188 JLOG(j.fatal())
1189 << "Invariant failed: withdrawal must decrease depositor shares";
1190 result = false;
1191 }
1192
1193 // We don't round shares, they are integral MPT
1194 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
1195 if (!vaultDeltaShares || vaultDeltaShares->delta == kZero)
1196 {
1197 JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault shares";
1198 return false; // That's all we can do
1199 }
1200
1201 if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta)
1202 {
1203 JLOG(j.fatal()) << "Invariant failed: " << //
1204 "withdrawal must change depositor and vault shares by equal amount";
1205 result = false;
1206 }
1207
1208 auto const assetTotalDelta = roundToAsset(
1209 vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
1210 // Note, vaultBalance is negative (see check above)
1211 bool const totalAddsUp = fix340Enabled
1212 ? agreesWithinOneUnit(
1213 assetTotalDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
1214 : assetTotalDelta == vaultPseudoDeltaAssets;
1215 if (!totalAddsUp)
1216 {
1217 JLOG(j.fatal())
1218 << "Invariant failed: withdrawal and assets outstanding must add up";
1219 result = false;
1220 }
1221
1222 auto const assetAvailableDelta = roundToAsset(
1223 vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
1224
1225 bool const availableAddsUp = fix340Enabled
1226 ? agreesWithinOneUnit(
1227 assetAvailableDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
1228 : assetAvailableDelta == vaultPseudoDeltaAssets;
1229 if (!availableAddsUp)
1230 {
1231 JLOG(j.fatal())
1232 << "Invariant failed: withdrawal and assets available must add up";
1233 result = false;
1234 }
1235
1236 return result;
1237 }
1238 case ttVAULT_CLAWBACK: {
1239 bool result = true;
1240
1241 XRPL_ASSERT(
1242 !beforeVault_.empty(), "xrpl::ValidVault::finalize : clawback updated a vault");
1243 auto const& beforeVault = beforeVault_[0];
1244
1245 if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount])
1246 {
1247 // The owner can use clawback to force-burn shares when the
1248 // vault is empty but there are outstanding shares
1249 if (!(beforeShares && beforeShares->sharesTotal > 0 &&
1250 isVaultEmpty(beforeVault) && beforeVault.owner == tx[sfAccount]))
1251 {
1252 JLOG(j.fatal()) << "Invariant failed: " << //
1253 "clawback may only be performed by the asset issuer, or by the vault "
1254 "owner of an empty vault";
1255 return false; // That's all we can do
1256 }
1257 }
1258
1259 auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
1260 if (maybeVaultDeltaAssets)
1261 {
1262 auto const minScale =
1263 computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
1264 auto const vaultDeltaAssets =
1265 roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
1266 if (vaultDeltaAssets >= kZero)
1267 {
1268 JLOG(j.fatal()) << "Invariant failed: clawback must decrease vault balance";
1269 result = false;
1270 }
1271
1272 auto const assetsTotalDelta = roundToAsset(
1273 vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
1274 bool const totalAddsUp = fix340Enabled
1275 ? agreesWithinOneUnit(
1276 assetsTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
1277 : assetsTotalDelta == vaultDeltaAssets;
1278 if (!totalAddsUp)
1279 {
1280 JLOG(j.fatal()) << //
1281 "Invariant failed: clawback and assets outstanding must add up";
1282 result = false;
1283 }
1284
1285 auto const assetAvailableDelta = roundToAsset(
1286 vaultAsset,
1287 afterVault.assetsAvailable - beforeVault.assetsAvailable,
1288 minScale);
1289 bool const availableAddsUp = fix340Enabled
1290 ? agreesWithinOneUnit(
1291 assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
1292 : assetAvailableDelta == vaultDeltaAssets;
1293 if (!availableAddsUp)
1294 {
1295 JLOG(j.fatal()) << //
1296 "Invariant failed: clawback and assets available must add up";
1297 result = false;
1298 }
1299 }
1300 else if (!isVaultEmpty(beforeVault))
1301 {
1302 JLOG(j.fatal()) << //
1303 "Invariant failed: clawback must change vault balance";
1304 return false; // That's all we can do
1305 }
1306
1307 // We don't need to round shares, they are integral MPT
1308 auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]);
1309 if (!maybeAccountDeltaShares)
1310 {
1311 JLOG(j.fatal()) << //
1312 "Invariant failed: clawback must change holder shares";
1313 return false; // That's all we can do
1314 }
1315 if (maybeAccountDeltaShares->delta >= kZero)
1316 {
1317 JLOG(j.fatal()) << //
1318 "Invariant failed: clawback must decrease holder shares";
1319 result = false;
1320 }
1321
1322 // We don't need to round shares, they are integral MPT
1323 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
1324 if (!vaultDeltaShares || vaultDeltaShares->delta == kZero)
1325 {
1326 JLOG(j.fatal()) << //
1327 "Invariant failed: clawback must change vault shares";
1328 return false; // That's all we can do
1329 }
1330
1331 if (vaultDeltaShares->delta * -1 != maybeAccountDeltaShares->delta)
1332 {
1333 JLOG(j.fatal()) << "Invariant failed: " << //
1334 "clawback must change holder and vault shares by equal amount";
1335 result = false;
1336 }
1337
1338 return result;
1339 }
1340
1341 case ttLOAN_SET:
1342 return finalizeLoanSet(view, j);
1343 case ttLOAN_MANAGE:
1344 case ttLOAN_PAY:
1345 return true;
1346
1347 default:
1348 // LCOV_EXCL_START
1349 UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type");
1350 return false;
1351 // LCOV_EXCL_STOP
1352 }
1353 }();
1354
1355 if (!result)
1356 {
1357 // The comment at the top of this file starting with "assert(enforce)"
1358 // explains this assert.
1359 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault invariants");
1360 return !enforce;
1361 }
1362
1363 return true;
1364}
1365
1366[[nodiscard]] ValidVault::DeltaInfo
1367ValidVault::DeltaInfo::makeDelta(Number const& before, Number const& after, Asset const& asset)
1368{
1369 return {
1370 .delta = after - before,
1371 .scale = std::max(xrpl::scale(after, asset), xrpl::scale(before, asset))};
1372}
1373
1374[[nodiscard]] std::int32_t
1376{
1377 if (numbers.empty())
1378 return 0;
1379
1380 auto const max = std::ranges::max_element(
1381 numbers, [](auto const& a, auto const& b) -> bool { return a.scale < b.scale; });
1382 XRPL_ASSERT_PARTS(
1383 max->scale, "xrpl::ValidVault::computeCoarsestScale", "scale set for destinationDelta");
1384 return max->scale.value_or(STAmount::kMaxOffset);
1385}
1386
1387} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
bool integral() const
Definition Asset.h:133
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
A view into a ledger.
Definition ReadView.h:41
virtual Rules const & rules() const =0
Returns the tx processing rules.
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
Rules controlling protocol behavior.
Definition Rules.h:40
bool enabled(UInt256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:182
static constexpr int kMaxOffset
Definition STAmount.h:62
UInt256 const & key() const
Returns the 'key' (or 'index') of this item.
LedgerEntryType getType() const
std::shared_ptr< STLedgerEntry const > const & ConstRef
T::value_type at(TypedField< T > const &f) const
Get the value of a field.
Definition STObject.h:1078
std::uint32_t getFieldU32(SField const &field) const
Definition STObject.cpp:601
UInt192 getFieldH192(SField const &field) const
Definition STObject.cpp:625
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:464
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:643
TxType getTxnType() const
Definition STTx.h:250
AccountID getFeePayerID() const
Definition STTx.cpp:673
static FeePayer getFeePayer(ReadView const &view, STTx const &tx)
static std::optional< AccountID > feePayerAccountRoot(ReadView const &view, STTx const &tx)
Return the AccountRoot whose XRP balance actually absorbed a transaction's fee, if any.
std::unordered_map< UInt256, DeltaInfo > deltas_
std::vector< Shares > afterMPTs_
std::optional< DeltaInfo > deltaAssetsForParty(ReadView const &view, AccountID const &id, STTx const &tx, XRPAmount fee, bool fix340Enabled) const
Return the vault-asset delta for a party inspected as a withdrawal/deposit counterparty,...
static bool isVaultEmpty(Vault const &vault)
Check whether a vault holds no assets.
std::vector< Vault > afterVault_
std::optional< DeltaInfo > deltaShares(AccountID const &id) const
Return the vault-share balance-change delta for an account.
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
std::vector< Shares > beforeMPTs_
static std::int32_t computeCoarsestScale(std::vector< DeltaInfo > const &numbers)
bool finalizeLoanSet(ReadView const &view, beast::Journal const &j) const
Invariant check for ttLOAN_SET.
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
std::vector< Vault > beforeVault_
static constexpr Number kZero
std::optional< DeltaInfo > deltaAssets(AccountID const &id) const
Return the vault-asset balance-change delta for an account.
std::int32_t computeVaultMinScale(DeltaInfo const &vaultDelta, Rules const &rules) const
Compute the minimum STAmount scale for rounding invariant calculations.
constexpr value_type drops() const
Returns the number of drops.
Definition XRPAmount.h:170
T empty(T... args)
T is_same_v
T max_element(T... args)
T max(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:567
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:275
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
static constexpr Number kNumZero
Definition Number.h:663
bool isXRP(AccountID const &c)
Definition AccountID.h:84
bool isValidClosedEndedGap(std::uint32_t sub, std::uint32_t red)
Returns true iff the (SubscriptionDate, RedemptionDate) gap of a closed-ended vault satisfies kMinInv...
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:794
STLedgerEntry SLE
BaseUInt< 256 > UInt256
Definition base_uint.h:580
bool hasPrivilege(STTx const &tx, Privilege priv)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
VaultPhase getVaultPhase(ReadView const &view, SLE::ConstRef vault)
Returns the current lifecycle phase of a vault.
MPTID makeMptID(std::uint32_t const sequence, AccountID const &account)
Definition Indexes.cpp:206
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:735
bool isPseudoAccount(SLE::const_pointer sleAcct)
Returns true if and only if sleAcct is a pseudo-account of any kind (i.e.
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
TERSubset< CanCvtToTER > TER
Definition TER.h:654
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:297
static DeltaInfo makeDelta(Number const &before, Number const &after, Asset const &asset)
std::optional< int > scale
static Shares make(SLE const &)
std::optional< std::uint8_t > vaultKind
std::optional< std::uint32_t > subscriptionDate
std::optional< std::uint32_t > redemptionDate
static Vault make(SLE const &)
T value(T... args)
T visit(T... args)