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