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 JLOG(j_.debug()) << "Loan Pay: principal paid: " << paymentParts->principalPaid
424 << ", interest paid: " << paymentParts->interestPaid
425 << ", fee paid: " << paymentParts->feePaid
426 << ", value change: " << paymentParts->valueChange;
427
428 //------------------------------------------------------
429 // LoanBroker object state changes
430 view.update(brokerSle);
431
432 auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable);
433 auto assetsTotalProxy = vaultSle->at(sfAssetsTotal);
434
435 auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid;
436 auto const totalPaidToVaultRounded =
437 roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward);
438 XRPL_ASSERT_PARTS(
439 !asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded,
440 "xrpl::LoanPay::doApply",
441 "rounding does nothing for integral asset");
442 // Account for value changes when reducing the broker's debt:
443 // - Positive value change (from full/late/overpayments): Subtract from the
444 // amount credited toward debt to avoid over-reducing the debt.
445 // - Negative value change (from full/overpayments): Add to the amount
446 // credited toward debt,effectively increasing the debt reduction.
447 auto const totalPaidToVaultForDebt = totalPaidToVaultRaw - paymentParts->valueChange;
448
449 auto const totalPaidToBroker = paymentParts->feePaid;
450
451 XRPL_ASSERT_PARTS(
452 (totalPaidToVaultRaw + totalPaidToBroker) ==
453 (paymentParts->principalPaid + paymentParts->interestPaid + paymentParts->feePaid),
454 "xrpl::LoanPay::doApply",
455 "payments add up");
456
457 // Decrease LoanBroker Debt by the amount paid, add the Loan value change
458 // (which might be negative). totalPaidToVaultForDebt may be negative,
459 // increasing the debt
460 XRPL_ASSERT_PARTS(
461 isRounded(asset, totalPaidToVaultForDebt, loanScale),
462 "xrpl::LoanPay::doApply",
463 "totalPaidToVaultForDebt rounding good");
464 // Despite our best efforts, it's possible for rounding errors to accumulate
465 // in the loan broker's debt total. This is because the broker may have more
466 // than one loan with significantly different scales.
467 adjustImpreciseNumber(debtTotalProxy, -totalPaidToVaultForDebt, asset, vaultScale);
468
469 //------------------------------------------------------
470 // Vault object state changes
471 view.update(vaultSle);
472
473 Number const assetsAvailableBefore = *assetsAvailableProxy;
474 Number const assetsTotalBefore = *assetsTotalProxy;
475#if !NDEBUG
476 {
477 Number const pseudoAccountBalanceBefore = accountHolds(
478 view,
479 vaultPseudoAccount,
480 asset,
483 j_);
484
485 XRPL_ASSERT_PARTS(
486 assetsAvailableBefore == pseudoAccountBalanceBefore,
487 "xrpl::LoanPay::doApply",
488 "vault pseudo balance agrees before");
489 }
490#endif
491
492 assetsAvailableProxy += totalPaidToVaultRounded;
493 assetsTotalProxy += paymentParts->valueChange;
494
495 XRPL_ASSERT_PARTS(
496 *assetsAvailableProxy <= *assetsTotalProxy,
497 "xrpl::LoanPay::doApply",
498 "assets available must not be greater than assets outstanding");
499
500 JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
501 << ", total paid to vault rounded: " << totalPaidToVaultRounded
502 << ", total paid to broker: " << totalPaidToBroker
503 << ", amount from transaction: " << amount;
504
505 // Move funds
506 XRPL_ASSERT_PARTS(
507 totalPaidToVaultRounded + totalPaidToBroker <= amount,
508 "xrpl::LoanPay::doApply",
509 "amount is sufficient");
510
511 if (!sendBrokerFeeToOwner)
512 {
513 // If there is not enough first-loss capital, add the fee to First Loss
514 // Cover Pool. Note that this moves the entire fee - it does not attempt
515 // to split it. The broker can Withdraw it later if they want, or leave
516 // it for future needs.
517 coverAvailableProxy += totalPaidToBroker;
518 }
519
520 associateAsset(*loanSle, asset);
521 associateAsset(*brokerSle, asset);
522 associateAsset(*vaultSle, asset);
523
524 // Duplicate some checks after rounding
525 Number const assetsAvailableAfter = *assetsAvailableProxy;
526 Number const assetsTotalAfter = *assetsTotalProxy;
527
528 XRPL_ASSERT_PARTS(
529 assetsAvailableAfter <= assetsTotalAfter,
530 "xrpl::LoanPay::doApply",
531 "assets available must not be greater than assets outstanding");
532 if (assetsAvailableAfter == assetsAvailableBefore)
533 {
534 // An unchanged assetsAvailable indicates that the amount paid to the
535 // vault was zero, or rounded to zero. That should be impossible, but I
536 // can't rule it out for extreme edge cases, so fail gracefully if it
537 // happens.
538 //
539 // LCOV_EXCL_START
540 JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
541 << "Before: " << assetsAvailableBefore //
542 << ", After: " << assetsAvailableAfter;
543 return tecPRECISION_LOSS;
544 // LCOV_EXCL_STOP
545 }
546 if (paymentParts->valueChange != beast::kZero && assetsTotalAfter == assetsTotalBefore)
547 {
548 // Non-zero valueChange with an unchanged assetsTotal indicates that the
549 // actual value change rounded to zero. That should be impossible, but I
550 // can't rule it out for extreme edge cases, so fail gracefully if it
551 // happens.
552 //
553 // LCOV_EXCL_START
554 JLOG(j_.warn())
555 << "LoanPay: Vault assets expected change, but unchanged after rounding: " //
556 << "Before: " << assetsTotalBefore //
557 << ", After: " << assetsTotalAfter //
558 << ", ValueChange: " << paymentParts->valueChange;
559 return tecPRECISION_LOSS;
560 // LCOV_EXCL_STOP
561 }
562 if (paymentParts->valueChange == beast::kZero && assetsTotalAfter != assetsTotalBefore)
563 {
564 // A change in assetsTotal when there was no valueChange indicates that
565 // something really weird happened. That should be flat out impossible.
566 //
567 // LCOV_EXCL_START
568 JLOG(j_.fatal()) << "LoanPay: Vault assets changed unexpectedly after rounding: " //
569 << "Before: " << assetsTotalBefore //
570 << ", After: " << assetsTotalAfter //
571 << ", ValueChange: " << paymentParts->valueChange;
572 return tecINTERNAL;
573 // LCOV_EXCL_STOP
574 }
575 if (assetsAvailableAfter > assetsTotalAfter)
576 {
577 // Assets available are not allowed to be larger than assets total.
578 // LCOV_EXCL_START
579 JLOG(j_.fatal()) << "LoanPay: Vault assets available must not be greater "
580 "than assets outstanding. Available: "
581 << assetsAvailableAfter << ", Total: " << assetsTotalAfter;
582 return tecINTERNAL;
583 // LCOV_EXCL_STOP
584 }
585
586 // These three values are used to check that funds are conserved after the transfers
587 auto const accountBalanceBefore = accountHolds(
588 view,
590 asset,
593 j_,
595 auto const vaultBalanceBefore = accountID_ == vaultPseudoAccount
596 ? STAmount{asset, 0}
597 : accountHolds(
598 view,
599 vaultPseudoAccount,
600 asset,
603 j_,
605 auto const brokerBalanceBefore = accountID_ == brokerPayee
606 ? STAmount{asset, 0}
607 : accountHolds(
608 view,
609 brokerPayee,
610 asset,
613 j_,
615
616 if (totalPaidToVaultRounded != beast::kZero)
617 {
618 if (auto const ter = requireAuth(view, asset, vaultPseudoAccount, AuthType::StrongAuth))
619 return ter;
620 }
621
622 if (totalPaidToBroker != beast::kZero)
623 {
624 if (brokerPayee == accountID_)
625 {
626 // The broker may have deleted their holding. Recreate it if needed
627 if (auto const ter = addEmptyHolding(
628 view, brokerPayee, brokerPayeeSle->at(sfBalance).value().xrp(), asset, j_);
629 ter && ter != tecDUPLICATE)
630 {
631 // ignore tecDUPLICATE. That means the holding already exists,
632 // and is fine here
633 return ter;
634 }
635 }
636 if (auto const ter = requireAuth(view, asset, brokerPayee, AuthType::StrongAuth))
637 return ter;
638 }
639
640 if (auto const ter = accountSendMulti(
641 view,
643 asset,
644 {{vaultPseudoAccount, totalPaidToVaultRounded}, {brokerPayee, totalPaidToBroker}},
645 j_,
647 return ter;
648
649#if !NDEBUG
650 {
651 Number const pseudoAccountBalanceAfter = accountHolds(
652 view,
653 vaultPseudoAccount,
654 asset,
657 j_);
658 XRPL_ASSERT_PARTS(
659 assetsAvailableAfter == pseudoAccountBalanceAfter,
660 "xrpl::LoanPay::doApply",
661 "vault pseudo balance agrees after");
662 }
663#endif
664
665 // Check that funds are conserved
666 auto const accountBalanceAfter = accountHolds(
667 view,
669 asset,
672 j_,
674 auto const vaultBalanceAfter = accountID_ == vaultPseudoAccount
675 ? STAmount{asset, 0}
676 : accountHolds(
677 view,
678 vaultPseudoAccount,
679 asset,
682 j_,
684 auto const brokerBalanceAfter = accountID_ == brokerPayee ? STAmount{asset, 0}
685 : accountHolds(
686 view,
687 brokerPayee,
688 asset,
691 j_,
693 auto const balanceScale = [&]() {
694 // Find a reasonable scale to use for the balance comparisons.
695 //
696 // First find the minimum and maximum exponent of all the non-zero balances, before and
697 // after. If min and max are equal, use that value. If they are not, use "max + 1" to reduce
698 // rounding discrepancies without making the result meaningless. Cap the scale at
699 // STAmount::kMaxOffset, just in case the numbers are all very large.
700 std::vector<int> exponents;
701 exponents.reserve(6);
702
703 for (auto const& a : {
704 accountBalanceBefore,
705 vaultBalanceBefore,
706 brokerBalanceBefore,
707 accountBalanceAfter,
708 vaultBalanceAfter,
709 brokerBalanceAfter,
710 })
711 {
712 // Exclude zeroes
713 if (a != beast::kZero)
714 exponents.push_back(a.exponent());
715 }
716 if (exponents.empty())
717 {
718 UNREACHABLE("xrpl::LoanPay::doApply : all zeroes");
719 return 0;
720 }
721 auto const [minItr, maxItr] = std::ranges::minmax_element(exponents);
722 auto const min = *minItr;
723 auto const max = *maxItr;
724 JLOG(j_.trace()) << "Min scale: " << min << ", max scale: " << max;
725 // IOU rounding can be interesting. We want all the balance checks to agree, but don't want
726 // to round to such an extreme that it becomes meaningless. e.g. Everything rounds to one
727 // digit. So add 1 to the max (reducing the number of digits after the decimal point by 1)
728 // if the scales are not already all the same.
729 return std::min(min == max ? max : max + 1, STAmount::kMaxOffset);
730 }();
731
732 // No object changes are made below this point
733 XRPL_ASSERT_PARTS(
735 "xrpl::LoanPay::doApply",
736 "Number rounding ToNearest");
738
739 auto const accountBalanceBeforeRounded = roundToScale(accountBalanceBefore, balanceScale);
740 auto const vaultBalanceBeforeRounded = roundToScale(vaultBalanceBefore, balanceScale);
741 auto const brokerBalanceBeforeRounded = roundToScale(brokerBalanceBefore, balanceScale);
742
743 auto const totalBalanceBefore = accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore;
744 auto const totalBalanceBeforeRounded = roundToScale(totalBalanceBefore, balanceScale);
745
746 JLOG(j_.trace()) << "Before: " //
747 << "account " << Number(accountBalanceBeforeRounded) << " ("
748 << Number(accountBalanceBefore) << ")"
749 << ", vault " << Number(vaultBalanceBeforeRounded) << " ("
750 << Number(vaultBalanceBefore) << ")"
751 << ", broker " << Number(brokerBalanceBeforeRounded) << " ("
752 << Number(brokerBalanceBefore) << ")"
753 << ", total " << Number(totalBalanceBeforeRounded) << " ("
754 << Number(totalBalanceBefore) << ")";
755
756 auto const accountBalanceAfterRounded = roundToScale(accountBalanceAfter, balanceScale);
757 auto const vaultBalanceAfterRounded = roundToScale(vaultBalanceAfter, balanceScale);
758 auto const brokerBalanceAfterRounded = roundToScale(brokerBalanceAfter, balanceScale);
759
760 auto const totalBalanceAfter = accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter;
761 auto const totalBalanceAfterRounded = roundToScale(totalBalanceAfter, balanceScale);
762
763 JLOG(j_.trace()) << "After: " //
764 << "account " << Number(accountBalanceAfterRounded) << " ("
765 << Number(accountBalanceAfter) << ")"
766 << ", vault " << Number(vaultBalanceAfterRounded) << " ("
767 << Number(vaultBalanceAfter) << ")"
768 << ", broker " << Number(brokerBalanceAfterRounded) << " ("
769 << Number(brokerBalanceAfter) << ")"
770 << ", total " << Number(totalBalanceAfterRounded) << " ("
771 << Number(totalBalanceAfter) << ")";
772
773 auto const accountBalanceChange = accountBalanceAfter - accountBalanceBefore;
774 auto const vaultBalanceChange = vaultBalanceAfter - vaultBalanceBefore;
775 auto const brokerBalanceChange = brokerBalanceAfter - brokerBalanceBefore;
776
777 auto const totalBalanceChange = accountBalanceChange + vaultBalanceChange + brokerBalanceChange;
778 auto const totalBalanceChangeRounded = roundToScale(totalBalanceChange, balanceScale);
779
780 JLOG(j_.trace()) << "Changes: " //
781 << "account " << to_string(accountBalanceChange) //
782 << ", vault " << to_string(vaultBalanceChange) //
783 << ", broker " << to_string(brokerBalanceChange) //
784 << ", total " << to_string(totalBalanceChangeRounded) << " ("
785 << Number(totalBalanceChange) << ")";
786
787 bool const goodRounding = totalBalanceBeforeRounded == totalBalanceAfterRounded ||
788 totalBalanceChangeRounded == beast::kZero;
789 if (totalBalanceBeforeRounded != totalBalanceAfterRounded)
790 {
791 JLOG((goodRounding ? j_.debug() : j_.warn()))
792 << "Total rounded balances don't match"
793 << (totalBalanceChangeRounded == beast::kZero ? ", but total changes do" : "");
794 }
795 if (totalBalanceChangeRounded != beast::kZero)
796 {
797 JLOG((goodRounding ? j_.debug() : j_.warn()))
798 << "Total balance changes don't match"
799 << (totalBalanceBeforeRounded == totalBalanceAfterRounded ? ", but total balances do"
800 : "");
801 }
802
803 // Rounding for IOUs can be weird, so check a few different ways to show
804 // that funds are conserved.
805 XRPL_ASSERT_PARTS(
806 goodRounding, "xrpl::LoanPay::doApply", "funds are conserved (with rounding)");
807
808 XRPL_ASSERT_PARTS(
809 accountBalanceAfter < accountBalanceBefore || accountID_ == asset.getIssuer(),
810 "xrpl::LoanPay::doApply",
811 "account balance decreased");
812 XRPL_ASSERT_PARTS(
813 vaultBalanceAfter >= beast::kZero && brokerBalanceAfter >= beast::kZero,
814 "xrpl::LoanPay::doApply",
815 "positive vault and broker balances");
816 XRPL_ASSERT_PARTS(
817 vaultBalanceAfter >= vaultBalanceBefore,
818 "xrpl::LoanPay::doApply",
819 "vault balance did not decrease");
820 XRPL_ASSERT_PARTS(
821 brokerBalanceAfter >= brokerBalanceBefore,
822 "xrpl::LoanPay::doApply",
823 "broker balance did not decrease");
824 XRPL_ASSERT_PARTS(
825 vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore,
826 "xrpl::LoanPay::doApply",
827 "vault and/or broker balance increased");
828
829 return tesSUCCESS;
830}
831
832void
834{
835 // No transaction-specific invariants yet (future work).
836}
837
838bool
840{
841 // No transaction-specific invariants yet (future work).
842 return true;
843}
844
845//------------------------------------------------------------------------------
846
847} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:38
Stream fatal() const
Definition Journal.h:321
Stream warn() const
Definition Journal.h:309
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:839
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:833
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:306
static RoundingMode setround(RoundingMode inMode)
Definition Number.cpp:111
static RoundingMode getround()
Definition Number.cpp:105
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.
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
std::shared_ptr< STLedgerEntry const > const & const_ref
bool isFlag(std::uint32_t) const
Definition STObject.cpp:501
std::uint32_t getFlags() const
Definition STObject.cpp:507
beast::Journal const j_
Definition Transactor.h:118
ApplyView & view()
Definition Transactor.h:136
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
AccountID const accountID_
Definition Transactor.h:120
ApplyContext & ctx_
Definition Transactor.h:116
T empty(T... args)
T max(T... args)
T min(T... args)
T minmax_element(T... args)
Keylet loanBroker(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:557
Keylet loan(uint256 const &loanBrokerID, std::uint32_t loanSeq) noexcept
Definition Indexes.cpp:563
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:551
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
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)
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:111
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp)
Determines whether the given expiration time has passed.
Definition View.cpp:47
int getAssetsTotalScale(SLE::const_ref vaultSle)
TER addEmptyHolding(ApplyView &view, AccountID const &accountID, XRPAmount priorBalance, MPTIssue const &mptIssue, beast::Journal journal)
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:779
@ tefBAD_LEDGER
Definition TER.h:160
Number minimumBrokerCover(Number const &debtTotal, TenthBips32 coverRateMinimum, SLE::const_ref vaultSle)
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:439
bool isDeepFrozen(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer)
constexpr FlagValue tfUniversal
Definition TxFlags.h:44
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
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:594
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
Number roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
@ temINVALID
Definition TER.h:96
@ temINVALID_FLAG
Definition TER.h:97
@ temBAD_AMOUNT
Definition TER.h:75
TERSubset< CanCvtToTER > TER
Definition TER.h:634
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:358
@ tecNO_ENTRY
Definition TER.h:304
@ tecINTERNAL
Definition TER.h:308
@ tecINSUFFICIENT_FUNDS
Definition TER.h:323
@ tecPRECISION_LOSS
Definition TER.h:361
@ tecKILLED
Definition TER.h:314
@ tecLIMIT_EXCEEDED
Definition TER.h:359
@ tecNO_PERMISSION
Definition TER.h:303
@ tecDUPLICATE
Definition TER.h:313
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:240
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:61
ReadView const & view
Definition Transactor.h:64
beast::Journal const j
Definition Transactor.h:69
State information when preflighting a tx.
Definition Transactor.h:18
beast::Journal const j
Definition Transactor.h:25