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/instrumentation.h>
7#include <xrpl/ledger/ReadView.h>
8#include <xrpl/ledger/helpers/AccountRootHelpers.h>
9#include <xrpl/ledger/helpers/VaultHelpers.h>
10#include <xrpl/protocol/Feature.h>
11#include <xrpl/protocol/Indexes.h>
12#include <xrpl/protocol/Issue.h>
13#include <xrpl/protocol/LedgerFormats.h>
14#include <xrpl/protocol/Protocol.h>
15#include <xrpl/protocol/SField.h>
16#include <xrpl/protocol/STAmount.h>
17#include <xrpl/protocol/STLedgerEntry.h>
18#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
19#include <xrpl/protocol/STTx.h>
20#include <xrpl/protocol/TER.h>
21#include <xrpl/protocol/TxFormats.h>
22#include <xrpl/protocol/XRPAmount.h>
23#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
24
25#include <algorithm>
26#include <cstdint>
27#include <optional>
28#include <utility>
29#include <variant>
30#include <vector>
31
32namespace xrpl {
33
34namespace {
35
36/*
37 * True iff the recorded sfVaultKind identifies a closed-ended vault.
38 * Centralizes the presence + enum-value check used by the phase-gate
39 * invariants below.
40 */
41[[nodiscard]] bool
42isClosedEnded(std::optional<std::uint8_t> const& vaultKind)
43{
44 return vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded);
45}
46
47} // namespace
48
51{
52 XRPL_ASSERT(from.getType() == ltVAULT, "ValidVault::Vault::make : from Vault object");
53
55 self.key = from.key();
56 self.asset = from.at(sfAsset);
57 self.pseudoId = from.getAccountID(sfAccount);
58 self.owner = from.at(sfOwner);
59 self.shareMPTID = from.getFieldH192(sfShareMPTID);
60 self.assetsTotal = from.at(sfAssetsTotal);
61 self.assetsAvailable = from.at(sfAssetsAvailable);
62 self.assetsMaximum = from.at(sfAssetsMaximum);
63 self.lossUnrealized = from.at(sfLossUnrealized);
64 self.vaultKind = from[~sfVaultKind];
65 self.subscriptionDate = from[~sfSubscriptionDate];
66 self.redemptionDate = from[~sfRedemptionDate];
67 return self;
68}
69
72{
73 XRPL_ASSERT(
74 from.getType() == ltMPTOKEN_ISSUANCE,
75 "ValidVault::Shares::make : from MPTokenIssuance object");
76
78 self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer)));
79 self.sharesTotal = from.at(sfOutstandingAmount);
80 self.sharesMaximum = from[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
81 return self;
82}
83
84void
86{
87 // If `before` is empty, this means an object is being created, in which
88 // case `isDelete` must be false. Otherwise `before` and `after` are set and
89 // `isDelete` indicates whether an object is being deleted or modified.
90 XRPL_ASSERT(
91 after != nullptr && (before != nullptr || !isDelete),
92 "xrpl::ValidVault::visitEntry : some object is available");
93
94 // Number balanceDelta will capture the difference (delta) between "before"
95 // state (zero if created) and "after" state (zero if destroyed), and
96 // preserves value scale (exponent) to round values to the same scale during
97 // validation. It is used to validate that the change in account
98 // balances matches the change in vault balances, stored to deltas_ at the
99 // end of this function.
100 DeltaInfo balanceDelta{.delta = kNumZero, .scale = std::nullopt};
101
102 std::int8_t sign = 0;
103 if (before)
104 {
105 switch (before->getType())
106 {
107 case ltVAULT:
108 beforeVault_.push_back(Vault::make(*before));
109 break;
110 case ltMPTOKEN_ISSUANCE:
111 // At this moment we have no way of telling if this object holds
112 // vault shares or something else. Save it for finalize.
113 beforeMPTs_.push_back(Shares::make(*before));
114 balanceDelta.delta =
115 static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
116 // MPTs are ints, so the scale is always 0.
117 balanceDelta.scale = 0;
118 sign = 1;
119 break;
120 case ltMPTOKEN:
121 balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
122 // MPTs are ints, so the scale is always 0.
123 balanceDelta.scale = 0;
124 sign = -1;
125 break;
126 case ltACCOUNT_ROOT:
127 balanceDelta.delta = before->getFieldAmount(sfBalance);
128 // Account balance is XRP, which is an int, so the scale is
129 // always 0.
130 balanceDelta.scale = 0;
131 sign = -1;
132 break;
133 case ltRIPPLE_STATE: {
134 auto const amount = before->getFieldAmount(sfBalance);
135 balanceDelta.delta = amount;
136 // Trust Line balances are STAmounts, so we can use the exponent
137 // directly to get the scale.
138 balanceDelta.scale = amount.exponent();
139 sign = -1;
140 break;
141 }
142 default:;
143 }
144 }
145
146 if (!isDelete && after)
147 {
148 switch (after->getType())
149 {
150 case ltVAULT:
151 afterVault_.push_back(Vault::make(*after));
152 break;
153 case ltMPTOKEN_ISSUANCE:
154 // At this moment we have no way of telling if this object holds
155 // vault shares or something else. Save it for finalize.
156 afterMPTs_.push_back(Shares::make(*after));
157 balanceDelta.delta -=
158 Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
159 // MPTs are ints, so the scale is always 0.
160 balanceDelta.scale = 0;
161 sign = 1;
162 break;
163 case ltMPTOKEN:
164 balanceDelta.delta -=
165 Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
166 // MPTs are ints, so the scale is always 0.
167 balanceDelta.scale = 0;
168 sign = -1;
169 break;
170 case ltACCOUNT_ROOT:
171 balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
172 // Account balance is XRP, which is an int, so the scale is
173 // always 0.
174 balanceDelta.scale = 0;
175 sign = -1;
176 break;
177 case ltRIPPLE_STATE: {
178 auto const amount = after->getFieldAmount(sfBalance);
179 balanceDelta.delta -= Number(amount);
180 // Trust Line balances are STAmounts, so we can use the exponent
181 // directly to get the scale.
182 if (amount.exponent() > balanceDelta.scale)
183 balanceDelta.scale = amount.exponent();
184 sign = -1;
185 break;
186 }
187 default:;
188 }
189 }
190
191 uint256 const key = (before ? before->key() : after->key());
192 // Append to deltas if sign is non-zero, i.e. an object of an interesting
193 // type has been updated. A transaction may update an object even when
194 // its balance has not changed, e.g. transaction fee equals the amount
195 // transferred to the account. We intentionally do not compare balanceDelta
196 // against zero, to avoid missing such updates.
197 if (sign != 0)
198 {
199 XRPL_ASSERT_PARTS(balanceDelta.scale, "xrpl::ValidVault::visitEntry", "scale initialized");
200 balanceDelta.delta *= sign;
201 deltas_[key] = balanceDelta;
202 }
203}
204
207{
208 auto const& vaultAsset = afterVault_[0].asset;
209 auto const lookup = [&](uint256 const& key) -> std::optional<DeltaInfo> {
210 auto const it = deltas_.find(key);
211 if (it == deltas_.end())
212 return std::nullopt;
213 return it->second;
214 };
215
216 return std::visit(
217 [&]<typename TIss>(TIss const& issue) -> std::optional<DeltaInfo> {
218 if constexpr (std::is_same_v<TIss, Issue>)
219 {
220 if (isXRP(issue))
221 return lookup(keylet::account(id).key);
222 auto result = lookup(keylet::trustLine(id, issue).key);
223 // Trust-line balance is stored from the low-account's perspective;
224 // negate if id is the high account so the delta is in id's terms.
225 if (result && id > issue.getIssuer())
226 result->delta = -result->delta;
227 return result;
228 }
229 else if constexpr (std::is_same_v<TIss, MPTIssue>)
230 {
231 return lookup(keylet::mptoken(issue.getMptID(), id).key);
232 }
233 },
234 vaultAsset.value());
235}
236
239{
240 auto const& vaultAsset = afterVault_[0].asset;
241 auto ret = deltaAssets(tx[sfAccount]);
242 if (!ret.has_value() || !vaultAsset.native())
243 return ret;
244
245 // Only add the fee back if tx[sfAccount] actually paid it. When the fee is
246 // paid by someone else (a delegate or a fee sponsor), the
247 // account's XRP balance moved only by the vault amount.
248 if (tx.getFeePayerID() != tx[sfAccount])
249 return ret;
250
251 ret->delta += fee.drops();
252 if (ret->delta == kZero)
253 return std::nullopt;
254
255 return ret;
256}
257
260{
261 auto const& afterVault = afterVault_[0];
262 auto const it = [&]() {
263 if (id == afterVault.pseudoId)
264 return deltas_.find(keylet::mptokenIssuance(afterVault.shareMPTID).key);
265 return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
266 }();
267
268 return it != deltas_.end() ? std::optional<DeltaInfo>(it->second) : std::nullopt;
269}
270
271bool
273{
274 return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
275}
276
277bool
279{
280 if (afterVault_.empty())
281 {
282 // LCOV_EXCL_START
283 UNREACHABLE("xrpl::ValidVault::finalizeLoanSet : vault exists");
284 return false;
285 // LCOV_EXCL_STOP
286 }
287
288 auto const& afterVault = afterVault_[0];
289
290 // Loan origination against a closed-ended vault is only permitted while the vault is in the
291 // Investment phase - strictly past SubscriptionDate and before RedemptionDate. Open-ended
292 // vaults have NoPhase and are unaffected.
293 auto const phase = getVaultPhase(
294 view, afterVault.vaultKind, afterVault.subscriptionDate, afterVault.redemptionDate);
295 if (phase == VaultPhase::NoPhase)
296 return true;
297
298 if (phase != VaultPhase::Investment)
299 {
300 JLOG(j.fatal()) << //
301 "Invariant failed: loan origination only allowed in Investment phase";
302 return false;
303 }
304
305 return true;
306}
307
309ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const
310{
311 // Returns the posterior `assetsTotal` scale.
312 //
313 // 1. Because STAmounts are normalized, `assetsTotal` (being >= `assetsAvailable`)
314 // safely represents the coarsest exponent needed for both fields.
315 //
316 // 2. The scale may decrease (withdraw/clawback) or increase (deposit). In both cases
317 // we ensure the vault is in a legitimate state in the post-transaction scale.
318 auto const& afterVault = afterVault_[0];
319 auto const& vaultAsset = afterVault.asset;
320 if (rules.enabled(fixCleanup3_2_0))
321 {
323 return scale(afterVault.assetsTotal, vaultAsset);
324 }
325
326 auto const& beforeVault = beforeVault_[0];
327 auto const totalDelta =
328 DeltaInfo::makeDelta(beforeVault.assetsTotal, afterVault.assetsTotal, vaultAsset);
329 auto const availableDelta =
330 DeltaInfo::makeDelta(beforeVault.assetsAvailable, afterVault.assetsAvailable, vaultAsset);
331 return computeCoarsestScale({vaultDelta, totalDelta, availableDelta});
332}
333
334bool
336 STTx const& tx,
337 TER const ret,
338 XRPAmount const fee,
339 ReadView const& view,
340 beast::Journal const& j)
341{
342 bool const enforce = view.rules().enabled(featureSingleAssetVault);
343
344 if (!isTesSuccess(ret))
345 return true; // Do not perform checks
346
347 if (afterVault_.empty() && beforeVault_.empty())
348 {
350 {
351 JLOG(j.fatal()) << //
352 "Invariant failed: vault operation succeeded without modifying "
353 "a vault";
354 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault noop invariant");
355 return !enforce;
356 }
357
358 return true; // Not a vault operation
359 }
361 {
362 JLOG(j.fatal()) << //
363 "Invariant failed: vault updated by a wrong transaction type";
364 XRPL_ASSERT(
365 enforce,
366 "xrpl::ValidVault::finalize : illegal vault transaction "
367 "invariant");
368 return !enforce; // Also not a vault operation
369 }
370
371 if (beforeVault_.size() > 1 || afterVault_.size() > 1)
372 {
373 JLOG(j.fatal()) << //
374 "Invariant failed: vault operation updated more than single vault";
375 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : single vault invariant");
376 return !enforce; // That's all we can do here
377 }
378
379 auto const txnType = tx.getTxnType();
380
381 // We do special handling for ttVAULT_DELETE first, because it's the only
382 // vault-modifying transaction without an "after" state of the vault
383 if (afterVault_.empty())
384 {
385 if (txnType != ttVAULT_DELETE)
386 {
387 JLOG(j.fatal()) << //
388 "Invariant failed: vault deleted by a wrong transaction type";
389 XRPL_ASSERT(
390 enforce,
391 "xrpl::ValidVault::finalize : illegal vault deletion "
392 "invariant");
393 return !enforce; // That's all we can do here
394 }
395
396 // Note, if afterVault_ is empty then we know that beforeVault_ is not
397 // empty, as enforced at the top of this function
398 auto const& beforeVault = beforeVault_[0];
399
400 // At this moment we only know a vault is being deleted and there
401 // might be some MPTokenIssuance objects which are deleted in the
402 // same transaction. Find the one matching this vault.
403 auto const deletedShares = [&]() -> std::optional<Shares> {
404 for (auto const& e : beforeMPTs_)
405 {
406 if (e.share.getMptID() == beforeVault.shareMPTID)
407 return e;
408 }
409 return std::nullopt;
410 }();
411
412 if (!deletedShares)
413 {
414 JLOG(j.fatal()) << "Invariant failed: deleted vault must also "
415 "delete shares";
416 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares deletion invariant");
417 return !enforce; // That's all we can do here
418 }
419
420 bool result = true;
421 if (deletedShares->sharesTotal != 0)
422 {
423 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
424 "shares outstanding";
425 result = false;
426 }
427 if (beforeVault.assetsTotal != kZero)
428 {
429 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
430 "assets outstanding";
431 result = false;
432 }
433 if (beforeVault.assetsAvailable != kZero)
434 {
435 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
436 "assets available";
437 result = false;
438 }
439
440 return result;
441 }
442 if (txnType == ttVAULT_DELETE)
443 {
444 JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without "
445 "deleting a vault";
446 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault deletion invariant");
447 return !enforce; // That's all we can do here
448 }
449
450 // Note, `afterVault_.empty()` is handled above
451 auto const& afterVault = afterVault_[0];
452 XRPL_ASSERT(
453 beforeVault_.empty() || beforeVault_[0].key == afterVault.key,
454 "xrpl::ValidVault::finalize : single vault operation");
455
456 auto const updatedShares = [&]() -> std::optional<Shares> {
457 // At this moment we only know that a vault is being updated and there
458 // might be some MPTokenIssuance objects which are also updated in the
459 // same transaction. Find the one matching the shares to this vault.
460 // Note, we expect updatedMPTs collection to be extremely small. For
461 // such collections linear search is faster than lookup.
462 for (auto const& e : afterMPTs_)
463 {
464 if (e.share.getMptID() == afterVault.shareMPTID)
465 return e;
466 }
467
468 auto const sleShares = view.read(keylet::mptokenIssuance(afterVault.shareMPTID));
469
470 return sleShares ? std::optional<Shares>(Shares::make(*sleShares)) : std::nullopt;
471 }();
472
473 bool result = true;
474
475 // Universal transaction checks
476 if (!beforeVault_.empty())
477 {
478 auto const& beforeVault = beforeVault_[0];
479 if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId ||
480 afterVault.shareMPTID != beforeVault.shareMPTID)
481 {
482 JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data";
483 result = false;
484 }
485 }
486
487 if (!updatedShares)
488 {
489 JLOG(j.fatal()) << "Invariant failed: updated vault must have shares";
490 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault has shares invariant");
491 return !enforce; // That's all we can do here
492 }
493
494 if (updatedShares->sharesTotal == 0)
495 {
496 if (afterVault.assetsTotal != kZero)
497 {
498 JLOG(j.fatal()) << "Invariant failed: updated zero sized "
499 "vault must have no assets outstanding";
500 result = false;
501 }
502 if (afterVault.assetsAvailable != kZero)
503 {
504 JLOG(j.fatal()) << "Invariant failed: updated zero sized "
505 "vault must have no assets available";
506 result = false;
507 }
508 }
509 else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
510 {
511 JLOG(j.fatal()) //
512 << "Invariant failed: updated shares must not exceed maximum "
513 << updatedShares->sharesMaximum;
514 result = false;
515 }
516
517 if (afterVault.assetsAvailable < kZero)
518 {
519 JLOG(j.fatal()) << "Invariant failed: assets available must not be negative";
520 result = false;
521 }
522
523 if (afterVault.assetsAvailable > afterVault.assetsTotal)
524 {
525 JLOG(j.fatal()) << "Invariant failed: assets available must "
526 "not be greater than assets outstanding";
527 result = false;
528 }
529 else if (afterVault.lossUnrealized > afterVault.assetsTotal - afterVault.assetsAvailable)
530 {
531 JLOG(j.fatal()) //
532 << "Invariant failed: loss unrealized must not exceed "
533 "the difference between assets outstanding and available";
534 result = false;
535 }
536
537 if (view.rules().enabled(fixCleanup3_4_0) && afterVault.lossUnrealized < kZero)
538 {
539 JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative";
540 result = false;
541 }
542
543 if (afterVault.assetsTotal < kZero)
544 {
545 JLOG(j.fatal()) << "Invariant failed: assets outstanding must not be negative";
546 result = false;
547 }
548
549 if (afterVault.assetsMaximum < kZero)
550 {
551 JLOG(j.fatal()) << "Invariant failed: assets maximum must not be negative";
552 result = false;
553 }
554
555 // Thanks to this check we can simply do `assert(!beforeVault_.empty()` when
556 // enforcing invariants on transaction types other than ttVAULT_CREATE
557 if (beforeVault_.empty() && txnType != ttVAULT_CREATE)
558 {
559 JLOG(j.fatal()) << //
560 "Invariant failed: vault created by a wrong transaction type";
561 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault creation invariant");
562 return !enforce; // That's all we can do here
563 }
564
565 if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
566 txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
567 {
568 JLOG(j.fatal()) << //
569 "Invariant failed: vault transaction must not change loss "
570 "unrealized";
571 result = false;
572 }
573
574 // Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced by
575 // NoModifiedUnmodifiableFields in InvariantCheck.cpp.
576
577 auto const beforeShares = [&]() -> std::optional<Shares> {
578 if (beforeVault_.empty())
579 return std::nullopt;
580 auto const& beforeVault = beforeVault_[0];
581
582 for (auto const& e : beforeMPTs_)
583 {
584 if (e.share.getMptID() == beforeVault.shareMPTID)
585 return e;
586 }
587 return std::nullopt;
588 }();
589
590 if (!beforeShares &&
591 (tx.getTxnType() == ttVAULT_DEPOSIT || //
592 tx.getTxnType() == ttVAULT_WITHDRAW || //
593 tx.getTxnType() == ttVAULT_CLAWBACK))
594 {
595 JLOG(j.fatal()) << "Invariant failed: vault operation succeeded "
596 "without updating shares";
597 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares noop invariant");
598 return !enforce; // That's all we can do here
599 }
600
601 auto const& vaultAsset = afterVault.asset;
602
603 // Technically this does not need to be a lambda, but it's more
604 // convenient thanks to early "return false"; the not-so-nice
605 // alternatives are several layers of nested if/else or more complex
606 // (i.e. brittle) if statements.
607 result &= [&]() {
608 switch (txnType)
609 {
610 case ttVAULT_CREATE: {
611 bool result = true;
612
613 if (!beforeVault_.empty())
614 {
615 JLOG(j.fatal()) //
616 << "Invariant failed: create operation must not have "
617 "updated a vault";
618 result = false;
619 }
620
621 if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
622 afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
623 {
624 JLOG(j.fatal()) //
625 << "Invariant failed: created vault must be empty";
626 result = false;
627 }
628
629 if (afterVault.pseudoId != updatedShares->share.getIssuer())
630 {
631 JLOG(j.fatal()) //
632 << "Invariant failed: shares issuer and vault "
633 "pseudo-account must be the same";
634 result = false;
635 }
636
637 auto const sleSharesIssuer =
638 view.read(keylet::account(updatedShares->share.getIssuer()));
639 if (!sleSharesIssuer)
640 {
641 JLOG(j.fatal()) //
642 << "Invariant failed: shares issuer must exist";
643 return false;
644 }
645
646 if (!isPseudoAccount(sleSharesIssuer))
647 {
648 JLOG(j.fatal()) //
649 << "Invariant failed: shares issuer must be a "
650 "pseudo-account";
651 result = false;
652 }
653
654 if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID];
655 !vaultId || *vaultId != afterVault.key)
656 {
657 JLOG(j.fatal()) //
658 << "Invariant failed: shares issuer pseudo-account "
659 "must point back to the vault";
660 result = false;
661 }
662
663 if (isClosedEnded(afterVault.vaultKind))
664 {
665 if (!afterVault.subscriptionDate || !afterVault.redemptionDate)
666 {
667 JLOG(j.fatal()) //
668 << "Invariant failed: closed-ended vault must have SubscriptionDate "
669 "and RedemptionDate";
670 result = false;
671 }
672 else if (!isValidClosedEndedGap(
673 *afterVault.subscriptionDate, *afterVault.redemptionDate))
674 {
675 JLOG(j.fatal()) //
676 << "Invariant failed: closed-ended vault RedemptionDate - "
677 "SubscriptionDate must be within [MIN_INVESTMENT_PERIOD, "
678 "MAX_INVESTMENT_PERIOD)";
679 result = false;
680 }
681 }
682
683 return result;
684 }
685 case ttVAULT_SET: {
686 bool result = true;
687
688 XRPL_ASSERT(
689 !beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault");
690 auto const& beforeVault = beforeVault_[0];
691
692 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
693 if (vaultDeltaAssets)
694 {
695 JLOG(j.fatal()) << //
696 "Invariant failed: set must not change vault balance";
697 result = false;
698 }
699
700 if (beforeVault.assetsTotal != afterVault.assetsTotal)
701 {
702 JLOG(j.fatal()) << //
703 "Invariant failed: set must not change assets "
704 "outstanding";
705 result = false;
706 }
707
708 if (afterVault.assetsMaximum > kZero &&
709 afterVault.assetsTotal > afterVault.assetsMaximum)
710 {
711 JLOG(j.fatal()) << //
712 "Invariant failed: set assets outstanding must not "
713 "exceed assets maximum";
714 result = false;
715 }
716
717 if (beforeVault.assetsAvailable != afterVault.assetsAvailable)
718 {
719 JLOG(j.fatal()) << //
720 "Invariant failed: set must not change assets "
721 "available";
722 result = false;
723 }
724
725 if (beforeShares && updatedShares &&
726 beforeShares->sharesTotal != updatedShares->sharesTotal)
727 {
728 JLOG(j.fatal()) << //
729 "Invariant failed: set must not change shares "
730 "outstanding";
731 result = false;
732 }
733
734 return result;
735 }
736 case ttVAULT_DEPOSIT: {
737 bool result = true;
738
739 XRPL_ASSERT(
740 !beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault");
741 auto const& beforeVault = beforeVault_[0];
742
743 // Deposit is only allowed while the vault is in NoPhase or
744 // Subscription.
745 auto const depositPhase = getVaultPhase(
746 view,
747 afterVault.vaultKind,
748 afterVault.subscriptionDate,
749 afterVault.redemptionDate);
750 if (depositPhase != VaultPhase::NoPhase && depositPhase != VaultPhase::Subscription)
751 {
752 JLOG(j.fatal()) << //
753 "Invariant failed: deposit only allowed in "
754 "Subscription or NoPhase";
755 result = false;
756 }
757
758 auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
759 if (!maybeVaultDeltaAssets)
760 {
761 JLOG(j.fatal()) << //
762 "Invariant failed: deposit must change vault balance";
763 return false; // That's all we can do
764 }
765
766 // Get the posterior scale to round calculations to
767 auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
768
769 auto const vaultDeltaAssets =
770 roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
771 auto const txAmount = roundToAsset(vaultAsset, tx[sfAmount], minScale);
772
773 if (vaultDeltaAssets > txAmount)
774 {
775 JLOG(j.fatal()) << //
776 "Invariant failed: deposit must not change vault "
777 "balance by more than deposited amount";
778 result = false;
779 }
780
781 if (vaultDeltaAssets <= kZero)
782 {
783 JLOG(j.fatal()) << //
784 "Invariant failed: deposit must increase vault balance";
785 result = false;
786 }
787
788 // Any payments (including deposits) made by the issuer
789 // do not change their balance, but create funds instead.
790 bool const issuerDeposit = [&]() -> bool {
791 if (vaultAsset.native())
792 return false;
793 return tx[sfAccount] == vaultAsset.getIssuer();
794 }();
795
796 if (!issuerDeposit)
797 {
798 auto const maybeAccDeltaAssets = deltaAssetsTxAccount(tx, fee);
799 if (!maybeAccDeltaAssets)
800 {
801 JLOG(j.fatal())
802 << "Invariant failed: deposit must change depositor balance";
803 return false;
804 }
805 auto const localMinScale =
806 std::max(minScale, computeCoarsestScale({*maybeAccDeltaAssets}));
807
808 auto const accountDeltaAssets =
809 roundToAsset(vaultAsset, maybeAccDeltaAssets->delta, localMinScale);
810 auto const localVaultDeltaAssets =
811 roundToAsset(vaultAsset, vaultDeltaAssets, localMinScale);
812
813 // For IOUs, if the deposit amount is not-representable at depositor trustline
814 // scale deposit amount could round to zero, giving depositor shares for no
815 // assets. Unlike withdrawal, we do not allow that.
816 if (accountDeltaAssets >= kZero)
817 {
818 JLOG(j.fatal())
819 << "Invariant failed: deposit must decrease depositor balance";
820 result = false;
821 }
822
823 if (localVaultDeltaAssets * -1 != accountDeltaAssets)
824 {
825 JLOG(j.fatal()) << "Invariant failed: " << //
826 "deposit must change vault and depositor balance by equal amount";
827 result = false;
828 }
829 }
830
831 if (afterVault.assetsMaximum > kZero &&
832 afterVault.assetsTotal > afterVault.assetsMaximum)
833 {
834 JLOG(j.fatal()) << "Invariant failed: " << //
835 "deposit assets outstanding must not exceed assets maximum";
836 result = false;
837 }
838
839 auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
840 if (!maybeAccDeltaShares)
841 {
842 JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares";
843 return false; // That's all we can do
844 }
845 // We don't round shares, they are integral MPT
846 auto const& accountDeltaShares = *maybeAccDeltaShares;
847 if (accountDeltaShares.delta <= kZero)
848 {
849 JLOG(j.fatal()) << "Invariant failed: deposit must increase depositor shares";
850 result = false;
851 }
852
853 auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
854 if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == kZero)
855 {
856 JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares";
857 return false; // That's all we can do
858 }
859
860 // We don't round shares, they are integral MPT
861 auto const& vaultDeltaShares = *maybeVaultDeltaShares;
862 if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
863 {
864 JLOG(j.fatal()) << "Invariant failed: " << //
865 "deposit must change depositor and vault shares by equal amount";
866 result = false;
867 }
868
869 auto const assetTotalDelta = roundToAsset(
870 vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
871 if (assetTotalDelta != vaultDeltaAssets)
872 {
873 JLOG(j.fatal())
874 << "Invariant failed: deposit and assets outstanding must add up";
875 result = false;
876 }
877
878 auto const assetAvailableDelta = roundToAsset(
879 vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
880 if (assetAvailableDelta != vaultDeltaAssets)
881 {
882 JLOG(j.fatal()) << "Invariant failed: deposit and assets available must add up";
883 result = false;
884 }
885
886 return result;
887 }
888 case ttVAULT_WITHDRAW: {
889 bool result = true;
890
891 XRPL_ASSERT(
892 !beforeVault_.empty(),
893 "xrpl::ValidVault::finalize : withdrawal updated a vault");
894 auto const& beforeVault = beforeVault_[0];
895
896 // Withdrawal from a closed-ended vault is not allowed during the Investment phase
897 // (strictly past SubscriptionDate, before RedemptionDate).
898 if (getVaultPhase(
899 view,
900 afterVault.vaultKind,
901 afterVault.subscriptionDate,
902 afterVault.redemptionDate) == VaultPhase::Investment)
903 {
904 JLOG(j.fatal()) << //
905 "Invariant failed: withdrawal not allowed during "
906 "Investment phase";
907 result = false;
908 }
909
910 auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
911 if (!maybeVaultDeltaAssets)
912 {
913 JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault balance";
914 return false; // That's all we can do
915 }
916
917 // Get the posterior scale to round calculations to
918 auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
919
920 auto const vaultPseudoDeltaAssets =
921 roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
922
923 if (vaultPseudoDeltaAssets >= kZero)
924 {
925 JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease vault balance";
926 result = false;
927 }
928
929 // Any payments (including withdrawal) going to the issuer
930 // do not change their balance, but destroy funds instead.
931 bool const issuerWithdrawal = [&]() -> bool {
932 if (vaultAsset.native())
933 return false;
934 auto const destination = tx[~sfDestination].value_or(tx[sfAccount]);
935 return destination == vaultAsset.getIssuer();
936 }();
937
938 if (!issuerWithdrawal)
939 {
940 auto const maybeAccDelta = deltaAssetsTxAccount(tx, fee);
941 auto const maybeOtherAccDelta = [&]() -> std::optional<DeltaInfo> {
942 if (auto const destination = tx[~sfDestination];
943 destination && *destination != tx[sfAccount])
944 return deltaAssets(*destination);
945 return std::nullopt;
946 }();
947
948 if (maybeAccDelta.has_value() == maybeOtherAccDelta.has_value())
949 {
950 JLOG(j.fatal()) << //
951 "Invariant failed: withdrawal must change one destination balance";
952 return false;
953 }
954
955 auto const destinationDelta = //
956 maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta;
957
958 // the scale of destinationDelta can be coarser than
959 // minScale, so we take that into account when rounding
960 auto const destinationScale = computeCoarsestScale({destinationDelta});
961 auto const localMinScale = std::max(minScale, destinationScale);
962
963 auto const roundedDestinationDelta =
964 roundToAsset(vaultAsset, destinationDelta.delta, localMinScale);
965
966 // Post-fixCleanup3_2_0: Tolerate zero-rounded destination deltas for IOUs only.
967 // If the receiver's trust line sits at a coarser scale, the inflow may
968 // safely round down to zero.
969 //
970 // XRP and MPT remain strict. Because they are integer-exact, a zero
971 // destination delta indicates a true accounting bug, not a rounding artifact.
972 bool const tolerateZeroDelta =
973 view.rules().enabled(fixCleanup3_2_0) && !vaultAsset.integral();
974 auto const invalidBalanceChange = tolerateZeroDelta
975 ? roundedDestinationDelta < kZero
976 : roundedDestinationDelta <= kZero;
977 if (invalidBalanceChange)
978 {
979 JLOG(j.fatal()) << //
980 "Invariant failed: withdrawal must increase destination balance";
981 result = false;
982 }
983
984 auto const localPseudoDeltaAssets =
985 roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
986 // For IOU assets near a precision boundary the destination's STAmount
987 // exponent can shift, making part of the sent value unrepresentable at the
988 // receiver's new scale — that portion is irreversibly absorbed by the IOU
989 // rail. Tolerate the mismatch only when the destroyed amount (vault outflow
990 // minus destination inflow, in Number space) is itself sub-ULP at the
991 // destination's scale. Floor rounding is used so that values exactly at the
992 // step boundary are not mistakenly dismissed. Any representable discrepancy
993 // indicates a real accounting bug and must be caught.
994 auto const destroyedIsSubUlp = tolerateZeroDelta &&
996 vaultAsset,
997 maybeVaultDeltaAssets->delta * -1 - destinationDelta.delta,
998 destinationScale,
1000 if (!destroyedIsSubUlp &&
1001 localPseudoDeltaAssets * -1 != roundedDestinationDelta)
1002 {
1003 JLOG(j.fatal()) << "Invariant failed: " << //
1004 "withdrawal must change vault and destination balance by equal "
1005 "amount";
1006 result = false;
1007 }
1008 }
1009
1010 // We don't round shares, they are integral MPT
1011 auto const accountDeltaShares = deltaShares(tx[sfAccount]);
1012 if (!accountDeltaShares)
1013 {
1014 JLOG(j.fatal()) << "Invariant failed: withdrawal must change depositor shares";
1015 return false;
1016 }
1017
1018 if (accountDeltaShares->delta >= kZero)
1019 {
1020 JLOG(j.fatal())
1021 << "Invariant failed: withdrawal must decrease depositor shares";
1022 result = false;
1023 }
1024
1025 // We don't round shares, they are integral MPT
1026 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
1027 if (!vaultDeltaShares || vaultDeltaShares->delta == kZero)
1028 {
1029 JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault shares";
1030 return false; // That's all we can do
1031 }
1032
1033 if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta)
1034 {
1035 JLOG(j.fatal()) << "Invariant failed: " << //
1036 "withdrawal must change depositor and vault shares by equal amount";
1037 result = false;
1038 }
1039
1040 auto const assetTotalDelta = roundToAsset(
1041 vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
1042 // Note, vaultBalance is negative (see check above)
1043 if (assetTotalDelta != vaultPseudoDeltaAssets)
1044 {
1045 JLOG(j.fatal())
1046 << "Invariant failed: withdrawal and assets outstanding must add up";
1047 result = false;
1048 }
1049
1050 auto const assetAvailableDelta = roundToAsset(
1051 vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
1052
1053 if (assetAvailableDelta != vaultPseudoDeltaAssets)
1054 {
1055 JLOG(j.fatal())
1056 << "Invariant failed: withdrawal and assets available must add up";
1057 result = false;
1058 }
1059
1060 return result;
1061 }
1062 case ttVAULT_CLAWBACK: {
1063 bool result = true;
1064
1065 XRPL_ASSERT(
1066 !beforeVault_.empty(), "xrpl::ValidVault::finalize : clawback updated a vault");
1067 auto const& beforeVault = beforeVault_[0];
1068
1069 if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount])
1070 {
1071 // The owner can use clawback to force-burn shares when the
1072 // vault is empty but there are outstanding shares
1073 if (!(beforeShares && beforeShares->sharesTotal > 0 &&
1074 isVaultEmpty(beforeVault) && beforeVault.owner == tx[sfAccount]))
1075 {
1076 JLOG(j.fatal()) << "Invariant failed: " << //
1077 "clawback may only be performed by the asset issuer, or by the vault "
1078 "owner of an empty vault";
1079 return false; // That's all we can do
1080 }
1081 }
1082
1083 auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
1084 if (maybeVaultDeltaAssets)
1085 {
1086 auto const minScale =
1087 computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
1088 auto const vaultDeltaAssets =
1089 roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
1090 if (vaultDeltaAssets >= kZero)
1091 {
1092 JLOG(j.fatal()) << "Invariant failed: clawback must decrease vault balance";
1093 result = false;
1094 }
1095
1096 auto const assetsTotalDelta = roundToAsset(
1097 vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
1098 if (assetsTotalDelta != vaultDeltaAssets)
1099 {
1100 JLOG(j.fatal()) << //
1101 "Invariant failed: clawback and assets outstanding must add up";
1102 result = false;
1103 }
1104
1105 auto const assetAvailableDelta = roundToAsset(
1106 vaultAsset,
1107 afterVault.assetsAvailable - beforeVault.assetsAvailable,
1108 minScale);
1109 if (assetAvailableDelta != vaultDeltaAssets)
1110 {
1111 JLOG(j.fatal()) << //
1112 "Invariant failed: clawback and assets available must add up";
1113 result = false;
1114 }
1115 }
1116 else if (!isVaultEmpty(beforeVault))
1117 {
1118 JLOG(j.fatal()) << //
1119 "Invariant failed: clawback must change vault balance";
1120 return false; // That's all we can do
1121 }
1122
1123 // We don't need to round shares, they are integral MPT
1124 auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]);
1125 if (!maybeAccountDeltaShares)
1126 {
1127 JLOG(j.fatal()) << //
1128 "Invariant failed: clawback must change holder shares";
1129 return false; // That's all we can do
1130 }
1131 if (maybeAccountDeltaShares->delta >= kZero)
1132 {
1133 JLOG(j.fatal()) << //
1134 "Invariant failed: clawback must decrease holder shares";
1135 result = false;
1136 }
1137
1138 // We don't need to round shares, they are integral MPT
1139 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
1140 if (!vaultDeltaShares || vaultDeltaShares->delta == kZero)
1141 {
1142 JLOG(j.fatal()) << //
1143 "Invariant failed: clawback must change vault shares";
1144 return false; // That's all we can do
1145 }
1146
1147 if (vaultDeltaShares->delta * -1 != maybeAccountDeltaShares->delta)
1148 {
1149 JLOG(j.fatal()) << "Invariant failed: " << //
1150 "clawback must change holder and vault shares by equal amount";
1151 result = false;
1152 }
1153
1154 return result;
1155 }
1156
1157 case ttLOAN_SET:
1158 return finalizeLoanSet(view, j);
1159 case ttLOAN_MANAGE:
1160 case ttLOAN_PAY:
1161 return true;
1162
1163 default:
1164 // LCOV_EXCL_START
1165 UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type");
1166 return false;
1167 // LCOV_EXCL_STOP
1168 }
1169 }();
1170
1171 if (!result)
1172 {
1173 // The comment at the top of this file starting with "assert(enforce)"
1174 // explains this assert.
1175 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault invariants");
1176 return !enforce;
1177 }
1178
1179 return true;
1180}
1181
1182[[nodiscard]] ValidVault::DeltaInfo
1183ValidVault::DeltaInfo::makeDelta(Number const& before, Number const& after, Asset const& asset)
1184{
1185 return {
1186 .delta = after - before,
1187 .scale = std::max(xrpl::scale(after, asset), xrpl::scale(before, asset))};
1188}
1189
1190[[nodiscard]] std::int32_t
1192{
1193 if (numbers.empty())
1194 return 0;
1195
1196 auto const max = std::ranges::max_element(
1197 numbers, [](auto const& a, auto const& b) -> bool { return a.scale < b.scale; });
1198 XRPL_ASSERT_PARTS(
1199 max->scale, "xrpl::ValidVault::computeCoarsestScale", "scale set for destinationDelta");
1200 return max->scale.value_or(STAmount::kMaxOffset);
1201}
1202
1203} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
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:180
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 & const_ref
uint192 getFieldH192(SField const &field) const
Definition STObject.cpp:625
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
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:643
TxType getTxnType() const
Definition STTx.h:226
AccountID getFeePayerID() const
Definition STTx.cpp:670
std::unordered_map< uint256, DeltaInfo > deltas_
std::vector< Shares > afterMPTs_
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.
std::vector< Shares > beforeMPTs_
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
static std::int32_t computeCoarsestScale(std::vector< DeltaInfo > const &numbers)
std::optional< DeltaInfo > deltaAssetsTxAccount(STTx const &tx, XRPAmount fee) const
Return the vault-asset delta for the transaction's sending account, adjusted for the fee.
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)
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:543
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:537
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:253
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...
VaultPhase getVaultPhase(ReadView const &view, SLE::const_ref vault)
Returns the current lifecycle phase of a vault.
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:794
STLedgerEntry SLE
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:572
MPTID makeMptID(std::uint32_t const sequence, AccountID const &account)
Definition Indexes.cpp:184
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
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:676
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
TERSubset< CanCvtToTER > TER
Definition TER.h:647
bool isPseudoAccount(SLE::const_pointer sleAcct, std::set< SField const * > const &pseudoFieldFilter={})
Returns true if and only if sleAcct is a pseudo-account or specific pseudo-accounts in pseudoFieldFil...
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:296
BaseUInt< 256 > uint256
Definition base_uint.h:580
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)