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/permissioned_domains.h>
8#include <test/jtx/ter.h>
9#include <test/jtx/vault.h>
10
11#include <xrpl/basics/Number.h>
12#include <xrpl/basics/base_uint.h>
13#include <xrpl/basics/chrono.h>
14#include <xrpl/beast/unit_test/suite.h>
15#include <xrpl/beast/utility/Zero.h>
16#include <xrpl/ledger/helpers/LendingHelpers.h>
17#include <xrpl/ledger/helpers/VaultHelpers.h>
18#include <xrpl/protocol/Feature.h>
19#include <xrpl/protocol/Indexes.h>
20#include <xrpl/protocol/Issue.h>
21#include <xrpl/protocol/Protocol.h>
22#include <xrpl/protocol/SField.h>
23#include <xrpl/protocol/SeqProxy.h>
24#include <xrpl/protocol/TER.h>
25#include <xrpl/protocol/TxFlags.h>
26
27#include <chrono>
28#include <cstdint>
29#include <functional>
30#include <string>
31#include <tuple>
32
33namespace xrpl::test {
34
35// LendingProtocolV1_1 ("cash-basis" accounting) dedicated coverage.
36//
37// Existing tests never enable featureLendingProtocolV1_1 (see `all_`
38// above), so these are the only tests in this file that exercise the
39// amendment. They are called once, directly, from
40// runAmendmentIndependent() -- not looped through
41// runAmendmentSensitive()/amendmentCombinations(), since doing so would
42// require re-deriving instant-recognition-specific expected values for ~15
43// unrelated regression tests.
45{
46private:
47 // 1. LoanSet origination: Vault.AssetsTotal/LoanBroker.DebtTotal deltas,
48 // and the AssetsMaximum/DebtMaximum guards. Instant-recognition AssetsMaximum still
49 // requires headroom for interestDue; cash-basis AssetsMaximum does not,
50 // because origination does not credit interest into AssetsTotal.
51 void
53 {
54 testcase("cash-basis: LoanSet origination");
55
56 using namespace jtx;
57 using namespace loan;
58
59 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
60 BrokerParameters const brokerParams{
61 .vaultDeposit = 100'000,
62 .debtMax = 0,
63 .coverRateMin = TenthBips32{0},
64 .coverDeposit = 0,
65 .managementFeeRate = TenthBips16{0},
66 .coverRateLiquidation = TenthBips32{0}};
67
68 Number const principalRequest{10'000};
69 TenthBips32 const interestRate{percentageToTenthBips(10)};
70 std::uint32_t const paymentTotal = 2;
71 std::uint32_t const paymentInterval = 86400;
72
73 // Creates a broker/vault, submits a single LoanSet with a nonzero
74 // interest rate, and returns the observed Vault.AssetsTotal /
75 // LoanBroker.DebtTotal deltas plus the loan's own computed
76 // interestDue and principalOutstanding.
77 auto runOrigination = [&](FeatureBitset features) {
78 Env env(*this, features);
79
80 Account const lender{"lender"};
81 Account const borrower{"borrower"};
82 env.fund(XRP(1'000'000), lender, borrower);
83 env.close();
84
85 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
86
87 auto const vaultBefore = env.le(broker.vaultKeylet());
88 auto const brokerBefore = env.le(broker.brokerKeylet());
89 BEAST_EXPECT(vaultBefore && brokerBefore);
90 Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
91 Number const debtTotalBefore = brokerBefore->at(sfDebtTotal);
92
93 auto const loanSequence = brokerBefore->at(sfLoanSequence);
94 auto const loanKeylet =
95 keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
96
97 env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()),
98 kCounterparty(lender),
99 kInterestRate(interestRate),
100 kPaymentTotal(paymentTotal),
101 kPaymentInterval(paymentInterval),
102 Sig(sfCounterpartySignature, lender),
103 Fee(env.current()->fees().base * 2),
104 Ter(tesSUCCESS));
105 env.close();
106
107 auto const loanSle = env.le(loanKeylet);
108 BEAST_EXPECT(loanSle);
109 Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
110 Number const totalValueOutstanding = loanSle->at(sfTotalValueOutstanding);
111 Number const interestDue = totalValueOutstanding - principalOutstanding;
112 BEAST_EXPECT(interestDue > beast::kZero);
113 BEAST_EXPECT(principalOutstanding == xrpAsset(principalRequest).value());
114
115 auto const vaultAfter = env.le(broker.vaultKeylet());
116 auto const brokerAfter = env.le(broker.brokerKeylet());
117 BEAST_EXPECT(vaultAfter && brokerAfter);
118 Number const assetsTotalDelta =
119 Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore;
120 Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore;
121
122 return std::make_tuple(
123 assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding);
124 };
125
126 Number interestDueCash{};
127 Number principalOutstandingCash{};
128 {
129 auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] =
130 runOrigination(all_ | featureLendingProtocolV1_1);
131 interestDueCash = interestDue;
132 principalOutstandingCash = principalOutstanding;
133
134 BEAST_EXPECTS(
135 assetsTotalDelta == beast::kZero,
136 "cash-basis origination must not change AssetsTotal; delta=" +
137 to_string(assetsTotalDelta));
138 BEAST_EXPECTS(
139 debtTotalDelta == principalOutstanding,
140 "cash-basis origination must add principal-only to DebtTotal; delta=" +
141 to_string(debtTotalDelta) + " principal=" + to_string(principalOutstanding));
142 }
143
144 {
145 auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] =
146 runOrigination(all_);
147
148 BEAST_EXPECTS(
149 assetsTotalDelta == interestDue,
150 "instant-recognition origination must add interestDue to AssetsTotal; delta=" +
151 to_string(assetsTotalDelta) + " interestDue=" + to_string(interestDue));
152 BEAST_EXPECTS(
153 debtTotalDelta == principalOutstanding + interestDue,
154 "instant-recognition origination must add principal+interest to DebtTotal; delta=" +
155 to_string(debtTotalDelta));
156 }
157
158 // AssetsMaximum guard checks interestDue headroom only under
159 // instant interest recognition; DebtMaximum guard also varies by model.
160 auto runVaultGuard = [&](FeatureBitset features, Number const& slack, TER expected) {
161 Env env(*this, features);
162
163 Account const lender{"lender"};
164 Account const borrower{"borrower"};
165 env.fund(XRP(1'000'000), lender, borrower);
166 env.close();
167
168 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
169
170 auto const vaultSle = env.le(broker.vaultKeylet());
171 BEAST_EXPECT(vaultSle);
172 Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal);
173
174 Vault const vault{env};
175 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
176 tx[sfAssetsMaximum] = assetsTotalBefore + slack;
177 env(tx);
178 env.close();
179
180 env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()),
181 kCounterparty(lender),
182 kInterestRate(interestRate),
183 kPaymentTotal(paymentTotal),
184 kPaymentInterval(paymentInterval),
185 Sig(sfCounterpartySignature, lender),
186 Fee(env.current()->fees().base * 2),
187 Ter(expected));
188 env.close();
189 };
190
191 auto runBrokerGuard = [&](FeatureBitset features, Number const& debtMaximum, TER expected) {
192 Env env(*this, features);
193
194 Account const lender{"lender"};
195 Account const borrower{"borrower"};
196 env.fund(XRP(1'000'000), lender, borrower);
197 env.close();
198
199 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
200
201 env(loan_broker::set(lender, broker.vaultID),
203 loan_broker::kDebtMaximum(debtMaximum),
204 Fee(env.current()->fees().base * 2));
205 env.close();
206
207 env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()),
208 kCounterparty(lender),
209 kInterestRate(interestRate),
210 kPaymentTotal(paymentTotal),
211 kPaymentInterval(paymentInterval),
212 Sig(sfCounterpartySignature, lender),
213 Fee(env.current()->fees().base * 2),
214 Ter(expected));
215 env.close();
216 };
217
218 Number const oneDrop = xrpAsset(1).value();
219 {
220 testcase(
221 "instant-recognition: LoanSet AssetsMaximum guard checks interestDue headroom");
222 // Guard rejects when there's not quite enough headroom for the
223 // interest.
224 runVaultGuard(all_, interestDueCash - oneDrop, tecLIMIT_EXCEEDED);
225 // Guard accepts at the exact boundary.
226 runVaultGuard(all_, interestDueCash, tesSUCCESS);
227 }
228
229 {
230 testcase("cash-basis: LoanSet AssetsMaximum guard ignores interestDue headroom");
231 // Even far less headroom than interestDue still succeeds, since
232 // cash-basis origination never adds interest to AssetsTotal.
233 runVaultGuard(all_ | featureLendingProtocolV1_1, oneDrop, tesSUCCESS);
234 // Fully subscribed: AssetsTotal == AssetsMaximum. Instant-recognition preclaim
235 // used to refuse this; origination must still succeed because it
236 // does not change AssetsTotal.
237 runVaultGuard(all_ | featureLendingProtocolV1_1, Number{0}, tesSUCCESS);
238 }
239
240 // DebtMaximum guard: cash-basis projects principal-only DebtTotal;
241 // instant recognition projects principal + interestDue.
242 for (auto const cashBasis : {true, false})
243 {
244 testcase(
245 std::string("LoanSet DebtMaximum guard (") +
246 (cashBasis ? "cash-basis)" : "instant-recognition)"));
247 auto const features = cashBasis ? all_ | featureLendingProtocolV1_1 : all_;
248 Number const newDebtTotal =
249 principalOutstandingCash + (cashBasis ? Number{} : interestDueCash);
250 runBrokerGuard(features, newDebtTotal - oneDrop, tecLIMIT_EXCEEDED);
251 runBrokerGuard(features, newDebtTotal, tesSUCCESS);
252 }
253 }
254
255 // 2. LoanPay: regular, late, overpayment, and full-payment types.
256 // Assert Vault.AssetsTotal/LoanBroker.DebtTotal deltas match
257 // interestPaid/principalPaid under cash-basis, and cross-check the
258 // amendment-disabled run's deltas against the documented instant-recognition
259 // formula (AssetsTotal += valueChange; DebtTotal mirrors the loan's own
260 // TotalValueOutstanding delta exactly, since instant-recognition debt recognition
261 // tracks total loan value).
262 void
264 {
265 using namespace jtx;
266 using namespace loan;
267 using namespace std::chrono_literals;
268 using Tp = NetClock::time_point;
269
270 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
271 BrokerParameters const brokerParams{
272 .vaultDeposit = 1'000'000,
273 .debtMax = 0,
274 .coverRateMin = TenthBips32{0},
275 .coverDeposit = 0,
276 .managementFeeRate = TenthBips16{0},
277 .coverRateLiquidation = TenthBips32{0}};
278
279 Number const principalRequest{12'000};
280 TenthBips32 const interestRate{percentageToTenthBips(12)};
281 std::uint32_t const paymentTotal = 4;
282 std::uint32_t const paymentInterval = 600;
283 std::uint32_t const gracePeriod = 300;
284
285 struct PaymentDeltas
286 {
287 Number principalPaid;
288 Number assetsTotalDelta;
289 Number debtTotalDelta;
290 Number totalValueDelta;
291 };
292
293 // Sets up a fresh broker + loan, advances time, submits a single
294 // payment of the given type/amount, and returns the observed deltas.
295 auto runPayment = [&](FeatureBitset features,
296 std::uint32_t loanSetFlags,
297 std::uint32_t payFlags,
298 std::function<void(Env&, Tp const&)> const& advanceTime,
299 std::function<STAmount(LoanState const&)> const& paymentAmount) {
300 Env env(*this, features);
301
302 Account const lender{"lender"};
303 Account const borrower{"borrower"};
304 env.fund(XRP(10'000'000), lender, borrower);
305 env.close();
306
307 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
308
309 LoanParameters const loanParams{
310 .account = borrower,
311 .counter = lender,
312 .principalRequest = principalRequest,
313 .interest = interestRate,
314 .payTotal = paymentTotal,
315 .payInterval = paymentInterval,
316 .gracePd = gracePeriod,
317 .flags = loanSetFlags,
318 };
319
320 auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
321 BEAST_EXPECT(brokerBeforeLoan);
322 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
323 auto const loanKeylet =
324 keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
325
326 env(loanParams(env, broker));
327 env.close();
328
329 LoanState const state = getCurrentState(env, broker, loanKeylet);
330
331 advanceTime(env, state.startDate);
332
333 auto const vaultBefore = env.le(broker.vaultKeylet());
334 auto const brokerBefore = env.le(broker.brokerKeylet());
335 auto const loanBefore = env.le(loanKeylet);
336 BEAST_EXPECT(vaultBefore && brokerBefore && loanBefore);
337
338 Number const principalBefore = loanBefore->at(sfPrincipalOutstanding);
339 Number const totalValueBefore = loanBefore->at(sfTotalValueOutstanding);
340 Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
341 Number const debtTotalBefore = brokerBefore->at(sfDebtTotal);
342
343 STAmount const amount = paymentAmount(state);
344 env(pay(borrower, loanKeylet.key, amount, payFlags), Ter(tesSUCCESS));
345 env.close();
346
347 auto const vaultAfter = env.le(broker.vaultKeylet());
348 auto const brokerAfter = env.le(broker.brokerKeylet());
349 auto const loanAfter = env.le(loanKeylet);
350 BEAST_EXPECT(vaultAfter && brokerAfter && loanAfter);
351
352 Number const principalAfter = loanAfter->at(sfPrincipalOutstanding);
353 Number const totalValueAfter = loanAfter->at(sfTotalValueOutstanding);
354 Number const assetsTotalAfter = vaultAfter->at(sfAssetsTotal);
355 Number const debtTotalAfter = brokerAfter->at(sfDebtTotal);
356
357 return PaymentDeltas{
358 .principalPaid = principalBefore - principalAfter,
359 .assetsTotalDelta = assetsTotalAfter - assetsTotalBefore,
360 .debtTotalDelta = debtTotalAfter - debtTotalBefore,
361 .totalValueDelta = totalValueAfter - totalValueBefore};
362 };
363
364 // Compares the disabled (instant-recognition) and enabled (cash-basis) runs
365 // of the same payment scenario, and asserts the documented
366 // relationships between them.
367 auto checkScenario = [&](std::string const& label,
368 PaymentDeltas const& off,
369 PaymentDeltas const& on) {
370 testcase("cash-basis: LoanPay " + label);
371
372 // The loan's own PrincipalOutstanding field is untouched by
373 // the amendment.
374 BEAST_EXPECTS(
375 off.principalPaid == on.principalPaid,
376 "principalPaid must be amendment-independent; off=" + to_string(off.principalPaid) +
377 " on=" + to_string(on.principalPaid));
378
379 // Whole-life structural invariant: DebtTotal (which
380 // recognizes a loan's full remaining value as debt) must
381 // change exactly as the loan's own TotalValueOutstanding
382 // does.
383 BEAST_EXPECTS(
384 off.debtTotalDelta == off.totalValueDelta,
385 "instant-recognition DebtTotal delta must mirror TotalValueOutstanding delta; "
386 "debtTotalDelta=" +
387 to_string(off.debtTotalDelta) +
388 " totalValueDelta=" + to_string(off.totalValueDelta));
389
390 // Derive interestPaid from the instant-recognition run's independent
391 // ledger deltas:
392 // assetsTotalDelta_off == valueChange
393 // debtTotalDelta_off == valueChange - (principalPaid + interestPaid)
394 // => interestPaid == assetsTotalDelta_off - debtTotalDelta_off - principalPaid
395 Number const interestPaid =
396 off.assetsTotalDelta - off.debtTotalDelta - off.principalPaid;
397 BEAST_EXPECTS(
398 interestPaid >= beast::kZero,
399 "derived interestPaid must be non-negative: " + to_string(interestPaid));
400
401 BEAST_EXPECTS(
402 on.assetsTotalDelta == interestPaid,
403 "cash-basis AssetsTotal delta must equal interestPaid; delta=" +
404 to_string(on.assetsTotalDelta) + " interestPaid=" + to_string(interestPaid));
405 BEAST_EXPECTS(
406 on.debtTotalDelta == -on.principalPaid,
407 "cash-basis DebtTotal delta must equal -principalPaid; delta=" +
408 to_string(on.debtTotalDelta) + " principalPaid=" + to_string(on.principalPaid));
409 };
410
411 // ---- Regular, on-time payment ----
412 {
413 auto const noAdvance = [](Env& env, Tp const&) { env.close(); };
414 auto const regularAmount = [&](LoanState const& state) {
415 return STAmount{
416 xrpAsset,
417 roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) *
418 Number{3, -1} * 5}; // 1.5x, so only a single period is paid
419 };
420
421 auto const off = runPayment(all_, 0, 0, noAdvance, regularAmount);
422 auto const on =
423 runPayment(all_ | featureLendingProtocolV1_1, 0, 0, noAdvance, regularAmount);
424
425 // Regular, on-time payments never change the loan's value beyond
426 // normal amortization (production asserts valueChange == 0), so
427 // AssetsTotal must be unaffected in the instant-recognition run.
428 BEAST_EXPECTS(
429 off.assetsTotalDelta == beast::kZero,
430 "regular on-time payment must not change AssetsTotal under instant recognition; "
431 "delta=" +
432 to_string(off.assetsTotalDelta));
433
434 checkScenario("regular payment", off, on);
435 }
436
437 // ---- Late payment ----
438 {
439 auto const advancePastDue = [&](Env& env, Tp const& startDate) {
440 env.close(startDate + std::chrono::seconds(paymentInterval + 1));
441 };
442 auto const lateAmount = [&](LoanState const& state) {
443 return STAmount{
444 xrpAsset,
445 roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) *
446 Number{3}}; // generous; excess is not withdrawn
447 };
448
449 auto const off = runPayment(all_, 0, tfLoanLatePayment, advancePastDue, lateAmount);
450 auto const on = runPayment(
451 all_ | featureLendingProtocolV1_1,
452 0,
453 tfLoanLatePayment,
454 advancePastDue,
455 lateAmount);
456
457 checkScenario("late payment", off, on);
458 }
459
460 // ---- Overpayment ----
461 {
462 auto const noAdvance = [](Env& env, Tp const&) { env.close(); };
463 auto const overpayAmount = [&](LoanState const& state) {
464 // One regular period, plus a generous extra principal
465 // paydown.
466 return STAmount{
467 xrpAsset,
468 roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) +
469 xrpAsset(2'000).value()};
470 };
471
472 auto const off =
473 runPayment(all_, tfLoanOverpayment, tfLoanOverpayment, noAdvance, overpayAmount);
474 auto const on = runPayment(
475 all_ | featureLendingProtocolV1_1,
476 tfLoanOverpayment,
477 tfLoanOverpayment,
478 noAdvance,
479 overpayAmount);
480
481 checkScenario("overpayment", off, on);
482 }
483
484 // ---- Full payment ----
485 {
486 auto const noAdvance = [](Env& env, Tp const&) { env.close(); };
487 auto const fullAmount = [&](LoanState const&) {
488 // Generously large: full payment only ever consumes exactly
489 // what's due (principal + accrued interest; close fee/
490 // prepayment penalty are 0 here), excess is not withdrawn.
491 return STAmount{xrpAsset, xrpAsset(principalRequest).value() * Number{2}};
492 };
493
494 auto const off = runPayment(all_, 0, tfLoanFullPayment, noAdvance, fullAmount);
495 auto const on = runPayment(
496 all_ | featureLendingProtocolV1_1, 0, tfLoanFullPayment, noAdvance, fullAmount);
497
498 checkScenario("full payment", off, on);
499 }
500 }
501
502 // VaultSet must still succeed when cash-basis LoanPay has already pushed
503 // AssetsTotal above a nonzero AssetsMaximum. Before fixCleanup3_4_0,
504 // ValidVault rejects that with tecINVARIANT_FAILED even though
505 // VaultSet::doApply and the product rule allow the over-cap state when
506 // the excess is interest.
507 void
509 {
510 using namespace jtx;
511 using namespace loan;
512
513 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
514 BrokerParameters const brokerParams{
515 .vaultDeposit = 1'000'000,
516 .debtMax = 0,
517 .coverRateMin = TenthBips32{0},
518 .coverDeposit = 0,
519 .managementFeeRate = TenthBips16{0},
520 .coverRateLiquidation = TenthBips32{0}};
521
522 auto run =
523 [&](FeatureBitset features, TER expectedOverCapSet, bool native, bool vaultPrivate) {
524 bool const fix340Enabled = features[fixCleanup3_4_0];
525 testcase(
526 std::string("cash-basis: VaultSet while AssetsTotal exceeds AssetsMaximum") +
527 (native ? " XRP" : " IOU") + (vaultPrivate ? " private" : "") +
528 (fix340Enabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
529
530 Account const issuer{"issuer"};
531 Account const lender{"lender"};
532 Account const borrower{"borrower"};
533 Env env(*this, features);
534
535 BrokerParameters params = brokerParams;
536 if (vaultPrivate)
537 params.vaultFlags = tfVaultPrivate;
538
539 PrettyAsset vaultAsset = xrpAsset;
540 if (native)
541 {
542 env.fund(XRP(10'000'000), lender, borrower);
543 env.close();
544 }
545 else
546 {
547 vaultAsset = createFundedIouAsset(env, issuer, lender, borrower);
548 }
549
550 BrokerInfo const broker{createVaultAndBroker(env, vaultAsset, lender, params)};
551 auto const vaultBefore = env.le(broker.vaultKeylet());
552 BEAST_EXPECT(vaultBefore);
553 // One unit at the vault's asset scale so the stored cap is
554 // strictly above AssetsTotal (a smaller ULP rounds away).
555 // Cash-basis origination does not credit interest, so LoanSet
556 // still succeeds.
557 Number const slack{1, -static_cast<int>(vaultBefore->at(sfScale))};
558 Number const assetsMaximum = Number(vaultBefore->at(sfAssetsTotal)) + slack;
559
560 Vault const vault{env};
561 {
562 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
563 tx[sfAssetsMaximum] = assetsMaximum;
564 env(tx);
565 env.close();
566 }
567
568 {
569 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
570 tx[sfData] = "AA";
571 env(tx, Ter(tesSUCCESS));
572 env.close();
573 }
574
575 auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
576 BEAST_EXPECT(brokerBeforeLoan);
577 auto const loanKeylet = keylet::loan(
578 broker.brokerID, SeqProxy::rawSequence(brokerBeforeLoan->at(sfLoanSequence)));
579
580 LoanParameters const loanParams{
581 .account = borrower,
582 .counter = lender,
583 .principalRequest = 12'000,
584 .interest = TenthBips32{percentageToTenthBips(12)},
585 .payTotal = 4,
586 .payInterval = 600,
587 .gracePd = 300,
588 };
589 env(loanParams(env, broker));
590 env.close();
591
592 auto const vaultAfterLoan = env.le(broker.vaultKeylet());
593 BEAST_EXPECT(vaultAfterLoan);
594 BEAST_EXPECT(vaultAfterLoan->at(sfAssetsTotal) <= assetsMaximum);
595
596 LoanState const state = getCurrentState(env, broker, loanKeylet);
597 STAmount const payment{
598 vaultAsset,
599 roundPeriodicPayment(vaultAsset, state.periodicPayment, state.loanScale) *
600 Number{3, -1} * 5};
601 env(pay(borrower, loanKeylet.key, payment), Ter(tesSUCCESS));
602 env.close();
603
604 auto const vaultAboveMaximum = env.le(broker.vaultKeylet());
605 BEAST_EXPECT(vaultAboveMaximum);
606 BEAST_EXPECT(vaultAboveMaximum->at(sfAssetsTotal) > assetsMaximum);
607 BEAST_EXPECT(vaultAboveMaximum->at(sfAssetsMaximum) == assetsMaximum);
608
609 {
610 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
611 tx[sfData] = "BB";
612 env(tx, Ter(expectedOverCapSet));
613 env.close();
614 }
615
616 if (vaultPrivate)
617 {
619 {.issuer = lender, .credType = "credential"}};
620 env(pdomain::setTx(lender, credentials));
621 auto const domainId = pdomain::getNewDomain(env.meta());
622 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
623 tx[sfDomainID] = to_string(domainId);
624 env(tx, Ter(expectedOverCapSet));
625 env.close();
626 }
627
628 if (!fix340Enabled)
629 return;
630
631 {
632 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
633 tx[sfAssetsMaximum] = assetsMaximum;
634 env(tx, Ter(tecLIMIT_EXCEEDED));
635 env.close();
636 }
637
638 {
639 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
640 tx[sfAssetsMaximum] = Number{0};
641 env(tx, Ter(tesSUCCESS));
642 env.close();
643 }
644 };
645
646 FeatureBitset const withFix = all_ | featureLendingProtocolV1_1;
647 FeatureBitset const withoutFix = withFix - fixCleanup3_4_0;
648
649 run(withFix, tesSUCCESS, true, true);
650 run(withoutFix, tecINVARIANT_FAILED, true, true);
651 run(withFix, tesSUCCESS, false, false);
652 run(withoutFix, tecINVARIANT_FAILED, false, false);
653 }
654
655 void
657 {
658 testcase("cash-basis: LoanSet after interest pushes AssetsTotal past AssetsMaximum");
659
660 using namespace jtx;
661 using namespace loan;
662
663 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
664 BrokerParameters const brokerParams{
665 .vaultDeposit = 1'000'000,
666 .debtMax = 0,
667 .coverRateMin = TenthBips32{0},
668 .coverDeposit = 0,
669 .managementFeeRate = TenthBips16{0},
670 .coverRateLiquidation = TenthBips32{0}};
671
672 Account const lender{"lender"};
673 Account const borrower{"borrower"};
674 Env env(*this, all_ | featureLendingProtocolV1_1);
675 env.fund(XRP(10'000'000), lender, borrower);
676 env.close();
677
678 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
679 auto const vaultBefore = env.le(broker.vaultKeylet());
680 BEAST_EXPECT(vaultBefore);
681 Number const assetsMaximum = Number(vaultBefore->at(sfAssetsTotal));
682
683 Vault const vault{env};
684 {
685 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
686 tx[sfAssetsMaximum] = assetsMaximum;
687 env(tx);
688 env.close();
689 }
690
691 auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
692 BEAST_EXPECT(brokerBeforeLoan);
693 auto const firstLoanKeylet = keylet::loan(
694 broker.brokerID, SeqProxy::rawSequence(brokerBeforeLoan->at(sfLoanSequence)));
695
696 Number const firstPrincipal = xrpAsset(12'000).value();
697 env(set(borrower, broker.brokerID, firstPrincipal),
698 kCounterparty(lender),
699 kInterestRate(TenthBips32{percentageToTenthBips(12)}),
700 kPaymentTotal(4),
701 kPaymentInterval(600),
702 Sig(sfCounterpartySignature, lender),
703 Fee(env.current()->fees().base * 2),
704 Ter(tesSUCCESS));
705 env.close();
706
707 auto const vaultAfterFirst = env.le(broker.vaultKeylet());
708 BEAST_EXPECT(vaultAfterFirst);
709 BEAST_EXPECT(vaultAfterFirst->at(sfAssetsTotal) == assetsMaximum);
710 BEAST_EXPECT(vaultAfterFirst->at(sfAssetsAvailable) == assetsMaximum - firstPrincipal);
711
712 LoanState const state = getCurrentState(env, broker, firstLoanKeylet);
713 STAmount const payment{
714 xrpAsset,
715 roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * Number{3, -1} *
716 5};
717 env(pay(borrower, firstLoanKeylet.key, payment), Ter(tesSUCCESS));
718 env.close();
719
720 auto const vaultAfterPay = env.le(broker.vaultKeylet());
721 BEAST_EXPECT(vaultAfterPay);
722 BEAST_EXPECT(vaultAfterPay->at(sfAssetsTotal) > assetsMaximum);
723 BEAST_EXPECT(vaultAfterPay->at(sfAssetsAvailable) > beast::kZero);
724
725 auto const brokerAfterPay = env.le(broker.brokerKeylet());
726 BEAST_EXPECT(brokerAfterPay);
727 auto const secondLoanKeylet = keylet::loan(
728 broker.brokerID, SeqProxy::rawSequence(brokerAfterPay->at(sfLoanSequence)));
729
730 Number const secondPrincipal = xrpAsset(1'000).value();
731 env(set(borrower, broker.brokerID, secondPrincipal),
732 kCounterparty(lender),
733 kInterestRate(TenthBips32{percentageToTenthBips(12)}),
734 kPaymentTotal(4),
735 kPaymentInterval(600),
736 Sig(sfCounterpartySignature, lender),
737 Fee(env.current()->fees().base * 2),
738 Ter(tesSUCCESS));
739 env.close();
740
741 auto const vaultAfterSecond = env.le(broker.vaultKeylet());
742 auto const secondLoan = env.le(secondLoanKeylet);
743 BEAST_EXPECT(vaultAfterSecond && secondLoan);
744 BEAST_EXPECT(vaultAfterSecond->at(sfAssetsTotal) == vaultAfterPay->at(sfAssetsTotal));
745 BEAST_EXPECT(secondLoan->at(sfPrincipalOutstanding) == secondPrincipal);
746 }
747
748 // 3. LoanManage: impair, unimpair, and default.
749 void
751 {
752 using namespace jtx;
753 using namespace loan;
754 using namespace std::chrono_literals;
755
756 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
757 BrokerParameters const brokerParams{
758 .vaultDeposit = 1'000'000,
759 .debtMax = 0,
760 .coverRateMin = TenthBips32{percentageToTenthBips(10)},
761 .coverDeposit = 5'000,
762 .managementFeeRate = TenthBips16{0},
763 .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}};
764
765 Number const principalRequest{10'000};
766 TenthBips32 const interestRate{percentageToTenthBips(12)};
767 std::uint32_t const paymentTotal = 4;
768 std::uint32_t const paymentInterval = 600;
769 std::uint32_t const gracePeriod = 60;
770
771 auto setupLoan = [&](Env& env) {
772 Account const lender{"lender"};
773 Account const borrower{"borrower"};
774 env.fund(XRP(10'000'000), lender, borrower);
775 env.close();
776
777 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
778
779 LoanParameters const loanParams{
780 .account = borrower,
781 .counter = lender,
782 .principalRequest = principalRequest,
783 .interest = interestRate,
784 .payTotal = paymentTotal,
785 .payInterval = paymentInterval,
786 .gracePd = gracePeriod,
787 };
788
789 auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
790 BEAST_EXPECT(brokerBeforeLoan);
791 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
792 auto const loanKeylet =
793 keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
794
795 env(loanParams(env, broker));
796 env.close();
797
798 return std::make_tuple(broker, loanKeylet, lender, borrower);
799 };
800
801 // ---- impair / unimpair ----
802 auto runImpairUnimpair = [&](FeatureBitset features) {
803 Env env(*this, features);
804 auto const [broker, loanKeylet, lender, borrower] = setupLoan(env);
805
806 auto const loanBefore = env.le(loanKeylet);
807 BEAST_EXPECT(loanBefore);
808 Number const principalOutstanding = loanBefore->at(sfPrincipalOutstanding);
809 Number const totalValueOutstanding = loanBefore->at(sfTotalValueOutstanding);
810 Number const managementFeeOutstanding = loanBefore->at(sfManagementFeeOutstanding);
811
812 Number const expectedExposure =
813 env.current()->rules().enabled(featureLendingProtocolV1_1)
814 ? principalOutstanding
815 : totalValueOutstanding - managementFeeOutstanding;
816
817 auto const vaultBeforeImpair = env.le(broker.vaultKeylet());
818 BEAST_EXPECT(vaultBeforeImpair);
819 Number const lossBefore = vaultBeforeImpair->at(sfLossUnrealized);
820
821 advancePastDueDate(env, loanKeylet);
822 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
823 env.close();
824
825 auto const vaultAfterImpair = env.le(broker.vaultKeylet());
826 BEAST_EXPECT(vaultAfterImpair);
827 Number const impairDelta = Number(vaultAfterImpair->at(sfLossUnrealized)) - lossBefore;
828
829 env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS));
830 env.close();
831
832 auto const vaultAfterUnimpair = env.le(broker.vaultKeylet());
833 BEAST_EXPECT(vaultAfterUnimpair);
834 Number const netDelta = Number(vaultAfterUnimpair->at(sfLossUnrealized)) - lossBefore;
835
836 return std::make_tuple(expectedExposure, impairDelta, netDelta);
837 };
838
839 for (auto const features : {all_ | featureLendingProtocolV1_1, all_})
840 {
841 testcase(
842 std::string("cash-basis: LoanManage impair/unimpair (") +
843 (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)"));
844 auto const [expectedExposure, impairDelta, netDelta] = runImpairUnimpair(features);
845
846 BEAST_EXPECTS(
847 impairDelta == expectedExposure,
848 "impair must add loanVaultExposure to LossUnrealized; delta=" +
849 to_string(impairDelta) + " expected=" + to_string(expectedExposure));
850 BEAST_EXPECTS(
851 netDelta == beast::kZero,
852 "unimpair must be an exact reversal of impair; net=" + to_string(netDelta));
853 }
854
855 // ---- impair, then default ----
856 auto runDefault = [&](FeatureBitset features) {
857 Env env(*this, features);
858 auto const [broker, loanKeylet, lender, borrower] = setupLoan(env);
859
860 auto const loanBeforeImpair = env.le(loanKeylet);
861 BEAST_EXPECT(loanBeforeImpair);
862 Number const principalOutstanding = loanBeforeImpair->at(sfPrincipalOutstanding);
863 Number const totalValueOutstanding = loanBeforeImpair->at(sfTotalValueOutstanding);
864 Number const managementFeeOutstanding =
865 loanBeforeImpair->at(sfManagementFeeOutstanding);
866
867 Number const expectedExposure =
868 env.current()->rules().enabled(featureLendingProtocolV1_1)
869 ? principalOutstanding
870 : totalValueOutstanding - managementFeeOutstanding;
871
872 advancePastDueDate(env, loanKeylet);
873 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
874 env.close();
875
876 LoanState const state = getCurrentState(env, broker, loanKeylet);
877 env.close(
878 state.startDate + std::chrono::seconds(paymentInterval) +
879 std::chrono::seconds(gracePeriod) + 60s);
880
881 auto const vaultBefore = env.le(broker.vaultKeylet());
882 auto const brokerBefore = env.le(broker.brokerKeylet());
883 BEAST_EXPECT(vaultBefore && brokerBefore);
884 Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
885 Number const debtTotalBefore = brokerBefore->at(sfDebtTotal);
886 Number const lossBefore = vaultBefore->at(sfLossUnrealized);
887 Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable);
888
889 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
890 env.close();
891
892 auto const vaultAfter = env.le(broker.vaultKeylet());
893 auto const brokerAfter = env.le(broker.brokerKeylet());
894 BEAST_EXPECT(vaultAfter && brokerAfter);
895 Number const assetsTotalDelta =
896 Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore;
897 Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore;
898 Number const lossDelta = Number(vaultAfter->at(sfLossUnrealized)) - lossBefore;
899 Number const coverAvailableDelta =
900 Number(brokerAfter->at(sfCoverAvailable)) - coverAvailableBefore;
901
902 Number const defaultCovered = -coverAvailableDelta;
903 Number const vaultDefaultAmount = expectedExposure - defaultCovered;
904
905 return std::make_tuple(
906 expectedExposure, assetsTotalDelta, debtTotalDelta, lossDelta, vaultDefaultAmount);
907 };
908
909 for (auto const features : {all_ | featureLendingProtocolV1_1, all_})
910 {
911 testcase(
912 std::string("cash-basis: LoanManage default (") +
913 (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)"));
914 auto const
915 [expectedExposure,
916 assetsTotalDelta,
917 debtTotalDelta,
918 lossDelta,
919 vaultDefaultAmount] = runDefault(features);
920
921 BEAST_EXPECTS(
922 debtTotalDelta == -expectedExposure,
923 "default must reduce DebtTotal by the unified default amount; delta=" +
924 to_string(debtTotalDelta) + " expected=" + to_string(expectedExposure));
925 BEAST_EXPECTS(
926 lossDelta == -expectedExposure,
927 "default must reverse the earlier impair's LossUnrealized exactly; delta=" +
928 to_string(lossDelta) + " expected=" + to_string(expectedExposure));
929 BEAST_EXPECTS(
930 assetsTotalDelta == -vaultDefaultAmount,
931 "default must reduce AssetsTotal by (defaultAmount - defaultCovered); delta=" +
932 to_string(assetsTotalDelta) + " expected=" + to_string(-vaultDefaultAmount));
933 }
934 }
935
936 // 3b. LEVersion regression: a Vault created before featureLendingProtocolV1_1
937 // activates (LEVersion absent) must keep instant interest recognition
938 // forever, even after the amendment is later enabled -- the switch is
939 // per-Vault (LEVersion == VaultVersion::CashBasis), not a single global amendment
940 // flag.
941 void
943 {
944 testcase(
945 "LEVersion: legacy vault keeps instant interest recognition after amendment enabled");
946
947 using namespace jtx;
948 using namespace loan;
949 using namespace std::chrono_literals;
950
951 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
952 BrokerParameters const brokerParams{
953 .vaultDeposit = 1'000'000,
954 .debtMax = 0,
955 .coverRateMin = TenthBips32{percentageToTenthBips(10)},
956 .coverDeposit = 5'000,
957 .managementFeeRate = TenthBips16{0},
958 .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}};
959
960 Number const principalRequest{10'000};
961 TenthBips32 const interestRate{percentageToTenthBips(12)};
962 std::uint32_t const paymentTotal = 4;
963 std::uint32_t const paymentInterval = 600;
964 std::uint32_t const gracePeriod = 60;
965
966 // Amendment disabled at Vault creation time: LEVersion stays absent.
967 Env env(*this, all_);
968
969 Account const lender{"lender"};
970 Account const borrower{"borrower"};
971 env.fund(XRP(10'000'000), lender, borrower);
972 env.close();
973
974 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
975
976 {
977 auto const vaultSle = env.le(broker.vaultKeylet());
978 BEAST_EXPECT(vaultSle);
979 BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion));
980 }
981
982 // Now enable the amendment -- production dispatch must still treat
983 // this specific Vault as instant interest recognition, since its LEVersion is
984 // (and remains) absent.
985 env.enableFeature(featureLendingProtocolV1_1);
986 env.close();
987
988 LoanParameters const loanParams{
989 .account = borrower,
990 .counter = lender,
991 .principalRequest = principalRequest,
992 .interest = interestRate,
993 .payTotal = paymentTotal,
994 .payInterval = paymentInterval,
995 .gracePd = gracePeriod,
996 };
997
998 auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
999 BEAST_EXPECT(brokerBeforeLoan);
1000 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
1001 auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
1002
1003 // ---- LoanSet origination: instant-recognition formulas expected ----
1004 auto const vaultBeforeSet = env.le(broker.vaultKeylet());
1005 auto const brokerBeforeSet = env.le(broker.brokerKeylet());
1006 BEAST_EXPECT(vaultBeforeSet && brokerBeforeSet);
1007 Number const assetsTotalBeforeSet = vaultBeforeSet->at(sfAssetsTotal);
1008 Number const debtTotalBeforeSet = brokerBeforeSet->at(sfDebtTotal);
1009
1010 env(loanParams(env, broker));
1011 env.close();
1012
1013 auto const loanAfterSet = env.le(loanKeylet);
1014 BEAST_EXPECT(loanAfterSet);
1015 Number const principalOutstanding = loanAfterSet->at(sfPrincipalOutstanding);
1016 Number const totalValueOutstanding = loanAfterSet->at(sfTotalValueOutstanding);
1017 Number const interestDue = totalValueOutstanding - principalOutstanding;
1018 BEAST_EXPECT(interestDue > beast::kZero);
1019
1020 auto const vaultAfterSet = env.le(broker.vaultKeylet());
1021 auto const brokerAfterSet = env.le(broker.brokerKeylet());
1022 BEAST_EXPECT(vaultAfterSet && brokerAfterSet);
1023 Number const assetsTotalDeltaSet =
1024 Number(vaultAfterSet->at(sfAssetsTotal)) - assetsTotalBeforeSet;
1025 Number const debtTotalDeltaSet =
1026 Number(brokerAfterSet->at(sfDebtTotal)) - debtTotalBeforeSet;
1027
1028 BEAST_EXPECTS(
1029 assetsTotalDeltaSet == interestDue,
1030 "legacy vault origination must still add interestDue to AssetsTotal; delta=" +
1031 to_string(assetsTotalDeltaSet) + " interestDue=" + to_string(interestDue));
1032 BEAST_EXPECTS(
1033 debtTotalDeltaSet == principalOutstanding + interestDue,
1034 "legacy vault origination must still add principal+interest to DebtTotal; delta=" +
1035 to_string(debtTotalDeltaSet));
1036
1037 LoanState const state = getCurrentState(env, broker, loanKeylet);
1038 env.close();
1039
1040 // ---- LoanPay: instant-recognition formulas expected ----
1041 auto const vaultBeforePay = env.le(broker.vaultKeylet());
1042 auto const brokerBeforePay = env.le(broker.brokerKeylet());
1043 auto const loanBeforePay = env.le(loanKeylet);
1044 BEAST_EXPECT(vaultBeforePay && brokerBeforePay && loanBeforePay);
1045 Number const totalValueBeforePay = loanBeforePay->at(sfTotalValueOutstanding);
1046 Number const assetsTotalBeforePay = vaultBeforePay->at(sfAssetsTotal);
1047 Number const debtTotalBeforePay = brokerBeforePay->at(sfDebtTotal);
1048
1049 STAmount const paymentAmount{
1050 xrpAsset, roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale)};
1051 env(pay(borrower, loanKeylet.key, paymentAmount), Ter(tesSUCCESS));
1052 env.close();
1053
1054 auto const vaultAfterPay = env.le(broker.vaultKeylet());
1055 auto const brokerAfterPay = env.le(broker.brokerKeylet());
1056 auto const loanAfterPay = env.le(loanKeylet);
1057 BEAST_EXPECT(vaultAfterPay && brokerAfterPay && loanAfterPay);
1058 Number const totalValueAfterPay = loanAfterPay->at(sfTotalValueOutstanding);
1059 Number const assetsTotalDeltaPay =
1060 Number(vaultAfterPay->at(sfAssetsTotal)) - assetsTotalBeforePay;
1061 Number const debtTotalDeltaPay =
1062 Number(brokerAfterPay->at(sfDebtTotal)) - debtTotalBeforePay;
1063 Number const totalValueDeltaPay = totalValueAfterPay - totalValueBeforePay;
1064
1065 // A regular, on-time payment has valueChange == 0, so instant-recognition
1066 // AssetsTotal is untouched and DebtTotal mirrors TotalValueOutstanding.
1067 BEAST_EXPECTS(
1068 assetsTotalDeltaPay == beast::kZero,
1069 "legacy vault regular payment must not change AssetsTotal; delta=" +
1070 to_string(assetsTotalDeltaPay));
1071 BEAST_EXPECTS(
1072 debtTotalDeltaPay == totalValueDeltaPay,
1073 "legacy vault DebtTotal delta must mirror TotalValueOutstanding delta; "
1074 "debtTotalDelta=" +
1075 to_string(debtTotalDeltaPay) + " totalValueDelta=" + to_string(totalValueDeltaPay));
1076
1077 // ---- LoanManage: impair, then default -- instant-recognition exposure expected ----
1078 auto const loanBeforeImpair = env.le(loanKeylet);
1079 BEAST_EXPECT(loanBeforeImpair);
1080 Number const totalValueBeforeImpair = loanBeforeImpair->at(sfTotalValueOutstanding);
1081 Number const managementFeeBeforeImpair = loanBeforeImpair->at(sfManagementFeeOutstanding);
1082 Number const expectedExposure = totalValueBeforeImpair - managementFeeBeforeImpair;
1083
1084 // With fixCleanup3_4_0, impairment is only allowed once the
1085 // payment is late. After the earlier LoanPay the due date advanced by
1086 // one interval, so use the current due date rather than startDate.
1087 std::uint32_t const dueDateBeforeImpair = loanBeforeImpair->at(sfNextPaymentDueDate);
1088 env.close(NetClock::time_point{NetClock::duration{dueDateBeforeImpair}} + 1s);
1089
1090 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
1091 env.close();
1092
1093 env.close(
1094 NetClock::time_point{NetClock::duration{dueDateBeforeImpair}} +
1095 std::chrono::seconds(gracePeriod) + 60s);
1096
1097 auto const vaultBeforeDefault = env.le(broker.vaultKeylet());
1098 auto const brokerBeforeDefault = env.le(broker.brokerKeylet());
1099 BEAST_EXPECT(vaultBeforeDefault && brokerBeforeDefault);
1100 Number const debtTotalBeforeDefault = brokerBeforeDefault->at(sfDebtTotal);
1101 Number const lossBeforeDefault = vaultBeforeDefault->at(sfLossUnrealized);
1102
1103 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
1104 env.close();
1105
1106 auto const vaultAfterDefault = env.le(broker.vaultKeylet());
1107 auto const brokerAfterDefault = env.le(broker.brokerKeylet());
1108 BEAST_EXPECT(vaultAfterDefault && brokerAfterDefault);
1109 Number const debtTotalDeltaDefault =
1110 Number(brokerAfterDefault->at(sfDebtTotal)) - debtTotalBeforeDefault;
1111 Number const lossDeltaDefault =
1112 Number(vaultAfterDefault->at(sfLossUnrealized)) - lossBeforeDefault;
1113
1114 BEAST_EXPECTS(
1115 debtTotalDeltaDefault == -expectedExposure,
1116 "legacy vault default must reduce DebtTotal by instant-recognition exposure; delta=" +
1117 to_string(debtTotalDeltaDefault) + " expected=" + to_string(expectedExposure));
1118 BEAST_EXPECTS(
1119 lossDeltaDefault == -expectedExposure,
1120 "legacy vault default must reverse the earlier impair's LossUnrealized exactly; "
1121 "delta=" +
1122 to_string(lossDeltaDefault) + " expected=" + to_string(expectedExposure));
1123
1124 // Confirm the Vault's LEVersion truly never got set, throughout.
1125 {
1126 auto const vaultSle = env.le(broker.vaultKeylet());
1127 BEAST_EXPECT(vaultSle);
1128 BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion));
1129 BEAST_EXPECT(getVaultVersion(vaultSle) == VaultVersion::Legacy);
1130 }
1131 }
1132
1133 // 4. End-to-end trajectory: LoanSet -> 2 LoanPays -> LoanManage(default),
1134 // entirely under the amendment, with independently hand-computed
1135 // expected AssetsTotal/DebtTotal/LossUnrealized/CoverAvailable values at
1136 // each step. 0% interest keeps the arithmetic exact and tractable; the
1137 // divergence from instant interest recognition is already covered directly by
1138 // testCashBasisLoanSetOrigination/LoanPay/LoanManage above, so this test
1139 // focuses purely on an independent, from-scratch trajectory check.
1140 void
1142 {
1143 testcase("cash-basis: end-to-end trajectory");
1144
1145 using namespace jtx;
1146 using namespace loan;
1147 using namespace std::chrono_literals;
1148
1149 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
1150 BrokerParameters const brokerParams{
1151 .vaultDeposit = 100'000, .managementFeeRate = TenthBips16{0}};
1152
1153 Env env(*this, all_ | featureLendingProtocolV1_1);
1154
1155 Account const lender{"lender"};
1156 Account const borrower{"borrower"};
1157 env.fund(XRP(10'000'000), lender, borrower);
1158 env.close();
1159
1160 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
1161
1162 // Hand computation (all values in XRP, drops == 1e-6 XRP):
1163 // Vault: AssetsTotal starts at 100'000 (the deposit).
1164 // Broker: DebtTotal starts at 0, CoverAvailable starts at 1'000
1165 // (BrokerParameters::defaults().coverDeposit).
1166 auto const vaultKeylet = broker.vaultKeylet();
1167 auto const brokerKeylet = broker.brokerKeylet();
1168
1169 // All the "human XRP unit" constants below (e.g. `100'000`) are
1170 // converted to raw native (drops) values via xrpAsset(...), since
1171 // that's how the ledger fields are actually denominated.
1172 auto const checkVaultBroker = [&](Number const& assetsTotalUnits,
1173 Number const& debtTotalUnits,
1174 Number const& lossUnrealizedUnits,
1175 Number const& coverAvailableUnits,
1176 char const* step) {
1177 Number const assetsTotal = xrpAsset(assetsTotalUnits).value();
1178 Number const debtTotal = xrpAsset(debtTotalUnits).value();
1179 Number const lossUnrealized = xrpAsset(lossUnrealizedUnits).value();
1180 Number const coverAvailable = xrpAsset(coverAvailableUnits).value();
1181
1182 auto const vaultSle = env.le(vaultKeylet);
1183 auto const brokerSle = env.le(brokerKeylet);
1184 BEAST_EXPECT(vaultSle && brokerSle);
1185 BEAST_EXPECTS(
1186 vaultSle->at(sfAssetsTotal) == assetsTotal,
1187 std::string(step) + ": AssetsTotal expected " + to_string(assetsTotal) + " got " +
1188 to_string(Number(vaultSle->at(sfAssetsTotal))));
1189 BEAST_EXPECTS(
1190 brokerSle->at(sfDebtTotal) == debtTotal,
1191 std::string(step) + ": DebtTotal expected " + to_string(debtTotal) + " got " +
1192 to_string(Number(brokerSle->at(sfDebtTotal))));
1193 BEAST_EXPECTS(
1194 vaultSle->at(sfLossUnrealized) == lossUnrealized,
1195 std::string(step) + ": LossUnrealized expected " + to_string(lossUnrealized) +
1196 " got " + to_string(Number(vaultSle->at(sfLossUnrealized))));
1197 BEAST_EXPECTS(
1198 brokerSle->at(sfCoverAvailable) == coverAvailable,
1199 std::string(step) + ": CoverAvailable expected " + to_string(coverAvailable) +
1200 " got " + to_string(Number(brokerSle->at(sfCoverAvailable))));
1201 };
1202
1203 checkVaultBroker(100'000, 0, 0, 1'000, "before LoanSet");
1204
1205 // Loan: principal=1200, 0% interest, 12 payments of 100 each, no fees.
1206 Number const principalRequest{1'200};
1207 std::uint32_t const paymentTotal = 12;
1208 std::uint32_t const paymentInterval = 600;
1209 std::uint32_t const gracePeriod = 60;
1210
1211 auto const brokerBeforeLoan = env.le(brokerKeylet);
1212 BEAST_EXPECT(brokerBeforeLoan);
1213 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
1214 auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
1215
1216 LoanParameters const loanParams{
1217 .account = borrower,
1218 .counter = lender,
1219 .principalRequest = principalRequest,
1220 .interest = TenthBips32{0},
1221 .payTotal = paymentTotal,
1222 .payInterval = paymentInterval,
1223 .gracePd = gracePeriod,
1224 };
1225 env(loanParams(env, broker));
1226 env.close();
1227
1228 // Origination (cash-basis): AssetsTotal += 0, DebtTotal += principal.
1229 checkVaultBroker(100'000, 1'200, 0, 1'000, "after LoanSet");
1230
1231 LoanState const state = getCurrentState(env, broker, loanKeylet);
1232 BEAST_EXPECT(state.periodicPayment == xrpAsset(100).value());
1233
1234 // Payment 1: principalPaid=100, interestPaid=0.
1235 // AssetsTotal += 0; DebtTotal -= 100.
1236 env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS));
1237 env.close();
1238 checkVaultBroker(100'000, 1'100, 0, 1'000, "after payment 1");
1239
1240 // Payment 2: same as above.
1241 env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS));
1242 env.close();
1243 checkVaultBroker(100'000, 1'000, 0, 1'000, "after payment 2");
1244
1245 // Default (no impair): principalOutstanding remaining is 1'000.
1246 // totalDefaultAmount (cash-basis) = PrincipalOutstanding = 1'000.
1247 // minimumCover = DebtTotal(1'000) * coverRateMin(10%) = 100.
1248 // covered = min(minimumCover * coverRateLiquidation(25%), totalDefaultAmount)
1249 // = min(25, 1'000) = 25.
1250 // defaultCovered = min(covered, CoverAvailable(1'000)) = 25.
1251 // vaultDefaultAmount = 1'000 - 25 = 975.
1252 // DebtTotal -= 1'000 -> 0. CoverAvailable -= 25 -> 975.
1253 // AssetsTotal -= 975 -> 99'025. LossUnrealized unaffected (never impaired).
1254 auto const loanBeforeDefault = env.le(loanKeylet);
1255 BEAST_EXPECT(loanBeforeDefault);
1256 BEAST_EXPECT(
1257 Number(loanBeforeDefault->at(sfPrincipalOutstanding)) == xrpAsset(1'000).value());
1258
1259 env.close(state.startDate + std::chrono::seconds((3 * paymentInterval) + gracePeriod) + 1s);
1260
1261 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
1262 env.close();
1263
1264 checkVaultBroker(99'025, 0, 0, 975, "after LoanManage(default)");
1265 }
1266
1267public:
1268 void
1279};
1280
1281BEAST_DEFINE_TESTSUITE(LoanCashBasis, tx, xrpl);
1282
1283} // 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
std::chrono::duration< rep, period > duration
Definition chrono.h:47
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.
Shared base for the Loan*_test family under src/test/app/lending/.
jtx::PrettyAsset createFundedIouAsset(jtx::Env &env, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, Number const &lenderPay=100 '000 '000, Number const &borrowerPay=1 '000 '000)
FeatureBitset const all_
void advancePastDueDate(jtx::Env &env, Keylet const &loanKeylet)
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< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:538
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:603
json::Value set(AccountID const &account, UInt256 const &vaultId, uint32_t flags)
std::vector< Credential > Credentials
UInt256 getNewDomain(std::shared_ptr< STObject const > const &meta)
json::Value setTx(AccountID const &account, Credentials const &credentials, std::optional< UInt256 > domain)
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:127
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:657
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:654
@ tecINVARIANT_FAILED
Definition TER.h:321
@ tecLIMIT_EXCEEDED
Definition TER.h:369
VaultVersion getVaultVersion(SLE::ConstRef vault)
Resolves a Vault's LEVersion, the single point every accounting touch point should call to determine ...
@ tesSUCCESS
Definition TER.h:250
std::optional< std::uint32_t > vaultFlags