xrpld
Loading...
Searching...
No Matches
LoanCashBasis_test.cpp
1#include <test/app/lending/LoanTestBase.h>
2#include <test/jtx/Account.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/fee.h>
7#include <test/jtx/ter.h>
8#include <test/jtx/vault.h>
9
10#include <xrpl/basics/Number.h>
11#include <xrpl/basics/chrono.h>
12#include <xrpl/beast/unit_test/suite.h>
13#include <xrpl/beast/utility/Zero.h>
14#include <xrpl/ledger/helpers/LendingHelpers.h>
15#include <xrpl/ledger/helpers/VaultHelpers.h>
16#include <xrpl/protocol/Feature.h>
17#include <xrpl/protocol/Indexes.h>
18#include <xrpl/protocol/Issue.h>
19#include <xrpl/protocol/Protocol.h>
20#include <xrpl/protocol/SField.h>
21#include <xrpl/protocol/SeqProxy.h>
22#include <xrpl/protocol/TER.h>
23#include <xrpl/protocol/TxFlags.h>
24
25#include <chrono>
26#include <cstdint>
27#include <functional>
28#include <tuple>
29
30namespace xrpl::test {
31
32// LendingProtocolV1_1 ("cash-basis" accounting) dedicated coverage.
33//
34// Existing tests never enable featureLendingProtocolV1_1 (see `all_`
35// above), so these are the only tests in this file that exercise the
36// amendment. They are called once, directly, from
37// runAmendmentIndependent() -- not looped through
38// runAmendmentSensitive()/amendmentCombinations(), since doing so would
39// require re-deriving whole-life-specific expected values for ~15
40// unrelated regression tests.
42{
43private:
44 // 1. LoanSet origination: Vault.AssetsTotal/LoanBroker.DebtTotal deltas,
45 // and the AssetsMaximum/DebtMaximum guards (which always check against
46 // principal + interestDue, regardless of the amendment).
47 void
49 {
50 testcase("cash-basis: LoanSet origination");
51
52 using namespace jtx;
53 using namespace loan;
54
55 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
56 BrokerParameters const brokerParams{
57 .vaultDeposit = 100'000,
58 .debtMax = 0,
59 .coverRateMin = TenthBips32{0},
60 .coverDeposit = 0,
61 .managementFeeRate = TenthBips16{0},
62 .coverRateLiquidation = TenthBips32{0}};
63
64 Number const principalRequest{10'000};
65 TenthBips32 const interestRate{percentageToTenthBips(10)};
66 std::uint32_t const paymentTotal = 2;
67 std::uint32_t const paymentInterval = 86400;
68
69 // Creates a broker/vault, submits a single LoanSet with a nonzero
70 // interest rate, and returns the observed Vault.AssetsTotal /
71 // LoanBroker.DebtTotal deltas plus the loan's own computed
72 // interestDue and principalOutstanding.
73 auto runOrigination = [&](FeatureBitset features) {
74 Env env(*this, features);
75
76 Account const lender{"lender"};
77 Account const borrower{"borrower"};
78 env.fund(XRP(1'000'000), lender, borrower);
79 env.close();
80
81 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
82
83 auto const vaultBefore = env.le(broker.vaultKeylet());
84 auto const brokerBefore = env.le(broker.brokerKeylet());
85 BEAST_EXPECT(vaultBefore && brokerBefore);
86 Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
87 Number const debtTotalBefore = brokerBefore->at(sfDebtTotal);
88
89 auto const loanSequence = brokerBefore->at(sfLoanSequence);
90 auto const loanKeylet =
91 keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
92
93 env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()),
94 kCounterparty(lender),
95 kInterestRate(interestRate),
96 kPaymentTotal(paymentTotal),
97 kPaymentInterval(paymentInterval),
98 Sig(sfCounterpartySignature, lender),
99 Fee(env.current()->fees().base * 2),
100 Ter(tesSUCCESS));
101 env.close();
102
103 auto const loanSle = env.le(loanKeylet);
104 BEAST_EXPECT(loanSle);
105 Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
106 Number const totalValueOutstanding = loanSle->at(sfTotalValueOutstanding);
107 Number const interestDue = totalValueOutstanding - principalOutstanding;
108 BEAST_EXPECT(interestDue > beast::kZero);
109 BEAST_EXPECT(principalOutstanding == xrpAsset(principalRequest).value());
110
111 auto const vaultAfter = env.le(broker.vaultKeylet());
112 auto const brokerAfter = env.le(broker.brokerKeylet());
113 BEAST_EXPECT(vaultAfter && brokerAfter);
114 Number const assetsTotalDelta =
115 Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore;
116 Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore;
117
118 return std::make_tuple(
119 assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding);
120 };
121
122 Number interestDueCash{};
123 Number principalOutstandingCash{};
124 {
125 auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] =
126 runOrigination(all_ | featureLendingProtocolV1_1);
127 interestDueCash = interestDue;
128 principalOutstandingCash = principalOutstanding;
129
130 BEAST_EXPECTS(
131 assetsTotalDelta == beast::kZero,
132 "cash-basis origination must not change AssetsTotal; delta=" +
133 to_string(assetsTotalDelta));
134 BEAST_EXPECTS(
135 debtTotalDelta == principalOutstanding,
136 "cash-basis origination must add principal-only to DebtTotal; delta=" +
137 to_string(debtTotalDelta) + " principal=" + to_string(principalOutstanding));
138 }
139
140 {
141 auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] =
142 runOrigination(all_);
143
144 BEAST_EXPECTS(
145 assetsTotalDelta == interestDue,
146 "whole-life origination must add interestDue to AssetsTotal; delta=" +
147 to_string(assetsTotalDelta) + " interestDue=" + to_string(interestDue));
148 BEAST_EXPECTS(
149 debtTotalDelta == principalOutstanding + interestDue,
150 "whole-life origination must add principal+interest to DebtTotal; delta=" +
151 to_string(debtTotalDelta));
152 }
153
154 // AssetsMaximum guard checks interestDue headroom only under
155 // whole-life accounting; DebtMaximum guard also varies by model.
156 auto runVaultGuard = [&](FeatureBitset features, Number const& slack, TER expected) {
157 Env env(*this, features);
158
159 Account const lender{"lender"};
160 Account const borrower{"borrower"};
161 env.fund(XRP(1'000'000), lender, borrower);
162 env.close();
163
164 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
165
166 auto const vaultSle = env.le(broker.vaultKeylet());
167 BEAST_EXPECT(vaultSle);
168 Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal);
169
170 Vault const vault{env};
171 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
172 tx[sfAssetsMaximum] = assetsTotalBefore + slack;
173 env(tx);
174 env.close();
175
176 env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()),
177 kCounterparty(lender),
178 kInterestRate(interestRate),
179 kPaymentTotal(paymentTotal),
180 kPaymentInterval(paymentInterval),
181 Sig(sfCounterpartySignature, lender),
182 Fee(env.current()->fees().base * 2),
183 Ter(expected));
184 env.close();
185 };
186
187 auto runBrokerGuard = [&](FeatureBitset features, Number const& debtMaximum, TER expected) {
188 Env env(*this, features);
189
190 Account const lender{"lender"};
191 Account const borrower{"borrower"};
192 env.fund(XRP(1'000'000), lender, borrower);
193 env.close();
194
195 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
196
197 env(loan_broker::set(lender, broker.vaultID),
199 loan_broker::kDebtMaximum(debtMaximum),
200 Fee(env.current()->fees().base * 2));
201 env.close();
202
203 env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()),
204 kCounterparty(lender),
205 kInterestRate(interestRate),
206 kPaymentTotal(paymentTotal),
207 kPaymentInterval(paymentInterval),
208 Sig(sfCounterpartySignature, lender),
209 Fee(env.current()->fees().base * 2),
210 Ter(expected));
211 env.close();
212 };
213
214 Number const oneDrop = xrpAsset(1).value();
215 {
216 testcase("whole-life: LoanSet AssetsMaximum guard checks interestDue headroom");
217 // Guard rejects when there's not quite enough headroom for the
218 // interest.
219 runVaultGuard(all_, interestDueCash - oneDrop, tecLIMIT_EXCEEDED);
220 // Guard accepts at the exact boundary.
221 runVaultGuard(all_, interestDueCash, tesSUCCESS);
222 }
223
224 {
225 testcase("cash-basis: LoanSet AssetsMaximum guard ignores interestDue headroom");
226 // Even far less headroom than interestDue still succeeds, since
227 // cash-basis origination never adds interest to AssetsTotal.
228 runVaultGuard(all_ | featureLendingProtocolV1_1, oneDrop, tesSUCCESS);
229 }
230
231 // DebtMaximum guard: cash-basis projects principal-only DebtTotal;
232 // whole-life projects principal + interestDue.
233 for (auto const cashBasis : {true, false})
234 {
235 testcase(
236 std::string("LoanSet DebtMaximum guard (") +
237 (cashBasis ? "cash-basis)" : "whole-life)"));
238 auto const features = cashBasis ? all_ | featureLendingProtocolV1_1 : all_;
239 Number const newDebtTotal =
240 principalOutstandingCash + (cashBasis ? Number{} : interestDueCash);
241 runBrokerGuard(features, newDebtTotal - oneDrop, tecLIMIT_EXCEEDED);
242 runBrokerGuard(features, newDebtTotal, tesSUCCESS);
243 }
244 }
245
246 // 2. LoanPay: regular, late, overpayment, and full-payment types.
247 // Assert Vault.AssetsTotal/LoanBroker.DebtTotal deltas match
248 // interestPaid/principalPaid under cash-basis, and cross-check the
249 // amendment-disabled run's deltas against the documented whole-life
250 // formula (AssetsTotal += valueChange; DebtTotal mirrors the loan's own
251 // TotalValueOutstanding delta exactly, since whole-life debt recognition
252 // tracks total loan value).
253 void
255 {
256 using namespace jtx;
257 using namespace loan;
258 using namespace std::chrono_literals;
259 using tp = NetClock::time_point;
260
261 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
262 BrokerParameters const brokerParams{
263 .vaultDeposit = 1'000'000,
264 .debtMax = 0,
265 .coverRateMin = TenthBips32{0},
266 .coverDeposit = 0,
267 .managementFeeRate = TenthBips16{0},
268 .coverRateLiquidation = TenthBips32{0}};
269
270 Number const principalRequest{12'000};
271 TenthBips32 const interestRate{percentageToTenthBips(12)};
272 std::uint32_t const paymentTotal = 4;
273 std::uint32_t const paymentInterval = 600;
274 std::uint32_t const gracePeriod = 300;
275
276 struct PaymentDeltas
277 {
278 Number principalPaid;
279 Number assetsTotalDelta;
280 Number debtTotalDelta;
281 Number totalValueDelta;
282 };
283
284 // Sets up a fresh broker + loan, advances time, submits a single
285 // payment of the given type/amount, and returns the observed deltas.
286 auto runPayment = [&](FeatureBitset features,
287 std::uint32_t loanSetFlags,
288 std::uint32_t payFlags,
289 std::function<void(Env&, tp const&)> const& advanceTime,
290 std::function<STAmount(LoanState const&)> const& paymentAmount) {
291 Env env(*this, features);
292
293 Account const lender{"lender"};
294 Account const borrower{"borrower"};
295 env.fund(XRP(10'000'000), lender, borrower);
296 env.close();
297
298 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
299
300 LoanParameters const loanParams{
301 .account = borrower,
302 .counter = lender,
303 .principalRequest = principalRequest,
304 .interest = interestRate,
305 .payTotal = paymentTotal,
306 .payInterval = paymentInterval,
307 .gracePd = gracePeriod,
308 .flags = loanSetFlags,
309 };
310
311 auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
312 BEAST_EXPECT(brokerBeforeLoan);
313 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
314 auto const loanKeylet =
315 keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
316
317 env(loanParams(env, broker));
318 env.close();
319
320 LoanState const state = getCurrentState(env, broker, loanKeylet);
321
322 advanceTime(env, state.startDate);
323
324 auto const vaultBefore = env.le(broker.vaultKeylet());
325 auto const brokerBefore = env.le(broker.brokerKeylet());
326 auto const loanBefore = env.le(loanKeylet);
327 BEAST_EXPECT(vaultBefore && brokerBefore && loanBefore);
328
329 Number const principalBefore = loanBefore->at(sfPrincipalOutstanding);
330 Number const totalValueBefore = loanBefore->at(sfTotalValueOutstanding);
331 Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
332 Number const debtTotalBefore = brokerBefore->at(sfDebtTotal);
333
334 STAmount const amount = paymentAmount(state);
335 env(pay(borrower, loanKeylet.key, amount, payFlags), Ter(tesSUCCESS));
336 env.close();
337
338 auto const vaultAfter = env.le(broker.vaultKeylet());
339 auto const brokerAfter = env.le(broker.brokerKeylet());
340 auto const loanAfter = env.le(loanKeylet);
341 BEAST_EXPECT(vaultAfter && brokerAfter && loanAfter);
342
343 Number const principalAfter = loanAfter->at(sfPrincipalOutstanding);
344 Number const totalValueAfter = loanAfter->at(sfTotalValueOutstanding);
345 Number const assetsTotalAfter = vaultAfter->at(sfAssetsTotal);
346 Number const debtTotalAfter = brokerAfter->at(sfDebtTotal);
347
348 return PaymentDeltas{
349 .principalPaid = principalBefore - principalAfter,
350 .assetsTotalDelta = assetsTotalAfter - assetsTotalBefore,
351 .debtTotalDelta = debtTotalAfter - debtTotalBefore,
352 .totalValueDelta = totalValueAfter - totalValueBefore};
353 };
354
355 // Compares the disabled (whole-life) and enabled (cash-basis) runs
356 // of the same payment scenario, and asserts the documented
357 // relationships between them.
358 auto checkScenario = [&](std::string const& label,
359 PaymentDeltas const& off,
360 PaymentDeltas const& on) {
361 testcase("cash-basis: LoanPay " + label);
362
363 // The loan's own PrincipalOutstanding field is untouched by
364 // the amendment.
365 BEAST_EXPECTS(
366 off.principalPaid == on.principalPaid,
367 "principalPaid must be amendment-independent; off=" + to_string(off.principalPaid) +
368 " on=" + to_string(on.principalPaid));
369
370 // Whole-life structural invariant: DebtTotal (which
371 // recognizes a loan's full remaining value as debt) must
372 // change exactly as the loan's own TotalValueOutstanding
373 // does.
374 BEAST_EXPECTS(
375 off.debtTotalDelta == off.totalValueDelta,
376 "whole-life DebtTotal delta must mirror TotalValueOutstanding delta; "
377 "debtTotalDelta=" +
378 to_string(off.debtTotalDelta) +
379 " totalValueDelta=" + to_string(off.totalValueDelta));
380
381 // Derive interestPaid from the whole-life run's independent
382 // ledger deltas:
383 // assetsTotalDelta_off == valueChange
384 // debtTotalDelta_off == valueChange - (principalPaid + interestPaid)
385 // => interestPaid == assetsTotalDelta_off - debtTotalDelta_off - principalPaid
386 Number const interestPaid =
387 off.assetsTotalDelta - off.debtTotalDelta - off.principalPaid;
388 BEAST_EXPECTS(
389 interestPaid >= beast::kZero,
390 "derived interestPaid must be non-negative: " + to_string(interestPaid));
391
392 BEAST_EXPECTS(
393 on.assetsTotalDelta == interestPaid,
394 "cash-basis AssetsTotal delta must equal interestPaid; delta=" +
395 to_string(on.assetsTotalDelta) + " interestPaid=" + to_string(interestPaid));
396 BEAST_EXPECTS(
397 on.debtTotalDelta == -on.principalPaid,
398 "cash-basis DebtTotal delta must equal -principalPaid; delta=" +
399 to_string(on.debtTotalDelta) + " principalPaid=" + to_string(on.principalPaid));
400 };
401
402 // ---- Regular, on-time payment ----
403 {
404 auto const noAdvance = [](Env& env, tp const&) { env.close(); };
405 auto const regularAmount = [&](LoanState const& state) {
406 return STAmount{
407 xrpAsset,
408 roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) *
409 Number{3, -1} * 5}; // 1.5x, so only a single period is paid
410 };
411
412 auto const off = runPayment(all_, 0, 0, noAdvance, regularAmount);
413 auto const on =
414 runPayment(all_ | featureLendingProtocolV1_1, 0, 0, noAdvance, regularAmount);
415
416 // Regular, on-time payments never change the loan's value beyond
417 // normal amortization (production asserts valueChange == 0), so
418 // AssetsTotal must be unaffected in the whole-life run.
419 BEAST_EXPECTS(
420 off.assetsTotalDelta == beast::kZero,
421 "regular on-time payment must not change AssetsTotal under whole-life; delta=" +
422 to_string(off.assetsTotalDelta));
423
424 checkScenario("regular payment", off, on);
425 }
426
427 // ---- Late payment ----
428 {
429 auto const advancePastDue = [&](Env& env, tp const& startDate) {
430 env.close(startDate + std::chrono::seconds(paymentInterval + 1));
431 };
432 auto const lateAmount = [&](LoanState const& state) {
433 return STAmount{
434 xrpAsset,
435 roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) *
436 Number{3}}; // generous; excess is not withdrawn
437 };
438
439 auto const off = runPayment(all_, 0, tfLoanLatePayment, advancePastDue, lateAmount);
440 auto const on = runPayment(
441 all_ | featureLendingProtocolV1_1,
442 0,
443 tfLoanLatePayment,
444 advancePastDue,
445 lateAmount);
446
447 checkScenario("late payment", off, on);
448 }
449
450 // ---- Overpayment ----
451 {
452 auto const noAdvance = [](Env& env, tp const&) { env.close(); };
453 auto const overpayAmount = [&](LoanState const& state) {
454 // One regular period, plus a generous extra principal
455 // paydown.
456 return STAmount{
457 xrpAsset,
458 roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) +
459 xrpAsset(2'000).value()};
460 };
461
462 auto const off =
463 runPayment(all_, tfLoanOverpayment, tfLoanOverpayment, noAdvance, overpayAmount);
464 auto const on = runPayment(
465 all_ | featureLendingProtocolV1_1,
466 tfLoanOverpayment,
467 tfLoanOverpayment,
468 noAdvance,
469 overpayAmount);
470
471 checkScenario("overpayment", off, on);
472 }
473
474 // ---- Full payment ----
475 {
476 auto const noAdvance = [](Env& env, tp const&) { env.close(); };
477 auto const fullAmount = [&](LoanState const&) {
478 // Generously large: full payment only ever consumes exactly
479 // what's due (principal + accrued interest; close fee/
480 // prepayment penalty are 0 here), excess is not withdrawn.
481 return STAmount{xrpAsset, xrpAsset(principalRequest).value() * Number{2}};
482 };
483
484 auto const off = runPayment(all_, 0, tfLoanFullPayment, noAdvance, fullAmount);
485 auto const on = runPayment(
486 all_ | featureLendingProtocolV1_1, 0, tfLoanFullPayment, noAdvance, fullAmount);
487
488 checkScenario("full payment", off, on);
489 }
490 }
491
492 // 3. LoanManage: impair, unimpair, and default.
493 void
495 {
496 using namespace jtx;
497 using namespace loan;
498 using namespace std::chrono_literals;
499
500 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
501 BrokerParameters const brokerParams{
502 .vaultDeposit = 1'000'000,
503 .debtMax = 0,
504 .coverRateMin = TenthBips32{percentageToTenthBips(10)},
505 .coverDeposit = 5'000,
506 .managementFeeRate = TenthBips16{0},
507 .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}};
508
509 Number const principalRequest{10'000};
510 TenthBips32 const interestRate{percentageToTenthBips(12)};
511 std::uint32_t const paymentTotal = 4;
512 std::uint32_t const paymentInterval = 600;
513 std::uint32_t const gracePeriod = 60;
514
515 auto setupLoan = [&](Env& env) {
516 Account const lender{"lender"};
517 Account const borrower{"borrower"};
518 env.fund(XRP(10'000'000), lender, borrower);
519 env.close();
520
521 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
522
523 LoanParameters const loanParams{
524 .account = borrower,
525 .counter = lender,
526 .principalRequest = principalRequest,
527 .interest = interestRate,
528 .payTotal = paymentTotal,
529 .payInterval = paymentInterval,
530 .gracePd = gracePeriod,
531 };
532
533 auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
534 BEAST_EXPECT(brokerBeforeLoan);
535 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
536 auto const loanKeylet =
537 keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
538
539 env(loanParams(env, broker));
540 env.close();
541
542 return std::make_tuple(broker, loanKeylet, lender, borrower);
543 };
544
545 // ---- impair / unimpair ----
546 auto runImpairUnimpair = [&](FeatureBitset features) {
547 Env env(*this, features);
548 auto const [broker, loanKeylet, lender, borrower] = setupLoan(env);
549
550 auto const loanBefore = env.le(loanKeylet);
551 BEAST_EXPECT(loanBefore);
552 Number const principalOutstanding = loanBefore->at(sfPrincipalOutstanding);
553 Number const totalValueOutstanding = loanBefore->at(sfTotalValueOutstanding);
554 Number const managementFeeOutstanding = loanBefore->at(sfManagementFeeOutstanding);
555
556 Number const expectedExposure =
557 env.current()->rules().enabled(featureLendingProtocolV1_1)
558 ? principalOutstanding
559 : totalValueOutstanding - managementFeeOutstanding;
560
561 auto const vaultBeforeImpair = env.le(broker.vaultKeylet());
562 BEAST_EXPECT(vaultBeforeImpair);
563 Number const lossBefore = vaultBeforeImpair->at(sfLossUnrealized);
564
565 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
566 env.close();
567
568 auto const vaultAfterImpair = env.le(broker.vaultKeylet());
569 BEAST_EXPECT(vaultAfterImpair);
570 Number const impairDelta = Number(vaultAfterImpair->at(sfLossUnrealized)) - lossBefore;
571
572 env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS));
573 env.close();
574
575 auto const vaultAfterUnimpair = env.le(broker.vaultKeylet());
576 BEAST_EXPECT(vaultAfterUnimpair);
577 Number const netDelta = Number(vaultAfterUnimpair->at(sfLossUnrealized)) - lossBefore;
578
579 return std::make_tuple(expectedExposure, impairDelta, netDelta);
580 };
581
582 for (auto const features : {all_ | featureLendingProtocolV1_1, all_})
583 {
584 testcase(
585 std::string("cash-basis: LoanManage impair/unimpair (") +
586 (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)"));
587 auto const [expectedExposure, impairDelta, netDelta] = runImpairUnimpair(features);
588
589 BEAST_EXPECTS(
590 impairDelta == expectedExposure,
591 "impair must add loanVaultExposure to LossUnrealized; delta=" +
592 to_string(impairDelta) + " expected=" + to_string(expectedExposure));
593 BEAST_EXPECTS(
594 netDelta == beast::kZero,
595 "unimpair must be an exact reversal of impair; net=" + to_string(netDelta));
596 }
597
598 // ---- impair, then default ----
599 auto runDefault = [&](FeatureBitset features) {
600 Env env(*this, features);
601 auto const [broker, loanKeylet, lender, borrower] = setupLoan(env);
602
603 auto const loanBeforeImpair = env.le(loanKeylet);
604 BEAST_EXPECT(loanBeforeImpair);
605 Number const principalOutstanding = loanBeforeImpair->at(sfPrincipalOutstanding);
606 Number const totalValueOutstanding = loanBeforeImpair->at(sfTotalValueOutstanding);
607 Number const managementFeeOutstanding =
608 loanBeforeImpair->at(sfManagementFeeOutstanding);
609
610 Number const expectedExposure =
611 env.current()->rules().enabled(featureLendingProtocolV1_1)
612 ? principalOutstanding
613 : totalValueOutstanding - managementFeeOutstanding;
614
615 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
616 env.close();
617
618 LoanState const state = getCurrentState(env, broker, loanKeylet);
619 env.close(
620 state.startDate + std::chrono::seconds(paymentInterval) +
621 std::chrono::seconds(gracePeriod) + 60s);
622
623 auto const vaultBefore = env.le(broker.vaultKeylet());
624 auto const brokerBefore = env.le(broker.brokerKeylet());
625 BEAST_EXPECT(vaultBefore && brokerBefore);
626 Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
627 Number const debtTotalBefore = brokerBefore->at(sfDebtTotal);
628 Number const lossBefore = vaultBefore->at(sfLossUnrealized);
629 Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable);
630
631 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
632 env.close();
633
634 auto const vaultAfter = env.le(broker.vaultKeylet());
635 auto const brokerAfter = env.le(broker.brokerKeylet());
636 BEAST_EXPECT(vaultAfter && brokerAfter);
637 Number const assetsTotalDelta =
638 Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore;
639 Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore;
640 Number const lossDelta = Number(vaultAfter->at(sfLossUnrealized)) - lossBefore;
641 Number const coverAvailableDelta =
642 Number(brokerAfter->at(sfCoverAvailable)) - coverAvailableBefore;
643
644 Number const defaultCovered = -coverAvailableDelta;
645 Number const vaultDefaultAmount = expectedExposure - defaultCovered;
646
647 return std::make_tuple(
648 expectedExposure, assetsTotalDelta, debtTotalDelta, lossDelta, vaultDefaultAmount);
649 };
650
651 for (auto const features : {all_ | featureLendingProtocolV1_1, all_})
652 {
653 testcase(
654 std::string("cash-basis: LoanManage default (") +
655 (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)"));
656 auto const
657 [expectedExposure,
658 assetsTotalDelta,
659 debtTotalDelta,
660 lossDelta,
661 vaultDefaultAmount] = runDefault(features);
662
663 BEAST_EXPECTS(
664 debtTotalDelta == -expectedExposure,
665 "default must reduce DebtTotal by the unified default amount; delta=" +
666 to_string(debtTotalDelta) + " expected=" + to_string(expectedExposure));
667 BEAST_EXPECTS(
668 lossDelta == -expectedExposure,
669 "default must reverse the earlier impair's LossUnrealized exactly; delta=" +
670 to_string(lossDelta) + " expected=" + to_string(expectedExposure));
671 BEAST_EXPECTS(
672 assetsTotalDelta == -vaultDefaultAmount,
673 "default must reduce AssetsTotal by (defaultAmount - defaultCovered); delta=" +
674 to_string(assetsTotalDelta) + " expected=" + to_string(-vaultDefaultAmount));
675 }
676 }
677
678 // 3b. LEVersion regression: a Vault created before featureLendingProtocolV1_1
679 // activates (LEVersion absent) must keep whole-life (accrual) accounting
680 // forever, even after the amendment is later enabled -- the switch is
681 // per-Vault (LEVersion == VaultVersion::CashBasis), not a single global amendment
682 // flag.
683 void
685 {
686 testcase("LEVersion: legacy vault keeps accrual after amendment enabled");
687
688 using namespace jtx;
689 using namespace loan;
690 using namespace std::chrono_literals;
691
692 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
693 BrokerParameters const brokerParams{
694 .vaultDeposit = 1'000'000,
695 .debtMax = 0,
696 .coverRateMin = TenthBips32{percentageToTenthBips(10)},
697 .coverDeposit = 5'000,
698 .managementFeeRate = TenthBips16{0},
699 .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}};
700
701 Number const principalRequest{10'000};
702 TenthBips32 const interestRate{percentageToTenthBips(12)};
703 std::uint32_t const paymentTotal = 4;
704 std::uint32_t const paymentInterval = 600;
705 std::uint32_t const gracePeriod = 60;
706
707 // Amendment disabled at Vault creation time: LEVersion stays absent.
708 Env env(*this, all_);
709
710 Account const lender{"lender"};
711 Account const borrower{"borrower"};
712 env.fund(XRP(10'000'000), lender, borrower);
713 env.close();
714
715 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
716
717 {
718 auto const vaultSle = env.le(broker.vaultKeylet());
719 BEAST_EXPECT(vaultSle);
720 BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion));
721 }
722
723 // Now enable the amendment -- production dispatch must still treat
724 // this specific Vault as accrual-basis, since its LEVersion is
725 // (and remains) absent.
726 env.enableFeature(featureLendingProtocolV1_1);
727 env.close();
728
729 LoanParameters const loanParams{
730 .account = borrower,
731 .counter = lender,
732 .principalRequest = principalRequest,
733 .interest = interestRate,
734 .payTotal = paymentTotal,
735 .payInterval = paymentInterval,
736 .gracePd = gracePeriod,
737 };
738
739 auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
740 BEAST_EXPECT(brokerBeforeLoan);
741 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
742 auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
743
744 // ---- LoanSet origination: whole-life formulas expected ----
745 auto const vaultBeforeSet = env.le(broker.vaultKeylet());
746 auto const brokerBeforeSet = env.le(broker.brokerKeylet());
747 BEAST_EXPECT(vaultBeforeSet && brokerBeforeSet);
748 Number const assetsTotalBeforeSet = vaultBeforeSet->at(sfAssetsTotal);
749 Number const debtTotalBeforeSet = brokerBeforeSet->at(sfDebtTotal);
750
751 env(loanParams(env, broker));
752 env.close();
753
754 auto const loanAfterSet = env.le(loanKeylet);
755 BEAST_EXPECT(loanAfterSet);
756 Number const principalOutstanding = loanAfterSet->at(sfPrincipalOutstanding);
757 Number const totalValueOutstanding = loanAfterSet->at(sfTotalValueOutstanding);
758 Number const interestDue = totalValueOutstanding - principalOutstanding;
759 BEAST_EXPECT(interestDue > beast::kZero);
760
761 auto const vaultAfterSet = env.le(broker.vaultKeylet());
762 auto const brokerAfterSet = env.le(broker.brokerKeylet());
763 BEAST_EXPECT(vaultAfterSet && brokerAfterSet);
764 Number const assetsTotalDeltaSet =
765 Number(vaultAfterSet->at(sfAssetsTotal)) - assetsTotalBeforeSet;
766 Number const debtTotalDeltaSet =
767 Number(brokerAfterSet->at(sfDebtTotal)) - debtTotalBeforeSet;
768
769 BEAST_EXPECTS(
770 assetsTotalDeltaSet == interestDue,
771 "legacy vault origination must still add interestDue to AssetsTotal; delta=" +
772 to_string(assetsTotalDeltaSet) + " interestDue=" + to_string(interestDue));
773 BEAST_EXPECTS(
774 debtTotalDeltaSet == principalOutstanding + interestDue,
775 "legacy vault origination must still add principal+interest to DebtTotal; delta=" +
776 to_string(debtTotalDeltaSet));
777
778 LoanState const state = getCurrentState(env, broker, loanKeylet);
779 env.close();
780
781 // ---- LoanPay: whole-life formulas expected ----
782 auto const vaultBeforePay = env.le(broker.vaultKeylet());
783 auto const brokerBeforePay = env.le(broker.brokerKeylet());
784 auto const loanBeforePay = env.le(loanKeylet);
785 BEAST_EXPECT(vaultBeforePay && brokerBeforePay && loanBeforePay);
786 Number const totalValueBeforePay = loanBeforePay->at(sfTotalValueOutstanding);
787 Number const assetsTotalBeforePay = vaultBeforePay->at(sfAssetsTotal);
788 Number const debtTotalBeforePay = brokerBeforePay->at(sfDebtTotal);
789
790 STAmount const paymentAmount{
791 xrpAsset, roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale)};
792 env(pay(borrower, loanKeylet.key, paymentAmount), Ter(tesSUCCESS));
793 env.close();
794
795 auto const vaultAfterPay = env.le(broker.vaultKeylet());
796 auto const brokerAfterPay = env.le(broker.brokerKeylet());
797 auto const loanAfterPay = env.le(loanKeylet);
798 BEAST_EXPECT(vaultAfterPay && brokerAfterPay && loanAfterPay);
799 Number const totalValueAfterPay = loanAfterPay->at(sfTotalValueOutstanding);
800 Number const assetsTotalDeltaPay =
801 Number(vaultAfterPay->at(sfAssetsTotal)) - assetsTotalBeforePay;
802 Number const debtTotalDeltaPay =
803 Number(brokerAfterPay->at(sfDebtTotal)) - debtTotalBeforePay;
804 Number const totalValueDeltaPay = totalValueAfterPay - totalValueBeforePay;
805
806 // A regular, on-time payment has valueChange == 0, so whole-life
807 // AssetsTotal is untouched and DebtTotal mirrors TotalValueOutstanding.
808 BEAST_EXPECTS(
809 assetsTotalDeltaPay == beast::kZero,
810 "legacy vault regular payment must not change AssetsTotal; delta=" +
811 to_string(assetsTotalDeltaPay));
812 BEAST_EXPECTS(
813 debtTotalDeltaPay == totalValueDeltaPay,
814 "legacy vault DebtTotal delta must mirror TotalValueOutstanding delta; "
815 "debtTotalDelta=" +
816 to_string(debtTotalDeltaPay) + " totalValueDelta=" + to_string(totalValueDeltaPay));
817
818 // ---- LoanManage: impair, then default -- whole-life exposure expected ----
819 auto const loanBeforeImpair = env.le(loanKeylet);
820 BEAST_EXPECT(loanBeforeImpair);
821 Number const totalValueBeforeImpair = loanBeforeImpair->at(sfTotalValueOutstanding);
822 Number const managementFeeBeforeImpair = loanBeforeImpair->at(sfManagementFeeOutstanding);
823 Number const expectedExposure = totalValueBeforeImpair - managementFeeBeforeImpair;
824
825 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
826 env.close();
827
828 LoanState const stateAtImpair = getCurrentState(env, broker, loanKeylet);
829 env.close(
830 stateAtImpair.startDate + std::chrono::seconds(paymentInterval) +
831 std::chrono::seconds(gracePeriod) + 60s);
832
833 auto const vaultBeforeDefault = env.le(broker.vaultKeylet());
834 auto const brokerBeforeDefault = env.le(broker.brokerKeylet());
835 BEAST_EXPECT(vaultBeforeDefault && brokerBeforeDefault);
836 Number const debtTotalBeforeDefault = brokerBeforeDefault->at(sfDebtTotal);
837 Number const lossBeforeDefault = vaultBeforeDefault->at(sfLossUnrealized);
838
839 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
840 env.close();
841
842 auto const vaultAfterDefault = env.le(broker.vaultKeylet());
843 auto const brokerAfterDefault = env.le(broker.brokerKeylet());
844 BEAST_EXPECT(vaultAfterDefault && brokerAfterDefault);
845 Number const debtTotalDeltaDefault =
846 Number(brokerAfterDefault->at(sfDebtTotal)) - debtTotalBeforeDefault;
847 Number const lossDeltaDefault =
848 Number(vaultAfterDefault->at(sfLossUnrealized)) - lossBeforeDefault;
849
850 BEAST_EXPECTS(
851 debtTotalDeltaDefault == -expectedExposure,
852 "legacy vault default must reduce DebtTotal by whole-life exposure; delta=" +
853 to_string(debtTotalDeltaDefault) + " expected=" + to_string(expectedExposure));
854 BEAST_EXPECTS(
855 lossDeltaDefault == -expectedExposure,
856 "legacy vault default must reverse the earlier impair's LossUnrealized exactly; "
857 "delta=" +
858 to_string(lossDeltaDefault) + " expected=" + to_string(expectedExposure));
859
860 // Confirm the Vault's LEVersion truly never got set, throughout.
861 {
862 auto const vaultSle = env.le(broker.vaultKeylet());
863 BEAST_EXPECT(vaultSle);
864 BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion));
865 BEAST_EXPECT(getVaultVersion(vaultSle) == VaultVersion::Legacy);
866 }
867 }
868
869 // 4. End-to-end trajectory: LoanSet -> 2 LoanPays -> LoanManage(default),
870 // entirely under the amendment, with independently hand-computed
871 // expected AssetsTotal/DebtTotal/LossUnrealized/CoverAvailable values at
872 // each step. 0% interest keeps the arithmetic exact and tractable; the
873 // divergence from whole-life accounting is already covered directly by
874 // testCashBasisLoanSetOrigination/LoanPay/LoanManage above, so this test
875 // focuses purely on an independent, from-scratch trajectory check.
876 void
878 {
879 testcase("cash-basis: end-to-end trajectory");
880
881 using namespace jtx;
882 using namespace loan;
883 using namespace std::chrono_literals;
884
885 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
886 BrokerParameters const brokerParams{
887 .vaultDeposit = 100'000, .managementFeeRate = TenthBips16{0}};
888
889 Env env(*this, all_ | featureLendingProtocolV1_1);
890
891 Account const lender{"lender"};
892 Account const borrower{"borrower"};
893 env.fund(XRP(10'000'000), lender, borrower);
894 env.close();
895
896 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
897
898 // Hand computation (all values in XRP, drops == 1e-6 XRP):
899 // Vault: AssetsTotal starts at 100'000 (the deposit).
900 // Broker: DebtTotal starts at 0, CoverAvailable starts at 1'000
901 // (BrokerParameters::defaults().coverDeposit).
902 auto const vaultKeylet = broker.vaultKeylet();
903 auto const brokerKeylet = broker.brokerKeylet();
904
905 // All the "human XRP unit" constants below (e.g. `100'000`) are
906 // converted to raw native (drops) values via xrpAsset(...), since
907 // that's how the ledger fields are actually denominated.
908 auto const checkVaultBroker = [&](Number const& assetsTotalUnits,
909 Number const& debtTotalUnits,
910 Number const& lossUnrealizedUnits,
911 Number const& coverAvailableUnits,
912 char const* step) {
913 Number const assetsTotal = xrpAsset(assetsTotalUnits).value();
914 Number const debtTotal = xrpAsset(debtTotalUnits).value();
915 Number const lossUnrealized = xrpAsset(lossUnrealizedUnits).value();
916 Number const coverAvailable = xrpAsset(coverAvailableUnits).value();
917
918 auto const vaultSle = env.le(vaultKeylet);
919 auto const brokerSle = env.le(brokerKeylet);
920 BEAST_EXPECT(vaultSle && brokerSle);
921 BEAST_EXPECTS(
922 vaultSle->at(sfAssetsTotal) == assetsTotal,
923 std::string(step) + ": AssetsTotal expected " + to_string(assetsTotal) + " got " +
924 to_string(Number(vaultSle->at(sfAssetsTotal))));
925 BEAST_EXPECTS(
926 brokerSle->at(sfDebtTotal) == debtTotal,
927 std::string(step) + ": DebtTotal expected " + to_string(debtTotal) + " got " +
928 to_string(Number(brokerSle->at(sfDebtTotal))));
929 BEAST_EXPECTS(
930 vaultSle->at(sfLossUnrealized) == lossUnrealized,
931 std::string(step) + ": LossUnrealized expected " + to_string(lossUnrealized) +
932 " got " + to_string(Number(vaultSle->at(sfLossUnrealized))));
933 BEAST_EXPECTS(
934 brokerSle->at(sfCoverAvailable) == coverAvailable,
935 std::string(step) + ": CoverAvailable expected " + to_string(coverAvailable) +
936 " got " + to_string(Number(brokerSle->at(sfCoverAvailable))));
937 };
938
939 checkVaultBroker(100'000, 0, 0, 1'000, "before LoanSet");
940
941 // Loan: principal=1200, 0% interest, 12 payments of 100 each, no fees.
942 Number const principalRequest{1'200};
943 std::uint32_t const paymentTotal = 12;
944 std::uint32_t const paymentInterval = 600;
945 std::uint32_t const gracePeriod = 60;
946
947 auto const brokerBeforeLoan = env.le(brokerKeylet);
948 BEAST_EXPECT(brokerBeforeLoan);
949 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
950 auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
951
952 LoanParameters const loanParams{
953 .account = borrower,
954 .counter = lender,
955 .principalRequest = principalRequest,
956 .interest = TenthBips32{0},
957 .payTotal = paymentTotal,
958 .payInterval = paymentInterval,
959 .gracePd = gracePeriod,
960 };
961 env(loanParams(env, broker));
962 env.close();
963
964 // Origination (cash-basis): AssetsTotal += 0, DebtTotal += principal.
965 checkVaultBroker(100'000, 1'200, 0, 1'000, "after LoanSet");
966
967 LoanState const state = getCurrentState(env, broker, loanKeylet);
968 BEAST_EXPECT(state.periodicPayment == xrpAsset(100).value());
969
970 // Payment 1: principalPaid=100, interestPaid=0.
971 // AssetsTotal += 0; DebtTotal -= 100.
972 env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS));
973 env.close();
974 checkVaultBroker(100'000, 1'100, 0, 1'000, "after payment 1");
975
976 // Payment 2: same as above.
977 env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS));
978 env.close();
979 checkVaultBroker(100'000, 1'000, 0, 1'000, "after payment 2");
980
981 // Default (no impair): principalOutstanding remaining is 1'000.
982 // totalDefaultAmount (cash-basis) = PrincipalOutstanding = 1'000.
983 // minimumCover = DebtTotal(1'000) * coverRateMin(10%) = 100.
984 // covered = min(minimumCover * coverRateLiquidation(25%), totalDefaultAmount)
985 // = min(25, 1'000) = 25.
986 // defaultCovered = min(covered, CoverAvailable(1'000)) = 25.
987 // vaultDefaultAmount = 1'000 - 25 = 975.
988 // DebtTotal -= 1'000 -> 0. CoverAvailable -= 25 -> 975.
989 // AssetsTotal -= 975 -> 99'025. LossUnrealized unaffected (never impaired).
990 auto const loanBeforeDefault = env.le(loanKeylet);
991 BEAST_EXPECT(loanBeforeDefault);
992 BEAST_EXPECT(
993 Number(loanBeforeDefault->at(sfPrincipalOutstanding)) == xrpAsset(1'000).value());
994
995 env.close(state.startDate + std::chrono::seconds((3 * paymentInterval) + gracePeriod) + 1s);
996
997 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
998 env.close();
999
1000 checkVaultBroker(99'025, 0, 0, 975, "after LoanManage(default)");
1001 }
1002
1003public:
1004 void
1013};
1014
1015BEAST_DEFINE_TESTSUITE(LoanCashBasis, tx, xrpl);
1016
1017} // namespace xrpl::test
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
void run() override
Runs the suite.
FeatureBitset const all_
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const &params=BrokerParameters::defaults())
LoanState getCurrentState(jtx::Env const &env, BrokerInfo const &broker, Keylet const &loanKeylet)
Get the state without checking anything.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
void enableFeature(uint256 const feature)
Definition Env.cpp:709
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
Set the fee on a JTx.
Definition fee.h:20
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
T make_tuple(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:573
json::Value set(AccountID const &account, uint256 const &vaultId, uint32_t flags)
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:126
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
Number roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
TERSubset< CanCvtToTER > TER
Definition TER.h:647
@ tecLIMIT_EXCEEDED
Definition TER.h:364
VaultVersion getVaultVersion(SLE::const_ref vault)
Resolves a Vault's LEVersion, the single point every accounting touch point should call to determine ...
@ tesSUCCESS
Definition TER.h:245