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