xrpld
Loading...
Searching...
No Matches
LoanPay.cpp
1#include <xrpl/tx/transactors/lending/LoanPay.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Number.h>
5#include <xrpl/beast/utility/Zero.h>
6#include <xrpl/beast/utility/instrumentation.h>
7#include <xrpl/json/to_string.h>
8#include <xrpl/ledger/ReadView.h>
9#include <xrpl/ledger/View.h>
10#include <xrpl/ledger/helpers/LendingHelpers.h>
11#include <xrpl/ledger/helpers/TokenHelpers.h>
12#include <xrpl/protocol/Feature.h>
13#include <xrpl/protocol/Indexes.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/STTakesAsset.h>
20#include <xrpl/protocol/STTx.h>
21#include <xrpl/protocol/TER.h>
22#include <xrpl/protocol/TxFlags.h>
23#include <xrpl/protocol/Units.h>
24#include <xrpl/protocol/XRPAmount.h>
25#include <xrpl/tx/Transactor.h>
26#include <xrpl/tx/transactors/lending/LoanManage.h>
27
28#include <algorithm>
29#include <bit>
30#include <cstdint>
31#include <expected>
32#include <vector>
33
34namespace xrpl {
35
36bool
41
44{
45 return tfLoanPayMask;
46}
47
50{
51 if (ctx.tx[sfLoanID] == beast::kZero)
52 return temINVALID;
53
54 if (ctx.tx[sfAmount] <= beast::kZero)
55 return temBAD_AMOUNT;
56
57 // The loan payment flags are all mutually exclusive. If more than one is
58 // set, the tx is malformed.
59 static_assert(
60 (tfLoanLatePayment | tfLoanFullPayment | tfLoanOverpayment) ==
61 ~(tfLoanPayMask | tfUniversal));
62 auto const flagsSet = ctx.tx.getFlags() & ~(tfLoanPayMask | tfUniversal);
63 if (std::popcount(flagsSet) > 1)
64 {
65 JLOG(ctx.j.warn()) << "Only one LoanPay flag can be set per tx. " << flagsSet
66 << " is too many.";
67 return temINVALID_FLAG;
68 }
69
70 return tesSUCCESS;
71}
72
75{
76 using namespace lending;
77
78 auto const normalCost = Transactor::calculateBaseFee(view, tx);
79
80 if (tx.isFlag(tfLoanFullPayment) || tx.isFlag(tfLoanLatePayment))
81 {
82 // The loan will be making one set of calculations for one full or late
83 // payment
84 return normalCost;
85 }
86
87 // The fee is based on the potential number of payments, unless the loan is
88 // being fully paid off.
89 auto const amount = tx[sfAmount];
90 auto const loanID = tx[sfLoanID];
91
92 auto const loanSle = view.read(keylet::loan(loanID));
93 if (!loanSle)
94 {
95 // Let preclaim worry about the error for this
96 return normalCost;
97 }
98
99 if (loanSle->at(sfPaymentRemaining) <= kLoanPaymentsPerFeeIncrement)
100 {
101 // If there are fewer than kLoanPaymentsPerFeeIncrement payments left to
102 // pay, we can skip the computations.
103 return normalCost;
104 }
105
106 if (hasExpired(view, loanSle->at(sfNextPaymentDueDate)))
107 {
108 // If the payment is late, and the late payment flag is not set, it'll
109 // fail
110 return normalCost;
111 }
112
113 auto const brokerSle = view.read(keylet::loanBroker(loanSle->at(sfLoanBrokerID)));
114 if (!brokerSle)
115 {
116 // Let preclaim worry about the error for this
117 return normalCost;
118 }
119 auto const vaultSle = view.read(keylet::vault(brokerSle->at(sfVaultID)));
120 if (!vaultSle)
121 {
122 // Let preclaim worry about the error for this
123 return normalCost;
124 }
125
126 auto const asset = vaultSle->at(sfAsset);
127
128 if (asset != amount.asset())
129 {
130 // Let preclaim worry about the error for this
131 return normalCost;
132 }
133
134 auto const scale = loanSle->at(sfLoanScale);
135
136 auto const regularPayment = roundPeriodicPayment(asset, loanSle->at(sfPeriodicPayment), scale) +
137 loanSle->at(sfLoanServiceFee);
138
139 // If making an overpayment, count it as a full payment because it will do
140 // about the same amount of work, if not more.
141 NumberRoundModeGuard const mg(
142 tx.isFlag(tfLoanOverpayment) ? Number::RoundingMode::Upward
144
145 static_assert(kLoanMaximumPaymentsPerTransaction % kLoanPaymentsPerFeeIncrement == 0);
146 static constexpr std::int64_t kMaxFeeIncrements =
147 kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement;
148
149 if (view.rules().enabled(fixCleanup3_1_3) &&
150 amount >= regularPayment * kLoanMaximumPaymentsPerTransaction)
151 {
152 // The payment handler will never process more than
153 // loanMaximumPaymentsPerTransaction payments (including overpayments),
154 // and one fee increment is charged for every
155 // loanPaymentsPerFeeIncrement, so don't charge more than
156 // loanMaximumPaymentsPerTransaction / loanPaymentsPerFeeIncrement fee
157 // increments.
158 return kMaxFeeIncrements * normalCost;
159 }
160
161 // Estimate how many payments will be made
162 Number const numPaymentEstimate = static_cast<std::int64_t>(amount / regularPayment);
163
164 // Charge one base fee per paymentsPerFeeIncrement payments, rounding up.
165 // This set round is safe because there's a mode guard just above
167 auto const feeIncrements = std::max(
168 std::int64_t(1),
169 static_cast<std::int64_t>(numPaymentEstimate / kLoanPaymentsPerFeeIncrement));
170 XRPL_ASSERT(
171 !view.rules().enabled(fixCleanup3_1_3) || feeIncrements <= kMaxFeeIncrements,
172 "xrpl::LoanPay::calculateBaseFee : number of fee increments is in "
173 "range");
174
175 return feeIncrements * normalCost;
176}
177
178TER
180{
181 auto const& tx = ctx.tx;
182
183 auto const account = tx[sfAccount];
184 auto const loanID = tx[sfLoanID];
185 auto const amount = tx[sfAmount];
186
187 auto const loanSle = ctx.view.read(keylet::loan(loanID));
188 if (!loanSle)
189 {
190 JLOG(ctx.j.warn()) << "Loan does not exist.";
191 return tecNO_ENTRY;
192 }
193
194 if (loanSle->at(sfBorrower) != account)
195 {
196 JLOG(ctx.j.warn()) << "Loan does not belong to the account.";
197 return tecNO_PERMISSION;
198 }
199
200 if (tx.isFlag(tfLoanOverpayment) && !loanSle->isFlag(lsfLoanOverpayment))
201 {
202 JLOG(ctx.j.warn()) << "Requested overpayment on a loan that doesn't allow it";
203 return ctx.view.rules().enabled(fixCleanup3_1_3) ? TER{tecNO_PERMISSION} : temINVALID_FLAG;
204 }
205
206 auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
207 auto const paymentRemaining = loanSle->at(sfPaymentRemaining);
208
209 if (paymentRemaining == 0 || principalOutstanding == 0)
210 {
211 JLOG(ctx.j.warn()) << "Loan is already paid off.";
212 return tecKILLED;
213 }
214
215 auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
216 auto const loanBrokerSle = ctx.view.read(keylet::loanBroker(loanBrokerID));
217 if (!loanBrokerSle)
218 {
219 // This should be impossible
220 // LCOV_EXCL_START
221 JLOG(ctx.j.fatal()) << "LoanBroker does not exist.";
222 return tefBAD_LEDGER;
223 // LCOV_EXCL_STOP
224 }
225 auto const vaultID = loanBrokerSle->at(sfVaultID);
226 auto const vaultSle = ctx.view.read(keylet::vault(vaultID));
227 if (!vaultSle)
228 {
229 // This should be impossible
230 // LCOV_EXCL_START
231 JLOG(ctx.j.fatal()) << "Vault does not exist.";
232 return tefBAD_LEDGER;
233 // LCOV_EXCL_STOP
234 }
235 auto const asset = vaultSle->at(sfAsset);
236 auto const vaultPseudoAccount = vaultSle->at(sfAccount);
237
238 if (amount.asset() != asset)
239 {
240 JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset.";
241 return tecWRONG_ASSET;
242 }
243
244 if (auto const ret = checkFrozen(ctx.view, account, asset))
245 {
246 JLOG(ctx.j.warn()) << "Borrower account is frozen.";
247 return ret;
248 }
249 if (auto const ret = checkDeepFrozen(ctx.view, vaultPseudoAccount, asset))
250 {
251 JLOG(ctx.j.warn()) << "Vault pseudo-account can not receive funds (deep frozen).";
252 return ret;
253 }
254 if (auto const ret = requireAuth(ctx.view, asset, account))
255 {
256 JLOG(ctx.j.warn()) << "Borrower account is not authorized.";
257 return ret;
258 }
259 // Make sure the borrower has enough funds to make the payment!
260 // Do not support "partial payments" - if the transaction says to pay X,
261 // then the account must have X available, even if the loan payment takes
262 // less.
263 if (auto const balance = accountHolds(
264 ctx.view,
265 account,
266 asset,
269 ctx.j,
271 balance < amount)
272 {
273 JLOG(ctx.j.warn()) << "Payment amount too large. Amount: " << to_string(amount.getJson())
274 << ". Balance: " << to_string(balance.getJson());
276 }
277
278 return tesSUCCESS;
279}
280
281TER
283{
284 auto const& tx = ctx_.tx;
285 auto& view = ctx_.view();
286
287 auto const amount = tx[sfAmount];
288
289 auto const loanID = tx[sfLoanID];
290 auto const loanSle = view.peek(keylet::loan(loanID));
291 if (!loanSle)
292 return tefBAD_LEDGER; // LCOV_EXCL_LINE
293 std::int32_t const loanScale = loanSle->at(sfLoanScale);
294
295 auto const brokerID = loanSle->at(sfLoanBrokerID);
296 auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
297 if (!brokerSle)
298 return tefBAD_LEDGER; // LCOV_EXCL_LINE
299 auto const brokerOwner = brokerSle->at(sfOwner);
300 auto const brokerPseudoAccount = brokerSle->at(sfAccount);
301 auto const vaultID = brokerSle->at(sfVaultID);
302 auto const vaultSle = view.peek(keylet::vault(vaultID));
303 if (!vaultSle)
304 return tefBAD_LEDGER; // LCOV_EXCL_LINE
305 auto const vaultPseudoAccount = vaultSle->at(sfAccount);
306 auto const asset = *vaultSle->at(sfAsset);
307
308 // Determine where to send the broker's fee
309 auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
310 TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
311 auto debtTotalProxy = brokerSle->at(sfDebtTotal);
312
313 auto const vaultScale = getAssetsTotalScale(vaultSle);
314
315 // Send the broker fee to the owner if they have sufficient cover available,
316 // _and_ if the owner can receive funds
317 // _and_ if the broker is authorized to hold funds. If not, so as not to
318 // block the payment, add it to the cover balance (send it to the broker
319 // pseudo account).
320 //
321 // Normally freeze status is checked in preclaim, but we do it here to
322 // avoid duplicating the check. It'll claim a fee either way.
323 bool const sendBrokerFeeToOwner = [&]() {
324 // In the fixCleanup3_2_0 path, vault-related values (for example,
325 // DebtTotal) use vaultScale. The legacy path below intentionally retains
326 // its pre-amendment loanScale behavior.
327 auto const minCover = [&]() {
328 if (view.rules().enabled(fixCleanup3_2_0))
329 {
330 return minimumBrokerCover(debtTotalProxy.value(), coverRateMinimum, vaultSle);
331 }
332 // Round the minimum required cover up to be conservative. This ensures
333 // CoverAvailable never drops below the theoretical minimum, protecting
334 // the broker's solvency.
336 return roundToAsset(
337 asset, tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), loanScale);
338 }();
339 return coverAvailableProxy >= minCover && !isDeepFrozen(view, brokerOwner, asset) &&
340 !requireAuth(view, asset, brokerOwner, AuthType::StrongAuth);
341 }();
342
343 auto const brokerPayee = sendBrokerFeeToOwner ? brokerOwner : brokerPseudoAccount;
344 auto const brokerPayeeSle = view.peek(keylet::account(brokerPayee));
345 if (!sendBrokerFeeToOwner)
346 {
347 // If we can't send the fee to the owner, and the pseudo-account is
348 // frozen, then we have to fail the payment.
349 if (auto const ret = checkDeepFrozen(view, brokerPayee, asset))
350 {
351 JLOG(j_.warn()) << "Both Loan Broker and Loan Broker pseudo-account "
352 "can not receive funds (deep frozen).";
353 return ret;
354 }
355 }
356
357 //------------------------------------------------------
358 // Loan object state changes
359
360 // Unimpair the loan if it was impaired. Do this before the payment is
361 // attempted, so the original values can be used. If the payment fails, this
362 // change will be discarded.
363 if (loanSle->isFlag(lsfLoanImpaired))
364 {
365 if (auto const ret = LoanManage::unimpairLoan(view, loanSle, vaultSle, asset, j_))
366 {
367 JLOG(j_.fatal()) << "Failed to unimpair loan before payment.";
368 return ret; // LCOV_EXCL_LINE
369 }
370 }
371
372 LoanPaymentType const paymentType = [&tx]() {
373 // preflight already checked that at most one flag is set.
374 if (tx.isFlag(tfLoanLatePayment))
376 if (tx.isFlag(tfLoanFullPayment))
378 if (tx.isFlag(tfLoanOverpayment))
381 }();
382
383 std::expected<LoanPaymentParts, TER> const paymentParts =
384 loanMakePayment(asset, view, loanSle, brokerSle, amount, paymentType, j_);
385
386 if (!paymentParts)
387 {
388 XRPL_ASSERT_PARTS(
389 paymentParts.error(), "xrpl::LoanPay::doApply", "payment error is an error");
390 return paymentParts.error();
391 }
392
393 // If the payment computation completed without error, the loanSle object
394 // has been modified.
395 view.update(loanSle);
396
397 XRPL_ASSERT_PARTS(
398 // It is possible to pay 0 principal
399 paymentParts->principalPaid >= 0,
400 "xrpl::LoanPay::doApply",
401 "valid principal paid");
402 XRPL_ASSERT_PARTS(
403 // It is possible to pay 0 interest
404 paymentParts->interestPaid >= 0,
405 "xrpl::LoanPay::doApply",
406 "valid interest paid");
407 XRPL_ASSERT_PARTS(
408 // It should not be possible to pay 0 total
409 paymentParts->principalPaid + paymentParts->interestPaid > 0,
410 "xrpl::LoanPay::doApply",
411 "valid total paid");
412 XRPL_ASSERT_PARTS(paymentParts->feePaid >= 0, "xrpl::LoanPay::doApply", "valid fee paid");
413
414 if (paymentParts->principalPaid < 0 || paymentParts->interestPaid < 0 ||
415 paymentParts->feePaid < 0)
416 {
417 // LCOV_EXCL_START
418 JLOG(j_.fatal()) << "Loan payment computation returned invalid values.";
419 return tecLIMIT_EXCEEDED;
420 // LCOV_EXCL_STOP
421 }
422
423 auto const [assetsTotalDelta, debtTotalDelta] = loanPaymentDeltas(vaultSle, *paymentParts);
424
425 JLOG(j_.debug()) << "Loan Pay: principal paid: " << paymentParts->principalPaid
426 << ", interest paid: " << paymentParts->interestPaid
427 << ", fee paid: " << paymentParts->feePaid
428 << ", assets total delta: " << assetsTotalDelta
429 << ", debt total delta: " << debtTotalDelta;
430
431 //------------------------------------------------------
432 // LoanBroker object state changes
433 view.update(brokerSle);
434
435 auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable);
436 auto assetsTotalProxy = vaultSle->at(sfAssetsTotal);
437
438 auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid;
439 auto const totalPaidToVaultRounded =
440 roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward);
441 XRPL_ASSERT_PARTS(
442 !asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded,
443 "xrpl::LoanPay::doApply",
444 "rounding does nothing for integral asset");
445 auto const totalPaidToBroker = paymentParts->feePaid;
446
447 XRPL_ASSERT_PARTS(
448 (totalPaidToVaultRaw + totalPaidToBroker) ==
449 (paymentParts->principalPaid + paymentParts->interestPaid + paymentParts->feePaid),
450 "xrpl::LoanPay::doApply",
451 "payments add up");
452
453 // Decrease LoanBroker Debt by the amount paid, add the Loan value change
454 // (which might be negative). debtTotalDelta may be negative, increasing the
455 // debt
456 XRPL_ASSERT_PARTS(
457 isRounded(asset, debtTotalDelta, loanScale),
458 "xrpl::LoanPay::doApply",
459 "debtTotalDelta rounding good");
460 // Despite our best efforts, it's possible for rounding errors to accumulate
461 // in the loan broker's debt total. This is because the broker may have more
462 // than one loan with significantly different scales.
463 adjustImpreciseNumber(debtTotalProxy, -debtTotalDelta, asset, vaultScale);
464
465 //------------------------------------------------------
466 // Vault object state changes
467 view.update(vaultSle);
468
469 Number const assetsAvailableBefore = *assetsAvailableProxy;
470 Number const assetsTotalBefore = *assetsTotalProxy;
471#if !NDEBUG
472 {
473 Number const pseudoAccountBalanceBefore = accountHolds(
474 view,
475 vaultPseudoAccount,
476 asset,
479 j_);
480
481 XRPL_ASSERT_PARTS(
482 assetsAvailableBefore == pseudoAccountBalanceBefore,
483 "xrpl::LoanPay::doApply",
484 "vault pseudo balance agrees before");
485 }
486#endif
487
488 assetsAvailableProxy += totalPaidToVaultRounded;
489 assetsTotalProxy += assetsTotalDelta;
490
491 XRPL_ASSERT_PARTS(
492 *assetsAvailableProxy <= *assetsTotalProxy,
493 "xrpl::LoanPay::doApply",
494 "assets available must not be greater than assets outstanding");
495
496 JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
497 << ", total paid to vault rounded: " << totalPaidToVaultRounded
498 << ", total paid to broker: " << totalPaidToBroker
499 << ", amount from transaction: " << amount;
500
501 // Move funds
502 XRPL_ASSERT_PARTS(
503 totalPaidToVaultRounded + totalPaidToBroker <= amount,
504 "xrpl::LoanPay::doApply",
505 "amount is sufficient");
506
507 if (!sendBrokerFeeToOwner)
508 {
509 // If there is not enough first-loss capital, add the fee to First Loss
510 // Cover Pool. Note that this moves the entire fee - it does not attempt
511 // to split it. The broker can Withdraw it later if they want, or leave
512 // it for future needs.
513 coverAvailableProxy += totalPaidToBroker;
514 }
515
516 associateAsset(*loanSle, asset);
517 associateAsset(*brokerSle, asset);
518 associateAsset(*vaultSle, asset);
519
520 // Duplicate some checks after rounding
521 Number const assetsAvailableAfter = *assetsAvailableProxy;
522 Number const assetsTotalAfter = *assetsTotalProxy;
523
524 XRPL_ASSERT_PARTS(
525 assetsAvailableAfter <= assetsTotalAfter,
526 "xrpl::LoanPay::doApply",
527 "assets available must not be greater than assets outstanding");
528 if (assetsAvailableAfter == assetsAvailableBefore)
529 {
530 // An unchanged assetsAvailable indicates that the amount paid to the
531 // vault was zero, or rounded to zero. That should be impossible, but I
532 // can't rule it out for extreme edge cases, so fail gracefully if it
533 // happens.
534 //
535 // LCOV_EXCL_START
536 JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
537 << "Before: " << assetsAvailableBefore //
538 << ", After: " << assetsAvailableAfter;
539 return tecPRECISION_LOSS;
540 // LCOV_EXCL_STOP
541 }
542 if (assetsTotalDelta != beast::kZero && assetsTotalAfter == assetsTotalBefore)
543 {
544 // Non-zero assetsTotalDelta with an unchanged assetsTotal indicates that
545 // the actual value change rounded to zero. That should be impossible, but
546 // I can't rule it out for extreme edge cases, so fail gracefully if it
547 // happens.
548 //
549 // LCOV_EXCL_START
550 JLOG(j_.warn())
551 << "LoanPay: Vault assets expected change, but unchanged after rounding: " //
552 << "Before: " << assetsTotalBefore //
553 << ", After: " << assetsTotalAfter //
554 << ", AssetsTotalDelta: " << assetsTotalDelta;
555 return tecPRECISION_LOSS;
556 // LCOV_EXCL_STOP
557 }
558 if (assetsTotalDelta == beast::kZero && assetsTotalAfter != assetsTotalBefore)
559 {
560 // A change in assetsTotal when there was no assetsTotalDelta indicates
561 // that something really weird happened. That should be flat out
562 // impossible.
563 //
564 // LCOV_EXCL_START
565 JLOG(j_.fatal()) << "LoanPay: Vault assets changed unexpectedly after rounding: " //
566 << "Before: " << assetsTotalBefore //
567 << ", After: " << assetsTotalAfter //
568 << ", AssetsTotalDelta: " << assetsTotalDelta;
569 return tecINTERNAL;
570 // LCOV_EXCL_STOP
571 }
572 if (assetsAvailableAfter > assetsTotalAfter)
573 {
574 // Assets available are not allowed to be larger than assets total.
575 // LCOV_EXCL_START
576 JLOG(j_.fatal()) << "LoanPay: Vault assets available must not be greater "
577 "than assets outstanding. Available: "
578 << assetsAvailableAfter << ", Total: " << assetsTotalAfter;
579 return tecINTERNAL;
580 // LCOV_EXCL_STOP
581 }
582
583 // These three values are used to check that funds are conserved after the transfers
584 auto const accountBalanceBefore = accountHolds(
585 view,
587 asset,
590 j_,
592 auto const vaultBalanceBefore = accountID_ == vaultPseudoAccount
593 ? STAmount{asset, 0}
594 : accountHolds(
595 view,
596 vaultPseudoAccount,
597 asset,
600 j_,
602 auto const brokerBalanceBefore = accountID_ == brokerPayee
603 ? STAmount{asset, 0}
604 : accountHolds(
605 view,
606 brokerPayee,
607 asset,
610 j_,
612
613 if (totalPaidToVaultRounded != beast::kZero)
614 {
615 if (auto const ter = requireAuth(view, asset, vaultPseudoAccount, AuthType::StrongAuth))
616 return ter;
617 }
618
619 if (totalPaidToBroker != beast::kZero)
620 {
621 if (brokerPayee == accountID_)
622 {
623 // The broker may have deleted their holding. Recreate it if needed
624 if (auto const ter = addEmptyHolding(
625 ctx_.getApplyViewContext(),
626 brokerPayee,
627 brokerPayeeSle->at(sfBalance).value().xrp(),
628 asset,
629 j_);
630 ter && ter != tecDUPLICATE)
631 {
632 // ignore tecDUPLICATE. That means the holding already exists,
633 // and is fine here
634 return ter;
635 }
636 }
637 if (auto const ter = requireAuth(view, asset, brokerPayee, AuthType::StrongAuth))
638 return ter;
639 }
640
641 if (auto const ter = accountSendMulti(
642 view,
644 asset,
645 {{vaultPseudoAccount, totalPaidToVaultRounded}, {brokerPayee, totalPaidToBroker}},
646 j_,
648 return ter;
649
650#if !NDEBUG
651 {
652 Number const pseudoAccountBalanceAfter = accountHolds(
653 view,
654 vaultPseudoAccount,
655 asset,
658 j_);
659 XRPL_ASSERT_PARTS(
660 assetsAvailableAfter == pseudoAccountBalanceAfter,
661 "xrpl::LoanPay::doApply",
662 "vault pseudo balance agrees after");
663 }
664#endif
665
666 // Check that funds are conserved
667 auto const accountBalanceAfter = accountHolds(
668 view,
670 asset,
673 j_,
675 auto const vaultBalanceAfter = accountID_ == vaultPseudoAccount
676 ? STAmount{asset, 0}
677 : accountHolds(
678 view,
679 vaultPseudoAccount,
680 asset,
683 j_,
685 auto const brokerBalanceAfter = accountID_ == brokerPayee ? STAmount{asset, 0}
686 : accountHolds(
687 view,
688 brokerPayee,
689 asset,
692 j_,
694 auto const balanceScale = [&]() {
695 // Find a reasonable scale to use for the balance comparisons.
696 //
697 // First find the minimum and maximum exponent of all the non-zero balances, before and
698 // after. If min and max are equal, use that value. If they are not, use "max + 1" to reduce
699 // rounding discrepancies without making the result meaningless. Cap the scale at
700 // STAmount::kMaxOffset, just in case the numbers are all very large.
701 std::vector<int> exponents;
702 exponents.reserve(6);
703
704 for (auto const& a : {
705 accountBalanceBefore,
706 vaultBalanceBefore,
707 brokerBalanceBefore,
708 accountBalanceAfter,
709 vaultBalanceAfter,
710 brokerBalanceAfter,
711 })
712 {
713 // Exclude zeroes
714 if (a != beast::kZero)
715 exponents.push_back(a.exponent());
716 }
717 if (exponents.empty())
718 {
719 UNREACHABLE("xrpl::LoanPay::doApply : all zeroes");
720 return 0;
721 }
722 auto const [minItr, maxItr] = std::ranges::minmax_element(exponents);
723 auto const min = *minItr;
724 auto const max = *maxItr;
725 JLOG(j_.trace()) << "Min scale: " << min << ", max scale: " << max;
726 // IOU rounding can be interesting. We want all the balance checks to agree, but don't want
727 // to round to such an extreme that it becomes meaningless. e.g. Everything rounds to one
728 // digit. So add 1 to the max (reducing the number of digits after the decimal point by 1)
729 // if the scales are not already all the same.
730 return std::min(min == max ? max : max + 1, STAmount::kMaxOffset);
731 }();
732
733 // No object changes are made below this point
734 XRPL_ASSERT_PARTS(
736 "xrpl::LoanPay::doApply",
737 "Number rounding ToNearest");
739
740 auto const accountBalanceBeforeRounded = roundToScale(accountBalanceBefore, balanceScale);
741 auto const vaultBalanceBeforeRounded = roundToScale(vaultBalanceBefore, balanceScale);
742 auto const brokerBalanceBeforeRounded = roundToScale(brokerBalanceBefore, balanceScale);
743
744 auto const totalBalanceBefore = accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore;
745 auto const totalBalanceBeforeRounded = roundToScale(totalBalanceBefore, balanceScale);
746
747 JLOG(j_.trace()) << "Before: " //
748 << "account " << Number(accountBalanceBeforeRounded) << " ("
749 << Number(accountBalanceBefore) << ")"
750 << ", vault " << Number(vaultBalanceBeforeRounded) << " ("
751 << Number(vaultBalanceBefore) << ")"
752 << ", broker " << Number(brokerBalanceBeforeRounded) << " ("
753 << Number(brokerBalanceBefore) << ")"
754 << ", total " << Number(totalBalanceBeforeRounded) << " ("
755 << Number(totalBalanceBefore) << ")";
756
757 auto const accountBalanceAfterRounded = roundToScale(accountBalanceAfter, balanceScale);
758 auto const vaultBalanceAfterRounded = roundToScale(vaultBalanceAfter, balanceScale);
759 auto const brokerBalanceAfterRounded = roundToScale(brokerBalanceAfter, balanceScale);
760
761 auto const totalBalanceAfter = accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter;
762 auto const totalBalanceAfterRounded = roundToScale(totalBalanceAfter, balanceScale);
763
764 JLOG(j_.trace()) << "After: " //
765 << "account " << Number(accountBalanceAfterRounded) << " ("
766 << Number(accountBalanceAfter) << ")"
767 << ", vault " << Number(vaultBalanceAfterRounded) << " ("
768 << Number(vaultBalanceAfter) << ")"
769 << ", broker " << Number(brokerBalanceAfterRounded) << " ("
770 << Number(brokerBalanceAfter) << ")"
771 << ", total " << Number(totalBalanceAfterRounded) << " ("
772 << Number(totalBalanceAfter) << ")";
773
774 auto const accountBalanceChange = accountBalanceAfter - accountBalanceBefore;
775 auto const vaultBalanceChange = vaultBalanceAfter - vaultBalanceBefore;
776 auto const brokerBalanceChange = brokerBalanceAfter - brokerBalanceBefore;
777
778 auto const totalBalanceChange = accountBalanceChange + vaultBalanceChange + brokerBalanceChange;
779 auto const totalBalanceChangeRounded = roundToScale(totalBalanceChange, balanceScale);
780
781 JLOG(j_.trace()) << "Changes: " //
782 << "account " << to_string(accountBalanceChange) //
783 << ", vault " << to_string(vaultBalanceChange) //
784 << ", broker " << to_string(brokerBalanceChange) //
785 << ", total " << to_string(totalBalanceChangeRounded) << " ("
786 << Number(totalBalanceChange) << ")";
787
788 bool const goodRounding = totalBalanceBeforeRounded == totalBalanceAfterRounded ||
789 totalBalanceChangeRounded == beast::kZero;
790 if (totalBalanceBeforeRounded != totalBalanceAfterRounded)
791 {
792 JLOG((goodRounding ? j_.debug() : j_.warn()))
793 << "Total rounded balances don't match"
794 << (totalBalanceChangeRounded == beast::kZero ? ", but total changes do" : "");
795 }
796 if (totalBalanceChangeRounded != beast::kZero)
797 {
798 JLOG((goodRounding ? j_.debug() : j_.warn()))
799 << "Total balance changes don't match"
800 << (totalBalanceBeforeRounded == totalBalanceAfterRounded ? ", but total balances do"
801 : "");
802 }
803
804 // Rounding for IOUs can be weird, so check a few different ways to show
805 // that funds are conserved.
806 XRPL_ASSERT_PARTS(
807 goodRounding, "xrpl::LoanPay::doApply", "funds are conserved (with rounding)");
808
809 XRPL_ASSERT_PARTS(
810 accountBalanceAfter < accountBalanceBefore || accountID_ == asset.getIssuer(),
811 "xrpl::LoanPay::doApply",
812 "account balance decreased");
813 XRPL_ASSERT_PARTS(
814 vaultBalanceAfter >= beast::kZero && brokerBalanceAfter >= beast::kZero,
815 "xrpl::LoanPay::doApply",
816 "non-negative vault and broker balances");
817 XRPL_ASSERT_PARTS(
818 vaultBalanceAfter >= vaultBalanceBefore,
819 "xrpl::LoanPay::doApply",
820 "vault balance did not decrease");
821 XRPL_ASSERT_PARTS(
822 brokerBalanceAfter >= brokerBalanceBefore,
823 "xrpl::LoanPay::doApply",
824 "broker balance did not decrease");
825 XRPL_ASSERT_PARTS(
826 vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore,
827 "xrpl::LoanPay::doApply",
828 "vault and/or broker balance increased");
829
830 return tesSUCCESS;
831}
832
833void
835{
836 // No transaction-specific invariants yet (future work).
837}
838
839bool
841{
842 // No transaction-specific invariants yet (future work).
843 return true;
844}
845
846//------------------------------------------------------------------------------
847
848} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
Stream warn() const
Definition Journal.h:356
static TER unimpairLoan(ApplyView &view, SLE::ref loanSle, SLE::ref vaultSle, Asset const &vaultAsset, beast::Journal j)
Helper function that might be needed by other transactors.
bool finalizeInvariants(STTx const &tx, TER result, XRPAmount fee, ReadView const &view, beast::Journal const &j) override
Check transaction-specific post-conditions after all entries have been visited.
Definition LoanPay.cpp:840
static TER preclaim(PreclaimContext const &ctx)
Definition LoanPay.cpp:179
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
Definition LoanPay.cpp:43
void visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override
Inspect a single ledger entry modified by this transaction.
Definition LoanPay.cpp:834
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
Definition LoanPay.cpp:74
static bool checkExtraFeatures(PreflightContext const &ctx)
Definition LoanPay.cpp:37
TER doApply() override
Definition LoanPay.cpp:282
static NotTEC preflight(PreflightContext const &ctx)
Definition LoanPay.cpp:49
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
static RoundingMode setround(RoundingMode inMode)
static RoundingMode getround()
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.
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
std::shared_ptr< STLedgerEntry const > const & const_ref
bool isFlag(std::uint32_t) const
Definition STObject.cpp:511
std::uint32_t getFlags() const
Definition STObject.cpp:517
beast::Journal const j_
Definition Transactor.h:155
ApplyView & view()
Definition Transactor.h:175
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
AccountID const accountID_
Definition Transactor.h:157
ApplyContext & ctx_
Definition Transactor.h:153
T empty(T... args)
T max(T... args)
T min(T... args)
T minmax_element(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:573
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:561
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:567
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
TER checkDeepFrozen(ReadView const &view, AccountID const &account, Issue const &issue)
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp, ExpiryComparison comparison=ExpiryComparison::Inclusive)
Determines whether the given expiration time has passed.
Definition View.cpp:48
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:138
int getAssetsTotalScale(SLE::const_ref vaultSle)
TER checkFrozen(ReadView const &view, AccountID const &account, Issue const &issue)
void adjustImpreciseNumber(NumberProxy value, Number const &adjustment, Asset const &asset, int vaultScale)
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:794
@ tefBAD_LEDGER
Definition TER.h:162
Number minimumBrokerCover(Number const &debtTotal, TenthBips32 coverRateMinimum, SLE::const_ref vaultSle)
TER addEmptyHolding(ApplyViewContext ctx, AccountID const &accountID, XRPAmount priorBalance, MPTIssue const &mptIssue, beast::Journal journal)
AccountingDeltas loanPaymentDeltas(SLE::const_ref vaultSle, LoanPaymentParts const &parts)
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
bool isDeepFrozen(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer)
constexpr FlagValue tfUniversal
Definition TxFlags.h:45
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
STAmount roundToScale(STAmount const &value, std::int32_t scale, Number::RoundingMode rounding=Number::getround())
Round an arbitrary precision Amount to the precision of an STAmount that has a given exponent.
std::expected< LoanPaymentParts, TER > loanMakePayment(Asset const &asset, ApplyView &view, SLE::ref loan, SLE::const_ref brokerSle, STAmount const &amount, LoanPaymentType const paymentType, beast::Journal j)
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:607
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
Number roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
@ temINVALID
Definition TER.h:98
@ temINVALID_FLAG
Definition TER.h:99
@ temBAD_AMOUNT
Definition TER.h:77
TERSubset< CanCvtToTER > TER
Definition TER.h:647
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, std::uint8_t depth=0)
Check if the account lacks required authorization for MPT.
@ tecWRONG_ASSET
Definition TER.h:363
@ tecNO_ENTRY
Definition TER.h:309
@ tecINTERNAL
Definition TER.h:313
@ tecINSUFFICIENT_FUNDS
Definition TER.h:328
@ tecPRECISION_LOSS
Definition TER.h:366
@ tecKILLED
Definition TER.h:319
@ tecLIMIT_EXCEEDED
Definition TER.h:364
@ tecNO_PERMISSION
Definition TER.h:308
@ tecDUPLICATE
Definition TER.h:318
void associateAsset(STLedgerEntry &sle, Asset const &asset)
Associate an Asset with all sMD_NeedsAsset fields in a ledger entry.
TER accountSendMulti(ApplyView &view, AccountID const &senderID, Asset const &asset, MultiplePaymentDestinations const &receivers, beast::Journal j, WaiveTransferFee waiveFee=WaiveTransferFee::No)
Like accountSend, except one account is sending multiple payments (with the same asset!...
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j, SpendableHandling includeFullBalance=SpendableHandling::SimpleBalance)
@ tesSUCCESS
Definition TER.h:245
bool checkLendingProtocolDependencies(Rules const &rules, STTx const &tx)
bool isRounded(Asset const &asset, Number const &value, std::int32_t scale)
T popcount(T... args)
T push_back(T... args)
T reserve(T... args)
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:83
ReadView const & view
Definition Transactor.h:86
beast::Journal const j
Definition Transactor.h:91
State information when preflighting a tx.
Definition Transactor.h:38
beast::Journal const j
Definition Transactor.h:45