xrpld
Loading...
Searching...
No Matches
LoanSet.cpp
1#include <xrpl/tx/transactors/lending/LoanSet.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/core/ServiceRegistry.h>
8#include <xrpl/ledger/ApplyView.h>
9#include <xrpl/ledger/View.h>
10#include <xrpl/ledger/helpers/AccountRootHelpers.h>
11#include <xrpl/ledger/helpers/LendingHelpers.h>
12#include <xrpl/ledger/helpers/SponsorHelpers.h>
13#include <xrpl/ledger/helpers/TokenHelpers.h>
14#include <xrpl/ledger/helpers/VaultHelpers.h>
15#include <xrpl/protocol/AccountID.h>
16#include <xrpl/protocol/Asset.h>
17#include <xrpl/protocol/Feature.h>
18#include <xrpl/protocol/Indexes.h>
19#include <xrpl/protocol/LedgerFormats.h>
20#include <xrpl/protocol/Protocol.h>
21#include <xrpl/protocol/SField.h>
22#include <xrpl/protocol/STLedgerEntry.h>
23#include <xrpl/protocol/STNumber.h>
24#include <xrpl/protocol/STObject.h>
25#include <xrpl/protocol/STTakesAsset.h>
26#include <xrpl/protocol/STTx.h>
27#include <xrpl/protocol/SeqProxy.h>
28#include <xrpl/protocol/TER.h>
29#include <xrpl/protocol/TxFlags.h>
30#include <xrpl/protocol/Units.h>
31#include <xrpl/protocol/XRPAmount.h>
32#include <xrpl/tx/Transactor.h>
33
34#include <cstddef>
35#include <cstdint>
36#include <limits>
37#include <memory>
38#include <optional>
39#include <type_traits>
40#include <vector>
41
42namespace xrpl {
43
44// StartDate is strictly after SubscriptionDate. A min-gap vault must still
45// fit a minimum-interval loan plus kLoanRedemptionBuffer. The interval and
46// buffer constants are independent; only their sum (plus the +1 for a
47// strictly-later StartDate) is required to fit in kMinInvestmentPeriod.
49
50bool
55
58{
59 return tfLoanSetMask;
60}
61
64{
65 using namespace lending;
66
67 auto const& tx = ctx.tx;
68
69 if (tx.isFieldPresent(sfSponsorFlags) && isReserveSponsored(tx))
70 {
71 JLOG(ctx.j.debug()) << "LoanSet: reserve sponsorship is not allowed.";
72 return temINVALID_FLAG;
73 }
74
75 // Special case for Batch inner transactions
76 if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1) &&
77 !tx.isFieldPresent(sfCounterparty))
78 {
79 auto const parentBatchId = ctx.parentBatchId.value_or(UInt256{0});
80 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
81 << "no Counterparty for inner LoanSet transaction.";
82 return temBAD_SIGNER;
83 }
84
85 // These extra hoops are because STObjects cannot be Proxy'd from STObject.
86 auto const counterPartySig = [&tx]() -> std::optional<STObject const> {
87 if (tx.isFieldPresent(sfCounterpartySignature))
88 return tx.getFieldObject(sfCounterpartySignature);
89 return std::nullopt;
90 }();
91 // A proposed LoanSet is stored unsigned; its CounterpartySignature is
92 // collected on-ledger afterward, so its absence here is expected, not an
93 // error (On-Chain Cosigner spec ยง5.3.1.2).
94 if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig && (ctx.flags & TapProposal) == 0)
95 {
96 JLOG(ctx.j.warn()) << "LoanSet transaction must have a CounterpartySignature.";
97 return temBAD_SIGNER;
98 }
99
100 if (counterPartySig)
101 {
102 if (auto const ret = xrpl::detail::preflightCheckSigningKey(*counterPartySig, ctx.j))
103 return ret;
104 }
105
106 if (auto const data = tx[~sfData];
107 data && !data->empty() && !validDataLength(tx[~sfData], kMaxDataPayloadLength))
108 return temINVALID;
109 for (auto const& field : {&sfLoanServiceFee, &sfLatePaymentFee, &sfClosePaymentFee})
110 {
111 if (!validNumericMinimum(tx[~*field]))
112 return temINVALID;
113 }
114 // Principal Requested is required
115 auto const p = tx[sfPrincipalRequested];
116 if (p <= 0)
117 return temINVALID;
118 if (!validNumericRange(tx[~sfLoanOriginationFee], p))
119 return temINVALID;
120 if (!validNumericRange(tx[~sfInterestRate], kMaxInterestRate))
121 return temINVALID;
122 if (!validNumericRange(tx[~sfOverpaymentFee], kMaxOverpaymentFee))
123 return temINVALID;
124 if (!validNumericRange(tx[~sfLateInterestRate], kMaxLateInterestRate))
125 return temINVALID;
126 if (!validNumericRange(tx[~sfCloseInterestRate], kMaxCloseInterestRate))
127 return temINVALID;
128 if (!validNumericRange(tx[~sfOverpaymentInterestRate], kMaxOverpaymentInterestRate))
129 return temINVALID;
130
131 if (auto const paymentTotal = tx[~sfPaymentTotal]; paymentTotal && *paymentTotal <= 0)
132 return temINVALID;
133
134 auto const paymentInterval = tx[~sfPaymentInterval];
136 return temINVALID; // Grace period is between min default value and payment interval
137 if (auto const gracePeriod = tx[~sfGracePeriod]; !validNumericRange(
138 gracePeriod,
139 paymentInterval.value_or(LoanSet::kDefaultPaymentInterval),
141 {
142 return temINVALID;
143 }
144
145 // Copied from preflight2
146 if (counterPartySig)
147 {
148 if (auto const ret =
149 xrpl::detail::preflightCheckSimulateKeys(ctx.flags, *counterPartySig, ctx.j))
150 return *ret;
151 }
152
153 if (auto const brokerID = ctx.tx[~sfLoanBrokerID]; brokerID && *brokerID == beast::kZero)
154 return temINVALID;
155
156 return tesSUCCESS;
157}
158
159NotTEC
161{
162 if (auto ret = Transactor::checkSign(ctx))
163 return ret;
164
165 // Counter signer is optional. If it's not specified, it's assumed to be
166 // `LoanBroker.Owner`. Note that we have not checked whether the
167 // loanbroker exists at this point.
168 auto const counterSigner = [&]() -> std::optional<AccountID> {
169 if (auto const c = ctx.tx.at(~sfCounterparty))
170 return c;
171
172 if (auto const broker = ctx.view.read(keylet::loanBroker(ctx.tx[sfLoanBrokerID])))
173 return broker->at(sfOwner);
174 return std::nullopt;
175 }();
176 if (!counterSigner)
177 return temBAD_SIGNER;
178
179 // Counterparty signature is optional. Presence is checked in preflight.
180 if (!ctx.tx.isFieldPresent(sfCounterpartySignature))
181 return tesSUCCESS;
182 auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature);
184 ctx.view, ctx.flags, ctx.parentBatchId, *counterSigner, counterSig, ctx.j);
185}
186
189{
190 auto const normalCost = Transactor::calculateBaseFee(view, tx);
191
192 // Compute the additional cost of each signature in the
193 // CounterpartySignature, whether a single signature or a multisignature
194 XRPAmount const baseFee = view.fees().base;
195
196 // Counterparty signature is optional, but getFieldObject will return an
197 // empty object if it's not present.
198 auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
199 // Each signer adds one more baseFee to the minimum required fee
200 // for the transaction. Note that unlike the base class, the single signer
201 // is counted if present. It will only be absent in a batch inner
202 // transaction.
203 std::size_t const signerCount = [&counterSig]() -> int {
204 // Compute defensively.
205 // Assure that "tx" cannot be accessed and cause confusion or miscalculations.
206 if (counterSig.isFieldPresent(sfSigners))
207 return counterSig.getFieldArray(sfSigners).size();
208 return counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0;
209 }();
210
211 return normalCost + (signerCount * baseFee);
212}
213
216{
217 static std::vector<OptionaledField<STNumber>> const kValueFields{
218 ~sfPrincipalRequested,
219 ~sfLoanOriginationFee,
220 ~sfLoanServiceFee,
221 ~sfLatePaymentFee,
222 ~sfClosePaymentFee
223 // Overpayment fee is really a rate. Don't check it here.
224 };
225
226 return kValueFields;
227}
228
229static std::uint32_t
231{
232 return view.header().closeTime.time_since_epoch().count();
233}
234
235TER
237{
238 auto const& tx = ctx.tx;
239 auto const interval = ctx.tx.at(~sfPaymentInterval).value_or(kDefaultPaymentInterval);
240 auto const total = ctx.tx.at(~sfPaymentTotal).value_or(kDefaultPaymentTotal);
241
242 {
243 // Check for numeric overflow of the schedule before we load any
244 // objects. The Grace Period for the last payment ends at:
245 // startDate + (paymentInterval * paymentTotal) + gracePeriod.
246 // If that value is larger than "maxTime", the value
247 // overflows, and we kill the transaction.
248 using TimeType = decltype(sfNextPaymentDueDate)::type::value_type;
250 constexpr TimeType kMaxTime = std::numeric_limits<TimeType>::max();
251 static_assert(kMaxTime == 4'294'967'295);
252
253 auto const timeAvailable = kMaxTime - getStartDate(ctx.view);
254 auto const grace = ctx.tx.at(~sfGracePeriod).value_or(kDefaultGracePeriod);
255
256 // The grace period can't be larger than the interval. Check it first,
257 // mostly so that unit tests can test that specific case.
258 if (grace > timeAvailable)
259 {
260 JLOG(ctx.j.warn()) << "Grace period exceeds protocol time limit.";
261 return tecKILLED;
262 }
263
264 if (interval > timeAvailable)
265 {
266 JLOG(ctx.j.warn()) << "Payment interval exceeds protocol time limit.";
267 return tecKILLED;
268 }
269
270 if (total > timeAvailable)
271 {
272 JLOG(ctx.j.warn()) << "Payment total exceeds protocol time limit.";
273 return tecKILLED;
274 }
275
276 auto const timeLastPayment = timeAvailable - grace;
277
278 if (timeLastPayment / interval < total)
279 {
280 JLOG(ctx.j.warn()) << "Last payment due date, or grace period for "
281 "last payment exceeds protocol time limit.";
282 return tecKILLED;
283 }
284 }
285
286 auto const account = tx[sfAccount];
287 auto const brokerID = tx[sfLoanBrokerID];
288
289 auto const brokerSle = ctx.view.read(keylet::loanBroker(brokerID));
290 if (!brokerSle)
291 {
292 // This can only be hit if there's a counterparty specified, otherwise
293 // it'll fail in the signature check
294 JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
295 return tecNO_ENTRY;
296 }
297 auto const brokerOwner = brokerSle->at(sfOwner);
298 auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
299 if (account != brokerOwner && counterparty != brokerOwner)
300 {
301 JLOG(ctx.j.warn()) << "Neither Account nor Counterparty are the owner "
302 "of the LoanBroker.";
303 return tecNO_PERMISSION;
304 }
305 auto const brokerPseudo = brokerSle->at(sfAccount);
306
307 auto const borrower = counterparty == brokerOwner ? account : counterparty;
308 if (auto const borrowerSle = ctx.view.read(keylet::account(borrower)); !borrowerSle)
309 {
310 // It may not be possible to hit this case, because it'll fail the
311 // signature check with terNO_ACCOUNT.
312 JLOG(ctx.j.warn()) << "Borrower does not exist.";
313 return terNO_ACCOUNT;
314 }
315
316 auto const vault = ctx.view.read(keylet::vault(brokerSle->at(sfVaultID)));
317 if (!vault)
318 {
319 // Should be impossible
320 return tefBAD_LEDGER; // LCOV_EXCL_LINE
321 }
322
323 if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
324 {
325 auto const phase = getVaultPhase(ctx.view, vault);
326 if (phase == VaultPhase::Subscription)
327 {
328 JLOG(ctx.j.warn()) << "Vault is still in the subscription phase.";
329 return tecTOO_SOON;
330 }
331 if (phase == VaultPhase::Redemption)
332 {
333 JLOG(ctx.j.warn()) << "Vault has entered the redemption phase.";
334 return tecEXPIRED;
335 }
336 if (phase == VaultPhase::Investment)
337 {
338 auto const finalPayment =
339 std::uint64_t{getStartDate(ctx.view)} + (std::uint64_t{interval} * total);
340 if (finalPayment + kLoanRedemptionBuffer > vault->at(sfRedemptionDate))
341 {
342 JLOG(ctx.j.warn())
343 << "Final loan payment date is fewer than " << kLoanRedemptionBuffer
344 << " seconds before the vault's redemption date.";
345 return tecNO_PERMISSION;
346 }
347 }
348 }
349
350 // Instant interest recognition credits interestDue into AssetsTotal, so a vault
351 // already at AssetsMaximum cannot take another loan. Cash-basis origination
352 // does not change AssetsTotal (see cash_basis::loanOriginationDeltas), so
353 // this leftover instant-recognition gate must not apply there.
354 if (getVaultVersion(vault) != VaultVersion::CashBasis && vault->at(sfAssetsMaximum) != 0 &&
355 vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum))
356 {
357 JLOG(ctx.j.warn()) << "Vault at maximum assets limit. Can't add another loan.";
358 return tecLIMIT_EXCEEDED;
359 }
360
361 Asset const asset = vault->at(sfAsset);
362
363 auto const vaultPseudo = vault->at(sfAccount);
364
365 // Check that relevant values can be represented as the vault asset type.
366 // This check is almost duplicated in doApply, but that check is done after
367 // the overall loan scale is known. This is mostly only relevant for
368 // integral (non-IOU) types
369 for (auto const& field : getValueFields())
370 {
371 if (auto const value = tx[field]; value && STAmount{asset, *value} != *value)
372 {
373 JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
374 << ") can not be represented as a(n) " << to_string(asset) << ".";
375 return tecPRECISION_LOSS;
376 }
377 }
378
379 // canAddHolding is an issuer-level check (DefaultRipple for IOU,
380 // lsfMPTCanTransfer for MPT); neither overload looks at the
381 // destination, so the holdingExists() clauses only decide whether a
382 // create path is reachable at all. It always runs before
383 // fixCleanup3_4_0: IOU addEmptyHolding checks DefaultRipple ahead of
384 // the existing-line case, so only preclaim can turn an existing line
385 // under a cleared DefaultRipple into terNO_RIPPLE rather than
386 // tecINTERNAL. After the amendment an existing line short-circuits to
387 // tecDUPLICATE, which doApply ignores, so run the check only when the
388 // borrower lacks a holding, or the origination fee is nonzero and the
389 // broker owner lacks one.
390 auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
391 if (!ctx.view.rules().enabled(fixCleanup3_4_0) || !holdingExists(ctx.view, borrower, asset) ||
392 (originationFee != beast::kZero && !holdingExists(ctx.view, brokerOwner, asset)))
393 {
394 if (auto const ter = canAddHolding(ctx.view, asset))
395 return ter;
396 }
397
398 // vaultPseudo is going to send funds, so it can't be frozen.
399 if (auto const ret = checkFrozen(ctx.view, vaultPseudo, asset))
400 {
401 JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen.";
402 return ret;
403 }
404
405 // brokerPseudo is the fallback account to receive LoanPay fees, even if the
406 // broker owner is unable to accept them. Don't create the loan if it is
407 // deep frozen.
408 if (auto const ret = checkDeepFrozen(ctx.view, brokerPseudo, asset))
409 {
410 JLOG(ctx.j.warn()) << "Broker pseudo-account is frozen.";
411 return ret;
412 }
413
414 // borrower is eventually going to have to pay back the loan, so it can't be
415 // frozen now. It is also going to receive funds, so it can't be deep
416 // frozen, but being frozen is a prerequisite for being deep frozen, so
417 // checking the one is sufficient.
418 if (auto const ret = checkFrozen(ctx.view, borrower, asset))
419 {
420 JLOG(ctx.j.warn()) << "Borrower account is frozen.";
421 return ret;
422 }
423 // brokerOwner is going to receive funds if there's an origination fee, so
424 // it can't be deep frozen
425 if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset))
426 {
427 JLOG(ctx.j.warn()) << "Broker owner account is frozen.";
428 return ret;
429 }
430
431 return tesSUCCESS;
432}
433
434TER
436{
437 auto const& tx = ctx_.tx;
438 auto& view = ctx_.view();
439
440 auto const brokerID = tx[sfLoanBrokerID];
441
442 auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
443 if (!brokerSle)
444 return tefBAD_LEDGER; // LCOV_EXCL_LINE
445 auto const brokerOwner = brokerSle->at(sfOwner);
446 auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner));
447 if (!brokerOwnerSle)
448 return tefBAD_LEDGER; // LCOV_EXCL_LINE
449
450 auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID)));
451 if (!vaultSle)
452 return tefBAD_LEDGER; // LCOV_EXCL_LINE
453 auto const vaultPseudo = vaultSle->at(sfAccount);
454 Asset const vaultAsset = vaultSle->at(sfAsset);
455
456 auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
457 auto const borrower = counterparty == brokerOwner ? accountID_ : counterparty;
458 auto const borrowerSle = view.peek(keylet::account(borrower));
459 if (!borrowerSle)
460 {
461 return tefBAD_LEDGER; // LCOV_EXCL_LINE
462 }
463
464 auto const brokerPseudo = brokerSle->at(sfAccount);
465 auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo));
466 if (!brokerPseudoSle)
467 {
468 return tefBAD_LEDGER; // LCOV_EXCL_LINE
469 }
470 auto const principalRequested = tx[sfPrincipalRequested];
471
472 auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
473 auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
474 auto const vaultScale = getAssetsTotalScale(vaultSle);
475 if (vaultAvailableProxy < principalRequested)
476 {
477 JLOG(j_.warn()) << "Insufficient assets available in the Vault to fund the loan.";
479 }
480
481 TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
482
483 auto const paymentInterval = tx[~sfPaymentInterval].value_or(kDefaultPaymentInterval);
484 auto const paymentTotal = tx[~sfPaymentTotal].value_or(kDefaultPaymentTotal);
485
486 auto const properties = computeLoanProperties(
487 view.rules(),
488 vaultAsset,
489 principalRequested,
490 interestRate,
491 paymentInterval,
492 paymentTotal,
493 TenthBips16{brokerSle->at(sfManagementFeeRate)},
494 vaultScale);
495
496 LoanState const state = constructLoanState(
497 properties.loanState.valueOutstanding,
498 principalRequested,
499 properties.loanState.managementFeeDue);
500
501 XRPL_ASSERT_PARTS(
502 *vaultSle->at(sfAssetsMaximum) == 0 ||
504 *vaultSle->at(sfAssetsMaximum) > *vaultTotalProxy,
505 "xrpl::LoanSet::doApply",
506 "instant-recognition vault is below maximum limit");
507
508 if (loanOriginationExceedsVaultMaximum(vaultSle, vaultTotalProxy, state.interestDue))
509 {
510 JLOG(j_.warn()) << "Loan would exceed the maximum assets of the vault";
511 return tecLIMIT_EXCEEDED;
512 }
513 // Check that relevant values won't lose precision. This is mostly only
514 // relevant for IOU assets.
515 for (auto const& field : getValueFields())
516 {
517 if (auto const value = tx[field];
518 value && !isRounded(vaultAsset, *value, properties.loanScale))
519 {
520 JLOG(j_.warn()) << field.f->getName() << " (" << *value
521 << ") has too much precision. Total loan value is "
522 << properties.loanState.valueOutstanding << " with a scale of "
523 << properties.loanScale;
524 return tecPRECISION_LOSS;
525 }
526 }
527
528 if (auto const ret = checkLoanGuards(
529 vaultAsset,
530 principalRequested,
531 interestRate != beast::kZero,
532 paymentTotal,
533 properties,
534 j_))
535 return ret;
536
537 // Check that the other computed values are valid
538 if (properties.loanState.managementFeeDue < 0 || properties.loanState.valueOutstanding <= 0 ||
539 properties.periodicPayment <= 0)
540 {
541 // LCOV_EXCL_START
542 JLOG(j_.warn()) << "Computed loan properties are invalid. Does not compute."
543 << " Management fee: " << properties.loanState.managementFeeDue
544 << ". Total Value: " << properties.loanState.valueOutstanding
545 << ". PeriodicPayment: " << properties.periodicPayment;
546 return tecINTERNAL;
547 // LCOV_EXCL_STOP
548 }
549
550 auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
551
552 auto const loanAssetsToBorrower = principalRequested - originationFee;
553
554 auto const [assetsTotalDelta, debtTotalDelta] =
555 loanOriginationDeltas(vaultSle, principalRequested, state.interestDue);
556 auto const newDebtTotal = brokerSle->at(sfDebtTotal) + debtTotalDelta;
557 if (auto const debtMaximum = brokerSle->at(sfDebtMaximum);
558 debtMaximum != 0 && debtMaximum < newDebtTotal)
559 {
560 JLOG(j_.warn()) << "Loan would exceed the maximum debt limit of the LoanBroker.";
561 return tecLIMIT_EXCEEDED;
562 }
563 TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
564 {
565 auto const minCover = [&]() {
566 if (ctx_.view().rules().enabled(fixCleanup3_2_0))
567 {
568 return minimumBrokerCover(newDebtTotal, coverRateMinimum, vaultSle);
569 }
570
571 // Round the minimum required cover up to be conservative. This ensures
572 // CoverAvailable never drops below the theoretical minimum, protecting
573 // the broker's solvency.
575 return tenthBipsOfValue(newDebtTotal, coverRateMinimum);
576 }();
577 if (brokerSle->at(sfCoverAvailable) < minCover)
578 {
579 JLOG(j_.warn()) << "Insufficient first-loss capital to cover the loan.";
581 }
582 }
583
584 increaseOwnerCount(view, borrowerSle, {}, 1, j_);
585
586 {
587 auto const balance =
588 accountID_ == borrower ? preFeeBalance_ : borrowerSle->at(sfBalance).value().xrp();
589 if (balance < accountReserve(view, borrowerSle, j_))
591 }
592
593 // Account for the origination fee using two payments
594 //
595 // 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
596 // from vault pseudo-account to the borrower.
597 // Create a holding for the borrower if one does not already exist.
598
599 XRPL_ASSERT_PARTS(
600 borrower == accountID_ || borrower == counterparty,
601 "xrpl::LoanSet::doApply",
602 "borrower signed transaction");
603 auto applyViewContext = ctx_.getApplyViewContext();
604 if (auto const ter = addEmptyHolding(
605 applyViewContext, borrower, borrowerSle->at(sfBalance).value().xrp(), vaultAsset, j_);
606 ter && ter != tecDUPLICATE)
607 {
608 // ignore tecDUPLICATE. That means the holding already exists, and
609 // is fine here
610 return ter;
611 }
612
613 if (auto const ter = requireAuth(view, vaultAsset, borrower, AuthType::StrongAuth))
614 return ter;
615
616 // 2. Transfer originationFee, if any, from vault pseudo-account to
617 // LoanBroker owner.
618 if (originationFee != beast::kZero)
619 {
620 // Create the holding if it doesn't already exist (necessary for MPTs).
621 // The owner may have deleted their MPT / line at some point.
622 XRPL_ASSERT_PARTS(
623 brokerOwner == accountID_ || brokerOwner == counterparty,
624 "xrpl::LoanSet::doApply",
625 "broker owner signed transaction");
626
627 if (auto const ter = addEmptyHolding(
628 applyViewContext,
629 brokerOwner,
630 brokerOwnerSle->at(sfBalance).value().xrp(),
631 vaultAsset,
632 j_);
633 ter && ter != tecDUPLICATE)
634 {
635 // ignore tecDUPLICATE. That means the holding already exists,
636 // and is fine here
637 return ter;
638 }
639 }
640
641 if (auto const ter = requireAuth(view, vaultAsset, brokerOwner, AuthType::StrongAuth))
642 return ter;
643
644 if (auto const ter = accountSendMulti(
645 view,
646 vaultPseudo,
647 vaultAsset,
648 {{borrower, loanAssetsToBorrower}, {brokerOwner, originationFee}},
649 j_,
651 return ter;
652
653 // Get shortcuts to the loan property values
654 auto const startDate = getStartDate(view);
655 auto loanSequenceProxy = brokerSle->at(sfLoanSequence);
656
657 // Create the loan
658 auto loan =
659 std::make_shared<SLE>(keylet::loan(brokerID, SeqProxy::rawSequence(*loanSequenceProxy)));
660
661 // Prevent copy/paste errors
662 auto setLoanField = [&loan, &tx](auto const& field, std::uint32_t const defValue = 0) {
663 // at() is smart enough to unseat a default field set to the default
664 // value
665 loan->at(field) = tx[field].value_or(defValue);
666 };
667
668 // Set required and fixed tx fields
669 loan->at(sfLoanScale) = properties.loanScale;
670 loan->at(sfStartDate) = startDate;
671 loan->at(sfPaymentInterval) = paymentInterval;
672 loan->at(sfLoanSequence) = *loanSequenceProxy;
673 loan->at(sfLoanBrokerID) = brokerID;
674 loan->at(sfBorrower) = borrower;
675 // Set all other transaction fields directly from the transaction
676 if (tx.isFlag(tfLoanOverpayment))
677 loan->setFlag(lsfLoanOverpayment);
678 setLoanField(~sfLoanOriginationFee);
679 setLoanField(~sfLoanServiceFee);
680 setLoanField(~sfLatePaymentFee);
681 setLoanField(~sfClosePaymentFee);
682 setLoanField(~sfOverpaymentFee);
683 setLoanField(~sfInterestRate);
684 setLoanField(~sfLateInterestRate);
685 setLoanField(~sfCloseInterestRate);
686 setLoanField(~sfOverpaymentInterestRate);
687 setLoanField(~sfGracePeriod, kDefaultGracePeriod);
688 // Set dynamic / computed fields to their initial values
689 loan->at(sfPrincipalOutstanding) = principalRequested;
690 loan->at(sfPeriodicPayment) = properties.periodicPayment;
691 loan->at(sfTotalValueOutstanding) = properties.loanState.valueOutstanding;
692 loan->at(sfManagementFeeOutstanding) = properties.loanState.managementFeeDue;
693 loan->at(sfPreviousPaymentDueDate) = 0;
694 loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
695 loan->at(sfPaymentRemaining) = paymentTotal;
696 view.insert(loan);
697
698 // Update the balances in the vault
699 vaultAvailableProxy -= principalRequested;
700 vaultTotalProxy += assetsTotalDelta;
701 XRPL_ASSERT_PARTS(
702 *vaultAvailableProxy <= *vaultTotalProxy,
703 "xrpl::LoanSet::doApply",
704 "assets available must not be greater than assets outstanding");
705 view.update(vaultSle);
706
707 // Update the balances in the loan broker
708 adjustImpreciseNumber(brokerSle->at(sfDebtTotal), debtTotalDelta, vaultAsset, vaultScale);
709 adjustLoanBrokerOwnerCount(view, brokerSle, 1, j_);
710 loanSequenceProxy += 1;
711 // The sequence should be extremely unlikely to roll over, but fail if it
712 // does
713 if (loanSequenceProxy == 0)
715 view.update(brokerSle);
716
717 // Put the loan into the pseudo-account's directory
718 if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode))
719 return ter;
720 // Borrower is the owner of the loan
721 if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode))
722 return ter;
723
724 associateAsset(*vaultSle, vaultAsset);
725 associateAsset(*brokerSle, vaultAsset);
726 associateAsset(*loan, vaultAsset);
727
728 return tesSUCCESS;
729}
730
731void
733{
734 // No transaction-specific invariants yet (future work).
735}
736
737bool
739{
740 // No transaction-specific invariants yet (future work).
741 return true;
742}
743
744//------------------------------------------------------------------------------
745
746} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
Stream debug() const
Definition Journal.h:344
Stream warn() const
Definition Journal.h:356
void visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override
Inspect a single ledger entry modified by this transaction.
Definition LoanSet.cpp:732
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
Definition LoanSet.cpp:57
static TER preclaim(PreclaimContext const &ctx)
Definition LoanSet.cpp:236
static XRPAmount calculateBaseFee(ReadView const &view, STTx const &tx)
Definition LoanSet.cpp:188
static constexpr std::uint32_t kDefaultPaymentTotal
static std::vector< OptionaledField< STNumber > > const & getValueFields()
Definition LoanSet.cpp:215
static NotTEC preflight(PreflightContext const &ctx)
Definition LoanSet.cpp:63
static NotTEC checkSign(PreclaimContext const &ctx)
Definition LoanSet.cpp:160
static constexpr std::uint32_t kDefaultPaymentInterval
static constexpr std::uint32_t kMinPaymentInterval
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 LoanSet.cpp:738
static bool checkExtraFeatures(PreflightContext const &ctx)
Definition LoanSet.cpp:51
TER doApply() override
Definition LoanSet.cpp:435
static constexpr std::uint32_t kDefaultGracePeriod
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
A view into a ledger.
Definition ReadView.h:41
virtual Rules const & rules() const =0
Returns the tx processing rules.
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
virtual LedgerHeader const & header() const =0
Returns information about the ledger.
bool enabled(UInt256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:182
std::shared_ptr< STLedgerEntry const > const & ConstRef
T::value_type at(TypedField< T > const &f) const
Get the value of a field.
Definition STObject.h:1078
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:464
STObject getFieldObject(SField const &field) const
Definition STObject.cpp:678
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
static NotTEC checkSign(PreclaimContext const &ctx)
static bool validNumericMinimum(std::optional< T > value, T min=T{})
Minimum will usually be zero.
Definition Transactor.h:696
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
XRPAmount preFeeBalance_
Definition Transactor.h:167
static bool validDataLength(std::optional< Slice > const &slice, std::size_t maxLength)
ApplyContext & ctx_
Definition Transactor.h:162
static bool validNumericRange(std::optional< T > value, T max, T min=T{})
Definition Transactor.h:677
T is_same_v
T make_shared(T... args)
T max(T... args)
constexpr Zero kZero
Definition Zero.h:30
std::optional< NotTEC > preflightCheckSimulateKeys(ApplyFlags flags, STObject const &sigObject, beast::Journal j)
Checks the special signing key state needed for simulation.
NotTEC preflightCheckSigningKey(STObject const &sigObject, beast::Journal j)
Checks the validity of the transactor signing key.
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)
@ terNO_ACCOUNT
Definition TER.h:218
constexpr FlagValue tfInnerBatchTxn
Definition TxFlags.h:44
bool loanOriginationExceedsVaultMaximum(SLE::ConstRef vaultSle, Number const &vaultTotal, Number const &interestDue)
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)
@ 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)
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
void increaseOwnerCount(ApplyView &view, SLE::Ref accountSle, SLE::Ref sponsorSle, std::uint32_t count, beast::Journal j)
Increase owner-count fields when the caller supplies the sponsor.
bool isReserveSponsored(STTx const &tx)
Whether the transaction's reserve is sponsored (sfSponsor present + spfSponsorReserve set).
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
constexpr std::size_t kMaxDataPayloadLength
The maximum length of Data payload.
Definition Protocol.h:303
TER checkLoanGuards(Asset const &vaultAsset, Number const &principalRequested, bool expectInterest, std::uint32_t paymentTotal, LoanProperties const &properties, beast::Journal j)
BaseUInt< 256 > UInt256
Definition base_uint.h:580
Number minimumBrokerCover(Number const &debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
TER canAddHolding(ReadView const &view, MPTIssue const &mptIssue)
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:614
TER dirLink(ApplyView &view, AccountID const &owner, SLE::pointer &object, SF_UINT64 const &node=sfOwnerNode)
Definition View.cpp:384
void adjustLoanBrokerOwnerCount(ApplyView &view, SLE::Ref brokerSle, std::int32_t delta, beast::Journal j)
Adjust a LoanBroker's owner count.
static std::uint32_t getStartDate(ReadView const &view)
Definition LoanSet.cpp:230
VaultPhase getVaultPhase(ReadView const &view, SLE::ConstRef vault)
Returns the current lifecycle phase of a vault.
@ TapProposal
Definition ApplyView.h:53
@ temINVALID
Definition TER.h:98
@ temINVALID_FLAG
Definition TER.h:99
@ temBAD_SIGNER
Definition TER.h:103
TERSubset< CanCvtToTER > TER
Definition TER.h:654
XRPAmount accountReserve(ReadView const &view, SLE::ConstRef sle, beast::Journal j, Adjustment adj={})
Returns the account reserve, in drops.
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.
@ tecNO_ENTRY
Definition TER.h:314
@ tecINTERNAL
Definition TER.h:318
@ tecTOO_SOON
Definition TER.h:326
@ tecINSUFFICIENT_FUNDS
Definition TER.h:333
@ tecEXPIRED
Definition TER.h:322
@ tecPRECISION_LOSS
Definition TER.h:371
@ tecINSUFFICIENT_RESERVE
Definition TER.h:315
@ tecKILLED
Definition TER.h:324
@ tecMAX_SEQUENCE_REACHED
Definition TER.h:328
@ 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!...
LoanProperties computeLoanProperties(Rules const &rules, Asset const &asset, Number const &principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate, std::int32_t minimumScale)
constexpr std::uint32_t kLoanRedemptionBuffer
Minimum gap between a closed-ended loan's final scheduled payment and the vault's RedemptionDate.
Definition Protocol.h:357
VaultVersion getVaultVersion(SLE::ConstRef vault)
Resolves a Vault's LEVersion, the single point every accounting touch point should call to determine ...
AccountingDeltas loanOriginationDeltas(SLE::ConstRef vaultSle, Number const &principalRequested, Number const &interestDue)
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
constexpr std::uint32_t kMinInvestmentPeriod
Bounds on the length of a closed-ended vault's Investment phase (RedemptionDate - SubscriptionDate).
Definition Protocol.h:369
@ tesSUCCESS
Definition TER.h:250
bool holdingExists(ReadView const &view, AccountID const &account, Asset const &asset)
True if the account already holds this asset (or is the issuer / XRP).
bool checkLendingProtocolDependencies(Rules const &rules, STTx const &tx)
bool isRounded(Asset const &asset, Number const &value, std::int32_t scale)
NetClock::time_point closeTime
This structure captures the parts of a loan state.
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:92
ReadView const & view
Definition Transactor.h:95
std::optional< UInt256 const > const parentBatchId
Definition Transactor.h:99
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
std::optional< UInt256 const > parentBatchId
Definition Transactor.h:45
T time_since_epoch(T... args)