xrpld
Loading...
Searching...
No Matches
LoanSecurity_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/noop.h>
8#include <test/jtx/sponsor.h>
9#include <test/jtx/ter.h>
10#include <test/jtx/txflags.h>
11#include <test/jtx/vault.h>
12
13#include <xrpl/basics/Number.h>
14#include <xrpl/basics/chrono.h>
15#include <xrpl/beast/unit_test/suite.h>
16#include <xrpl/beast/utility/Journal.h>
17#include <xrpl/beast/utility/Zero.h>
18#include <xrpl/core/ServiceRegistry.h>
19#include <xrpl/json/to_string.h>
20#include <xrpl/ledger/OpenView.h>
21#include <xrpl/ledger/helpers/LendingHelpers.h>
22#include <xrpl/protocol/Feature.h>
23#include <xrpl/protocol/Indexes.h>
24#include <xrpl/protocol/Issue.h>
25#include <xrpl/protocol/Keylet.h>
26#include <xrpl/protocol/LedgerFormats.h>
27#include <xrpl/protocol/Protocol.h>
28#include <xrpl/protocol/SField.h>
29#include <xrpl/protocol/STAmount.h>
30#include <xrpl/protocol/SeqProxy.h>
31#include <xrpl/protocol/TER.h>
32#include <xrpl/protocol/TxFlags.h>
33#include <xrpl/protocol/Units.h>
34#include <xrpl/protocol/jss.h>
35
36#include <algorithm>
37#include <cstdint>
38#include <memory>
39#include <ostream>
40
41namespace xrpl::test {
42
44{
45private:
46 // Env::close() cannot land the ledger's parentCloseTime on an arbitrary
47 // instant: it always rounds the requested time forward to the next
48 // close-time-resolution boundary (see Env::close() and
49 // roundCloseTime()/effCloseTime() in LedgerTiming.h), so it can only be
50 // used to reach times strictly *after* a given due date, never exactly
51 // on it. To pin the exact-boundary behavior of isPaymentLate(), directly
52 // overwrite the loan's NextPaymentDueDate instead, without closing the
53 // ledger again.
54 void
55 setLoanNextPaymentDueDate(jtx::Env& env, Keylet const& loanKeylet, std::uint32_t dueDate)
56 {
57 using namespace jtx;
58 bool const ok = env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
59 auto const sle = view.read(loanKeylet);
60 if (!sle)
61 return false;
62 auto replacement = std::make_shared<SLE>(*sle);
63 (*replacement)[sfNextPaymentDueDate] = dueDate;
64 view.rawReplace(replacement);
65 return true;
66 });
67 BEAST_EXPECT(ok);
68 }
69
70 void
72 {
73 // --- PoC Summary ----------------------------------------------------
74 // Scenario: Borrower makes one periodic payment early (before next due)
75 // so doPayment sets sfPreviousPaymentDueDate to the (future)
76 // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one
77 // interval. Borrower then immediately performs a full-payment
78 // (tfLoanFullPayment). Why it matters: Full-payment interest accrual
79 // uses
80 // delta = now - max(prevPaymentDate, startDate)
81 // with an unsigned clock representation (uint32). If prevPaymentDate is
82 // in the future, the subtraction underflows to a very large positive
83 // number. This inflates roundedFullInterest and total full-close due,
84 // and LoanPay applies the inflated valueChange to the vault
85 // (sfAssetsTotal), increasing NAV.
86 // --------------------------------------------------------------------
87 testcase("PoC: Unsigned-underflow full-pay accrual after early periodic");
88
89 using namespace jtx;
90 using namespace loan;
91 using namespace std::chrono_literals;
92
93 Env env{*this, features};
94
95 Account const lender{"poc_lender4"};
96 Account const borrower{"poc_borrower4"};
97 env.fund(XRP(3'000'000), lender, borrower);
98 env.close();
99
100 PrettyAsset const asset{xrpIssue(), 1'000'000};
101 BrokerParameters const brokerParams{};
102 auto const broker = createVaultAndBroker(env, asset, lender, brokerParams);
103
104 // Create a 3-payment loan so full-payment path is enabled after 1
105 // periodic payment.
106 auto const loanSetFee = Fee(env.current()->fees().base * 2);
107 Number const principalRequest = asset(1000).value();
108 auto const originationFee = asset(0).value();
109 auto const serviceFee = asset(1).value();
110 auto const serviceFeePA = asset(1);
111 auto const lateFee = asset(0).value();
112 auto const closeFee = asset(0).value();
113 auto const interest = percentageToTenthBips(12);
114 auto const lateInterest = percentageToTenthBips(12) / 10;
115 auto const closeInterest = percentageToTenthBips(12) / 10;
116 auto const overpaymentInterest = percentageToTenthBips(12) / 10;
117 auto const total = 3u;
118 auto const interval = 600u;
119 auto const grace = 60u;
120
121 auto createJtx = env.jt(
122 set(borrower, broker.brokerID, principalRequest, 0),
123 Sig(sfCounterpartySignature, lender),
124 kLoanOriginationFee(originationFee),
125 kLoanServiceFee(serviceFee),
126 kLatePaymentFee(lateFee),
127 kClosePaymentFee(closeFee),
128 kOverpaymentFee(percentageToTenthBips(5) / 10),
129 kInterestRate(interest),
130 kLateInterestRate(lateInterest),
131 kCloseInterestRate(closeInterest),
132 kOverpaymentInterestRate(overpaymentInterest),
133 kPaymentTotal(total),
134 kPaymentInterval(interval),
135 kGracePeriod(grace),
136 Fee(loanSetFee));
137
138 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
139 BEAST_EXPECT(brokerSle);
140 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
141 auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
142
143 env(createJtx);
144 env.close();
145
146 // Compute a regular periodic due and pay it early (before next due).
147 auto state = getCurrentState(env, broker, loanKeylet);
148 Number const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval);
149 auto const components = xrpl::detail::computePaymentComponents(
150 env.current()->rules(),
151 asset.raw(),
152 state.loanScale,
153 state.totalValue,
154 state.principalOutstanding,
155 state.managementFeeOutstanding,
156 state.periodicPayment,
157 periodicRate,
158 state.paymentRemaining,
159 brokerParams.managementFeeRate);
160 STAmount const regularDue{asset, components.trackedValueDelta + serviceFeePA.number()};
161 // now < nextDue immediately after creation, so this is an early pay.
162 env(pay(borrower, loanKeylet.key, regularDue));
163 env.close();
164
165 // Immediately attempt a full payoff. Compute the exact full-payment
166 // due to ensure the tx applies.
167 auto after = getCurrentState(env, broker, loanKeylet);
168 auto const loanSle = env.le(loanKeylet);
169 BEAST_EXPECT(loanSle);
170 auto const brokerSle2 = env.le(keylet::loanBroker(broker.brokerID));
171 BEAST_EXPECT(brokerSle2);
172
173 auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) : Number{};
174 auto const closeInterestRate =
175 loanSle ? TenthBips32{loanSle->at(sfCloseInterestRate)} : TenthBips32{};
176 auto const managementFeeRate =
177 brokerSle2 ? TenthBips16{brokerSle2->at(sfManagementFeeRate)} : TenthBips16{};
178
179 Number const periodicRate2 = loanPeriodicRate(after.interestRate, after.paymentInterval);
180 // Accrued + prepayment-penalty interest based on current periodic
181 // schedule
182 auto const fullPaymentInterest = computeFullPaymentInterest(
184 env.current()->rules(),
185 after.periodicPayment,
186 periodicRate2,
187 after.paymentRemaining),
188 periodicRate2,
189 env.current()->parentCloseTime(),
190 after.paymentInterval,
191 after.previousPaymentDate,
192 static_cast<std::uint32_t>(after.startDate.time_since_epoch().count()),
193 closeInterestRate);
194
195 // Round to asset scale and split interest/fee parts
196 auto const roundedInterest =
197 roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale);
198 Number const roundedFullMgmtFee =
199 computeManagementFee(asset.raw(), roundedInterest, managementFeeRate, after.loanScale);
200 Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee;
201
202 // Show both signed and unsigned deltas to highlight the underflow.
203 auto const nowSecs =
204 static_cast<std::uint32_t>(env.current()->parentCloseTime().time_since_epoch().count());
205 auto const startSecs =
206 static_cast<std::uint32_t>(after.startDate.time_since_epoch().count());
207 auto const lastPaymentDate = std::max(after.previousPaymentDate, startSecs);
208 auto const signedDelta =
209 static_cast<std::int64_t>(nowSecs) - static_cast<std::int64_t>(lastPaymentDate);
210 auto const unsignedDelta = static_cast<std::uint32_t>(nowSecs - lastPaymentDate);
211 log << "PoC window: prev=" << after.previousPaymentDate << " start=" << startSecs
212 << " now=" << nowSecs << " signedDelta=" << signedDelta
213 << " unsignedDelta=" << unsignedDelta << std::endl;
214
215 // Reference (clamped) computation: emulate a non-negative accrual
216 // window by clamping prevPaymentDate to 'now' for the full-pay path.
217 auto const prevClamped = std::min(after.previousPaymentDate, nowSecs);
218 auto const fullPaymentInterestClamped = computeFullPaymentInterest(
220 env.current()->rules(),
221 after.periodicPayment,
222 periodicRate2,
223 after.paymentRemaining),
224 periodicRate2,
225 env.current()->parentCloseTime(),
226 after.paymentInterval,
227 prevClamped,
228 startSecs,
229 closeInterestRate);
230 auto const roundedInterestClamped =
231 roundToAsset(asset.raw(), fullPaymentInterestClamped, after.loanScale);
232 Number const roundedFullMgmtFeeClamped = computeManagementFee(
233 asset.raw(), roundedInterestClamped, managementFeeRate, after.loanScale);
234 Number const roundedFullInterestClamped =
235 roundedInterestClamped - roundedFullMgmtFeeClamped;
236 STAmount const fullDueClamped{
237 asset,
238 after.principalOutstanding + roundedFullInterestClamped + roundedFullMgmtFeeClamped +
239 closePaymentFee};
240
241 // Collect vault NAV before closing payment
242 auto const vaultId2 = brokerSle2 ? brokerSle2->at(sfVaultID) : UInt256{};
243 auto const vaultKey2 = keylet::vault(vaultId2);
244 auto const vaultBefore = env.le(vaultKey2);
245 BEAST_EXPECT(vaultBefore);
246 Number const assetsTotalBefore = vaultBefore ? vaultBefore->at(sfAssetsTotal) : Number{};
247
248 STAmount const fullDue{
249 asset,
250 after.principalOutstanding + roundedFullInterest + roundedFullMgmtFee +
251 closePaymentFee};
252
253 log << "PoC payoff: principalOutstanding=" << after.principalOutstanding
254 << " roundedFullInterest=" << roundedFullInterest
255 << " roundedFullMgmtFee=" << roundedFullMgmtFee << " closeFee=" << closePaymentFee
256 << " fullDue=" << to_string(fullDue.getJson()) << std::endl;
257 log << "PoC reference (clamped): roundedFullInterestClamped=" << roundedFullInterestClamped
258 << " roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped
259 << " fullDueClamped=" << to_string(fullDueClamped.getJson()) << std::endl;
260
261 env(pay(borrower, loanKeylet.key, fullDue), Txflags(tfLoanFullPayment));
262 env.close();
263
264 // Sanity: underflow present (unsigned delta very large relative to
265 // interval)
266 BEAST_EXPECT(unsignedDelta > after.paymentInterval);
267
268 // Compare vault NAV before/after the full close
269 auto const vaultAfter = env.le(vaultKey2);
270 BEAST_EXPECT(vaultAfter);
271 if (vaultAfter)
272 {
273 auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal);
274 log << "PoC NAV: assetsTotalBefore=" << assetsTotalBefore
275 << " assetsTotalAfter=" << assetsTotalAfter
276 << " delta=" << (assetsTotalAfter - assetsTotalBefore) << std::endl;
277
278 // Regression check: the underflowed window must be clamped so the
279 // payoff matches the non-underflow reference, i.e. no overcharge.
280 BEAST_EXPECT(fullDue == fullDueClamped);
281 if (fullDue != fullDueClamped)
282 log << "PoC delta: overcharge (fullDue > clamped)" << std::endl;
283 }
284
285 // Loan should be paid off
286 auto const finalLoan = env.le(loanKeylet);
287 BEAST_EXPECT(finalLoan);
288 if (finalLoan)
289 {
290 BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0);
291 BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0);
292 }
293 }
294
295 void
297 {
298 using namespace jtx;
299
300 testcase("RIPD-3831");
301
302 Account const issuer("issuer");
303 Account const lender("lender");
304 Account const borrower("borrower");
305
306 BrokerParameters const brokerParams{
307 .vaultDeposit = 100000,
308 .debtMax = 0,
309 .coverRateMin = TenthBips32{0},
310 // .managementFeeRate = TenthBips16{5919},
311 .coverRateLiquidation = TenthBips32{0}};
312 LoanParameters const loanParams{
313 .account = lender,
314 .counter = borrower,
315 .principalRequest = Number{200'000, -6},
316 .lateFee = Number{200, -6},
317 .interest = TenthBips32{50'000},
318 .payTotal = 10,
319 .payInterval = 150};
320
321 auto const assetType = AssetType::XRP;
322
323 Env env{*this, features};
324
325 auto loanResult =
326 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
327
328 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
329 return;
330
331 auto broker = std::get<BrokerInfo>(*loanResult);
332 auto loanKeylet = std::get<Keylet>(*loanResult);
333
334 using Tp = NetClock::time_point;
335 using D = NetClock::duration;
336
337 auto state = getCurrentState(env, broker, loanKeylet);
338 if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
339 {
340 env.close(Tp{D{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
341 }
342
343 topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee);
344
345 using namespace jtx::loan;
346
347 auto jv = pay(borrower, loanKeylet.key, drops(XRPAmount(state.totalValue)));
348
349 {
350 auto const submitParam = to_string(jv);
351 auto const jr = env.rpc("submit", borrower.name(), submitParam);
352
353 BEAST_EXPECT(jr.isMember(jss::result));
354 }
355
356 env.close();
357
358 // Make sure the system keeps responding
359 env(noop(borrower));
360 env.close();
361 env(noop(issuer));
362 env.close();
363 env(noop(lender));
364 env.close();
365 }
366
367 void
369 {
370 testcase("RIPD-3459 - LoanBroker incorrect debt total");
371
372 using namespace jtx;
373
374 Account const issuer("issuer");
375 Account const lender("lender");
376 Account const borrower("borrower");
377
378 BrokerParameters const brokerParams{
379 .vaultDeposit = 200'000,
380 .debtMax = 0,
381 .coverRateMin = TenthBips32{0},
382 .managementFeeRate = TenthBips16{500},
383 .coverRateLiquidation = TenthBips32{0}};
384 LoanParameters const loanParams{
385 .account = lender,
386 .counter = borrower,
387 .principalRequest = Number{100'000, -4},
388 .interest = TenthBips32{100'000},
389 .payTotal = 10};
390
391 auto const assetType = AssetType::MPT;
392
393 Env env{*this, features};
394
395 auto loanResult =
396 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
397
398 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
399 return;
400
401 auto broker = std::get<BrokerInfo>(*loanResult);
402 auto loanKeylet = std::get<Keylet>(*loanResult);
403 auto pseudoAcct = std::get<Account>(*loanResult);
404
405 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
406
407 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
408 {
409 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
410 {
411 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding));
412 }
413 }
414
416 env,
417 broker,
418 loanParams,
419 loanKeylet,
420 verifyLoanStatus,
421 issuer,
422 lender,
423 borrower,
424 PaymentParameters{.showStepBalances = true});
425
426 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
427 {
428 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
429 {
430 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding));
431 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::kZero);
432 }
433 }
434 }
435
436 void
438 {
439 testcase("Crash with tfLoanOverpayment");
440 using namespace jtx;
441 using namespace loan;
442 Account const lender{"lender"};
443 Account const issuer{"issuer"};
444 Account const borrower{"borrower"};
445 Account const depositor{"depositor"};
446 auto const txFee = Fee(XRP(100));
447
448 // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
449 // accepts closed-ended vaults, so build one and advance past
450 // SubscriptionDate before creating the broker and the loan.
451 Env env(*this);
452 Vault const vault(env);
453
454 env.fund(XRP(10'000), lender, issuer, borrower, depositor);
455 env.close();
456
457 auto [tx, vaultKeyLet, subscriptionDate] =
458 vault.createClosedEnded({.owner = lender, .asset = xrpIssue()});
459 env(tx, txFee);
460 env.close();
461
462 env(vault.deposit({.depositor = depositor, .id = vaultKeyLet.key, .amount = XRP(1'000)}),
463 txFee);
464 env.close();
465
466 // Move into the Investment phase before creating the broker and
467 // the loan.
468 vault.closePastSubscription(subscriptionDate);
469
470 auto const brokerKeyLet =
471 keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
472
473 env(loan_broker::set(lender, vaultKeyLet.key), txFee);
474 env.close();
475
476 STAmount const debtMaximumRequest = XRPAmount(200'000);
477
478 env(set(borrower, brokerKeyLet.key, debtMaximumRequest),
479 Sig(sfCounterpartySignature, lender),
480 kInterestRate(TenthBips32(50'000)),
481 kPaymentTotal(2),
482 kPaymentInterval(150),
483 Txflags(tfLoanOverpayment),
484 txFee);
485 env.close();
486
487 std::uint32_t const loanSequence = 1;
488 auto const loanKeylet = keylet::loan(brokerKeyLet.key, SeqProxy::rawSequence(loanSequence));
489
490 if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan))
491 {
492 env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)),
493 Txflags(tfLoanOverpayment),
494 txFee);
495 env.close();
496 }
497 }
498
499 void
501 {
502 testcase("RIPD-3902 - 1 IOU loan payments");
503
504 using namespace jtx;
505
506 Account const issuer("issuer");
507 Account const lender("lender");
508 Account const borrower("borrower");
509
510 BrokerParameters const brokerParams{
511 .vaultDeposit = 10,
512 .debtMax = 0,
513 .coverRateMin = TenthBips32{0},
514 .managementFeeRate = TenthBips16{0},
515 .coverRateLiquidation = TenthBips32{0}};
516 LoanParameters const loanParams{
517 .account = lender,
518 .counter = borrower,
519 .principalRequest = Number{1, 0},
520 .interest = TenthBips32{100'000},
521 .payTotal = 5,
522 .payInterval = 150,
523 .gracePd = 60};
524
525 auto const assetType = AssetType::IOU;
526
527 Env env{*this, features};
528
529 auto loanResult =
530 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
531
532 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
533 return;
534
535 auto broker = std::get<BrokerInfo>(*loanResult);
536 auto loanKeylet = std::get<Keylet>(*loanResult);
537 auto pseudoAcct = std::get<Account>(*loanResult);
538
539 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
540
542 env,
543 broker,
544 loanParams,
545 loanKeylet,
546 verifyLoanStatus,
547 issuer,
548 lender,
549 borrower,
550 PaymentParameters{.showStepBalances = true});
551 }
552
553 // Verify that with fixCleanup3_4_0:
554 // 1. A loan cannot be impaired before its payment is late.
555 // 2. Impairing a late loan does not change sfNextPaymentDueDate.
556 // 3. The unimpair operation does not change sfNextPaymentDueDate.
557 void
559 {
560 using namespace jtx;
561 using namespace loan;
562 using namespace std::chrono_literals;
563
564 testcase("Impairment does not change payment due date");
565
566 Env env(*this, all_ | fixCleanup3_4_0);
567 BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
568
569 Account const lender{"lender"};
570 Account const borrower{"borrower"};
571
572 env.fund(XRP(100'000'000), lender, borrower);
573 env.close();
574
575 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
576 auto const broker = createVaultAndBroker(env, xrpAsset, lender);
577
578 auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
579 if (!BEAST_EXPECT(sleBroker))
580 return;
581 auto const loanKeylet =
582 keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
583
584 Number const principalRequest{1, 3};
585 env(set(borrower, broker.brokerID, broker.asset(principalRequest).value()),
586 Sig(sfCounterpartySignature, lender),
587 kPaymentTotal(12),
588 kPaymentInterval(600),
589 Fee(env.current()->fees().base * 2));
590 env.close();
591
592 auto const loanSle = env.le(loanKeylet);
593 if (!BEAST_EXPECT(loanSle))
594 return;
595 std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
596 BEAST_EXPECT(originalNextDueDate > 0);
597
598 // 1. Impairment must fail when payment is not yet late
599 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecTOO_SOON));
600
601 {
602 auto const loan = env.le(loanKeylet);
603 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
604 }
605
606 // 1b. Impairment must still fail at the exact due date instant: a
607 // payment due "now" is not yet late (strict/Exclusive comparison).
608 // Temporarily set NextPaymentDueDate to exactly the current
609 // parentCloseTime (without closing the ledger again), exercise the
610 // check, then restore the original due date.
611 {
612 std::uint32_t const exactNow =
613 env.current()->parentCloseTime().time_since_epoch().count();
614 setLoanNextPaymentDueDate(env, loanKeylet, exactNow);
615
616 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecTOO_SOON));
617
618 setLoanNextPaymentDueDate(env, loanKeylet, originalNextDueDate);
619 }
620
621 {
622 auto const loan = env.le(loanKeylet);
623 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
624 }
625
626 env.close(NetClock::time_point{NetClock::duration{originalNextDueDate}} + 1s);
627
628 // 2. Impairment succeeds when payment is late
629 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
630
631 {
632 auto const loan = env.le(loanKeylet);
633 if (!BEAST_EXPECT(loan))
634 return;
635 BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
636 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
637 }
638
639 // 3. Unimpair also does not change sfNextPaymentDueDate
640 env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS));
641
642 {
643 auto const loan = env.le(loanKeylet);
644 if (!BEAST_EXPECT(loan))
645 return;
646 BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
647 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
648 }
649 }
650
651 // Verify that without fixCleanup3_4_0, the pre-amendment
652 // impair/unimpair behaviour is preserved:
653 // 1. Impairing a loan before its payment is late moves
654 // sfNextPaymentDueDate to "now".
655 // 2a. Unimpair within the original payment interval restores
656 // sfNextPaymentDueDate to StartDate + PaymentInterval.
657 // 2b. Unimpair after the original due date sets
658 // sfNextPaymentDueDate to now + PaymentInterval.
659 void
661 {
662 using namespace jtx;
663 using namespace loan;
664 using namespace std::chrono_literals;
665
666 testcase("Pre-amendment impair/unimpair date restoration");
667
668 Env env(*this, all_ - fixCleanup3_4_0);
669 BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
670
671 Account const lender{"lender"};
672 Account const borrower{"borrower"};
673
674 env.fund(XRP(100'000'000), lender, borrower);
675 env.close();
676
677 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
678 auto const broker = createVaultAndBroker(env, xrpAsset, lender);
679
680 Number const principalRequest{1, 3};
681 auto createNewLoan = [&]() {
682 auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
683 if (!BEAST_EXPECT(sleBroker))
684 return keylet::loan(UInt256{});
685 auto const lk =
686 keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
687 env(set(borrower, broker.brokerID, broker.asset(principalRequest).value()),
688 Sig(sfCounterpartySignature, lender),
689 kPaymentTotal(12),
690 kPaymentInterval(600),
691 Fee(env.current()->fees().base * 2));
692 env.close();
693 return lk;
694 };
695
696 // Default + delete a loan and replenish first-loss capital so the
697 // broker is ready for the next loan.
698 auto cleanupLoan = [&](Keylet const& loanKeylet, std::uint32_t dueDate) {
699 env.close(NetClock::time_point{NetClock::duration{dueDate + 60}} + 1s);
700 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
701 env.close();
702
703 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
704 if (!BEAST_EXPECT(brokerSle))
705 return;
706 auto const coverNeeded =
707 broker.asset(broker.params.coverDeposit).value() - brokerSle->at(sfCoverAvailable);
708 if (coverNeeded > 0)
709 {
711 lender, broker.brokerID, STAmount{broker.asset, coverNeeded}));
712 env.close();
713 }
714 env(del(lender, loanKeylet.key));
715 env.close();
716 };
717
718 // ---- Case A: impair before late, unimpair within original interval ----
719 {
720 auto const loanKeylet = createNewLoan();
721 auto const loanSle = env.le(loanKeylet);
722 if (!BEAST_EXPECT(loanSle))
723 return;
724 std::uint32_t const startDate = loanSle->at(sfStartDate);
725 std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
726 BEAST_EXPECT(originalNextDueDate == startDate + 600);
727
728 // Payment is not late yet - impair succeeds and moves due date
729 // to now (pre-amendment allows immediate impairment)
730 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
731
732 {
733 auto const loan = env.le(loanKeylet);
734 if (!BEAST_EXPECT(loan))
735 return;
736 BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
737 std::uint32_t const movedDueDate = loan->at(sfNextPaymentDueDate);
738 BEAST_EXPECT(movedDueDate != originalNextDueDate);
739 BEAST_EXPECT(movedDueDate < originalNextDueDate);
740 }
741
742 // Unimpair while still within the original payment interval. The
743 // normal due date (startDate + 600) has not yet expired, so it
744 // should be restored.
745 env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS));
746
747 {
748 auto const loan = env.le(loanKeylet);
749 if (!BEAST_EXPECT(loan))
750 return;
751 BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
752 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
753 }
754
755 cleanupLoan(loanKeylet, originalNextDueDate);
756 }
757
758 // ---- Case B: impair before late, unimpair after original due date ----
759 {
760 auto const loanKeylet = createNewLoan();
761 auto const loanSle = env.le(loanKeylet);
762 if (!BEAST_EXPECT(loanSle))
763 return;
764 std::uint32_t const startDate = loanSle->at(sfStartDate);
765 std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
766 BEAST_EXPECT(originalNextDueDate == startDate + 600);
767
768 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
769
770 env.close(NetClock::time_point{NetClock::duration{originalNextDueDate}} + 10s);
771
772 auto const timeBeforeUnimpair =
773 env.current()->header().parentCloseTime.time_since_epoch().count();
774
775 env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS));
776
777 {
778 auto const loan = env.le(loanKeylet);
779 if (!BEAST_EXPECT(loan))
780 return;
781 BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
782 std::uint32_t const newDueDate = loan->at(sfNextPaymentDueDate);
783 BEAST_EXPECT(newDueDate > originalNextDueDate);
784 BEAST_EXPECT(newDueDate == timeBeforeUnimpair + 600);
785 }
786 }
787 }
788
789 // FN-68: a borrower must not be able to bypass late-payment charges by
790 // paying an impaired, overdue loan with a plain LoanPay. Under
791 // fixCleanup3_4_0 impairment no longer moves the due date, so
792 // the payment logic sees the real (overdue) date: a regular payment is
793 // rejected with tecEXPIRED, and only a tfLoanLatePayment (which charges
794 // the late fee + late interest) is accepted.
795 void
797 {
798 using namespace jtx;
799 using namespace loan;
800 using namespace std::chrono_literals;
801
802 testcase("Impaired overdue LoanPay requires late-payment flag");
803
804 Env env(*this, all_ | fixCleanup3_4_0);
805 BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
806
807 Account const lender{"lender"};
808 Account const borrower{"borrower"};
809
810 env.fund(XRP(100'000'000), lender, borrower);
811 env.close();
812
813 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
814 auto const broker = createVaultAndBroker(env, xrpAsset, lender);
815
816 auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
817 if (!BEAST_EXPECT(sleBroker))
818 return;
819 auto const loanKeylet =
820 keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
821
822 // Loan with non-zero late-payment terms, so the late path carries a
823 // real penalty that the exploit would otherwise avoid.
824 Number const principalRequest{1, 3};
825 env(set(borrower, broker.brokerID, broker.asset(principalRequest).value()),
826 Sig(sfCounterpartySignature, lender),
827 kPaymentTotal(12),
828 kPaymentInterval(600),
829 kLatePaymentFee(broker.asset(3).number()),
830 kLateInterestRate(TenthBips32{30322}),
831 Fee(env.current()->fees().base * 2));
832 env.close();
833
834 auto const loanSle = env.le(loanKeylet);
835 if (!BEAST_EXPECT(loanSle))
836 return;
837 std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
838 std::uint32_t const paymentsBefore = loanSle->at(sfPaymentRemaining);
839 BEAST_EXPECT(originalNextDueDate > 0);
840
841 // Advance past the due date so the loan is overdue, then impair it
842 // (impairment is only allowed once the payment is late).
843 env.close(NetClock::time_point{NetClock::duration{originalNextDueDate}} + 1s);
844 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
845 env.close();
846
847 {
848 auto const loan = env.le(loanKeylet);
849 if (!BEAST_EXPECT(loan))
850 return;
851 BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
852 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
853 }
854
855 auto const payAmount = broker.asset(500).value();
856
857 // The exploit: a plain LoanPay (Flags = 0) on an impaired, overdue
858 // loan must be rejected. Before FN-9 the auto-unimpair pushed the due
859 // date into the future and this returned tesSUCCESS, letting the
860 // borrower skip the late fee and late interest.
861 env(pay(borrower, loanKeylet.key, payAmount), Ter(tecEXPIRED));
862 env.close();
863
864 {
865 auto const loan = env.le(loanKeylet);
866 if (!BEAST_EXPECT(loan))
867 return;
868 BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
869 BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentsBefore);
870 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
871 }
872
873 env(pay(borrower, loanKeylet.key, payAmount, tfLoanLatePayment), Ter(tesSUCCESS));
874 env.close();
875 {
876 auto const loan = env.le(loanKeylet);
877 if (!BEAST_EXPECT(loan))
878 return;
879 BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
880 BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentsBefore - 1);
881 }
882
883 {
884 auto const vaultSle = env.le(broker.vaultKeylet());
885 if (!BEAST_EXPECT(vaultSle))
886 return;
887 BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
888 }
889 }
890
891 // FN-68 (pre-amendment): documents the original vulnerability. Without
892 // fixCleanup3_4_0, impairing moves the due date and LoanPay
893 // auto-unimpair pushes it into the future before the late check, so a
894 // plain (Flags = 0) LoanPay on an impaired, overdue loan is accepted as
895 // on-time (tesSUCCESS) and the borrower dodges the late-payment charges.
896 // This is what testImpairedOverdueLoanPayRequiresLateFlag closes once the
897 // amendment is enabled.
898 void
900 {
901 using namespace jtx;
902 using namespace loan;
903 using namespace std::chrono_literals;
904
905 testcase("Impaired overdue LoanPay bypass (pre-amendment)");
906
907 Env env(*this, all_ - fixCleanup3_4_0);
908 BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
909
910 Account const lender{"lender"};
911 Account const borrower{"borrower"};
912
913 env.fund(XRP(100'000'000), lender, borrower);
914 env.close();
915
916 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
917 auto const broker = createVaultAndBroker(env, xrpAsset, lender);
918
919 auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
920 if (!BEAST_EXPECT(sleBroker))
921 return;
922 auto const loanKeylet =
923 keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
924
925 Number const principalRequest{1, 3};
926 env(set(borrower, broker.brokerID, broker.asset(principalRequest).value()),
927 Sig(sfCounterpartySignature, lender),
928 kPaymentTotal(12),
929 kPaymentInterval(600),
930 kLatePaymentFee(broker.asset(3).number()),
931 kLateInterestRate(TenthBips32{30322}),
932 Fee(env.current()->fees().base * 2));
933 env.close();
934
935 auto const loanSle = env.le(loanKeylet);
936 if (!BEAST_EXPECT(loanSle))
937 return;
938 std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
939 BEAST_EXPECT(originalNextDueDate > 0);
940
941 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
942 env.close();
943
944 env.close(NetClock::time_point{NetClock::duration{originalNextDueDate}} + 1s);
945
946 {
947 auto const loan = env.le(loanKeylet);
948 if (!BEAST_EXPECT(loan))
949 return;
950 BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
951 }
952
953 auto const payAmount = broker.asset(500).value();
954
955 // The bug: a plain LoanPay is accepted as on-time and clears the
956 // loan's impaired flag, so the late fee / late interest are never
957 // charged.
958 env(pay(borrower, loanKeylet.key, payAmount), Ter(tesSUCCESS));
959 env.close();
960 {
961 auto const loan = env.le(loanKeylet);
962 if (!BEAST_EXPECT(loan))
963 return;
964 BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
965 }
966 }
967
968 // Default uses NextPaymentDueDate + GracePeriod. Once fixCleanup3_4_0
969 // is enabled, that gate is Exclusive, matching impair/isPaymentLate:
970 // default is allowed only after grace has passed, not at the instant
971 // it expires.
972 void
974 {
975 testcase("LoanManage default at exact grace expiry rejected with fixCleanup3_4_0");
976
977 using namespace jtx;
978 using namespace loan;
979 using namespace std::chrono_literals;
980
981 Env env(*this, all_);
982 BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
983
984 Account const lender{"lender"};
985 Account const borrower{"borrower"};
986
987 env.fund(XRP(100'000'000), lender, borrower);
988 env.close();
989
990 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
991 auto const broker = createVaultAndBroker(env, xrpAsset, lender);
992
993 auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
994 if (!BEAST_EXPECT(sleBroker))
995 return;
996 auto const loanKeylet =
997 keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
998
999 env(set(borrower, broker.brokerID, broker.asset(Number{1, 3}).value()),
1000 Sig(sfCounterpartySignature, lender),
1001 kPaymentTotal(12),
1002 kPaymentInterval(600),
1003 kGracePeriod(60),
1004 Fee(env.current()->fees().base * 2));
1005 env.close();
1006
1007 // Advance far enough that parentCloseTime > GracePeriod, so
1008 // (now - grace) cannot underflow when pinning the exact expiry.
1009 env.close(env.now() + 1000s);
1010
1011 auto const loanSle = env.le(loanKeylet);
1012 if (!BEAST_EXPECT(loanSle))
1013 return;
1014 auto const grace = loanSle->at(sfGracePeriod);
1015 std::uint32_t const now = env.current()->parentCloseTime().time_since_epoch().count();
1016 BEAST_EXPECT(now > grace + 1);
1017
1018 // parentCloseTime == NextPaymentDueDate + GracePeriod: grace expires
1019 // this instant, so default must still be too soon.
1020 setLoanNextPaymentDueDate(env, loanKeylet, now - grace);
1021 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecTOO_SOON));
1022 {
1023 auto const loan = env.le(loanKeylet);
1024 if (!BEAST_EXPECT(loan))
1025 return;
1026 BEAST_EXPECT(!loan->isFlag(lsfLoanDefault));
1027 }
1028
1029 // One second after grace expires, default succeeds.
1030 setLoanNextPaymentDueDate(env, loanKeylet, now - grace - 1);
1031 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
1032 {
1033 auto const loan = env.le(loanKeylet);
1034 if (!BEAST_EXPECT(loan))
1035 return;
1036 BEAST_EXPECT(loan->isFlag(lsfLoanDefault));
1037 }
1038 }
1039
1040 void
1042 {
1043 testcase("LoanManage default at exact grace expiry succeeds without fixCleanup3_4_0");
1044
1045 using namespace jtx;
1046 using namespace loan;
1047 using namespace std::chrono_literals;
1048
1049 Env env(*this, all_ - fixCleanup3_4_0);
1050 BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
1051
1052 Account const lender{"lender"};
1053 Account const borrower{"borrower"};
1054
1055 env.fund(XRP(100'000'000), lender, borrower);
1056 env.close();
1057
1058 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
1059 auto const broker = createVaultAndBroker(env, xrpAsset, lender);
1060
1061 auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
1062 if (!BEAST_EXPECT(sleBroker))
1063 return;
1064 auto const loanKeylet =
1065 keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
1066
1067 env(set(borrower, broker.brokerID, broker.asset(Number{1, 3}).value()),
1068 Sig(sfCounterpartySignature, lender),
1069 kPaymentTotal(12),
1070 kPaymentInterval(600),
1071 kGracePeriod(60),
1072 Fee(env.current()->fees().base * 2));
1073 env.close();
1074
1075 env.close(env.now() + 1000s);
1076
1077 auto const loanSle = env.le(loanKeylet);
1078 if (!BEAST_EXPECT(loanSle))
1079 return;
1080 auto const grace = loanSle->at(sfGracePeriod);
1081 std::uint32_t const now = env.current()->parentCloseTime().time_since_epoch().count();
1082 BEAST_EXPECT(now > grace);
1083
1084 setLoanNextPaymentDueDate(env, loanKeylet, now - grace);
1085 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
1086 {
1087 auto const loan = env.le(loanKeylet);
1088 if (!BEAST_EXPECT(loan))
1089 return;
1090 BEAST_EXPECT(loan->isFlag(lsfLoanDefault));
1091 }
1092 }
1093
1094 // Every signature on a transaction covered the same bytes before
1095 // fixCleanup3_4_0, so a signature could be moved from the role that made
1096 // it into another role. Here the lender signs a LoanSet as the
1097 // counterparty, and the borrower copies that signature into the
1098 // SponsorSignature, making the lender pay the fee without the lender ever
1099 // agreeing to sponsor it.
1100 void
1102 {
1103 testcase(
1104 std::string("Counterparty signature copied into the sponsor slot") +
1105 (fixEnabled ? "" : " (pre-amendment)"));
1106
1107 using namespace jtx;
1108 using namespace loan;
1109
1110 Env env(*this, fixEnabled ? all_ : all_ - fixCleanup3_4_0);
1111 BEAST_EXPECT(env.enabled(fixCleanup3_4_0) == fixEnabled);
1112
1113 Account const lender{"lender"};
1114 Account const borrower{"borrower"};
1115
1116 env.fund(XRP(100'000'000), lender, borrower);
1117 env.close();
1118
1119 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
1120 auto const broker = createVaultAndBroker(env, xrpAsset, lender);
1121
1122 auto const feeAmt = XRP(1);
1123
1124 // The lender agrees to the loan by signing the Counterparty slot of a
1125 // LoanSet that names the lender as the fee sponsor. The lender signs
1126 // nothing else.
1127 auto loanSet = env.json(
1128 set(borrower, broker.brokerID, broker.asset(Number{1, 3}).value()),
1129 sponsor::As(lender, spfSponsorFee),
1130 Sig(sfCounterpartySignature, lender),
1131 Fee(feeAmt));
1132
1133 // The borrower copies the lender's signature into the sponsor slot.
1134 loanSet[sfSponsorSignature.jsonName] = loanSet[sfCounterpartySignature.jsonName];
1135
1136 auto const lenderBalance = env.balance(lender);
1137 auto const borrowerBalance = env.balance(borrower);
1138
1139 env(loanSet, Ter(fixEnabled ? TER{telENV_RPC_FAILED} : TER{tesSUCCESS}));
1140 env.close();
1141
1142 if (fixEnabled)
1143 {
1144 // The copied signature does not verify in the sponsor slot, so
1145 // nothing happens at all.
1146 BEAST_EXPECT(env.balance(lender) == lenderBalance);
1147 BEAST_EXPECT(env.balance(borrower) == borrowerBalance);
1148 }
1149 else
1150 {
1151 // The lender paid the fee, and the borrower got the loan.
1152 BEAST_EXPECT(env.balance(lender) == lenderBalance - feeAmt);
1153 BEAST_EXPECT(env.balance(borrower).value() > borrowerBalance.value());
1154 }
1155 }
1156
1157 void
1170
1171 // Tests run under each entry in amendmentCombinations().
1172 void
1174 {
1176 testRIPD3831(features);
1177 testRIPD3459(features);
1178 testRIPD3902(features);
1179 }
1180
1181public:
1182 void
1183 run() override
1184 {
1186 for (auto const& features : jtx::amendmentCombinations(
1187 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
1188 runAmendmentSensitive(features);
1189 }
1190};
1191
1192BEAST_DEFINE_TESTSUITE(LoanSecurity, tx, xrpl);
1193
1194} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:44
LogOs< char > log
Logging output stream.
Definition suite.h:150
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
bool modify(ModifyType const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
void rawReplace(SLE::Ref sle) override
Unconditionally replace a state item.
Definition OpenView.cpp:244
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
Definition OpenView.cpp:168
json::Value getJson(JsonOptions=JsonOptions::Values::None) const override
Definition STAmount.cpp:735
STAmount const & value() const noexcept
Definition STAmount.h:610
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
virtual OpenLedger & getOpenLedger()=0
void testRIPD3902(FeatureBitset features)
void runAmendmentSensitive(FeatureBitset features)
void setLoanNextPaymentDueDate(jtx::Env &env, Keylet const &loanKeylet, std::uint32_t dueDate)
void testSignatureCopiedBetweenRoles(bool fixEnabled)
void testRIPD3459(FeatureBitset features)
void run() override
Runs the suite.
void testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(FeatureBitset features)
void testRIPD3831(FeatureBitset features)
Shared base for the Loan*_test family under src/test/app/lending/.
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.
void makeLoanPayments(jtx::Env &env, BrokerInfo const &broker, LoanParameters const &loanParams, Keylet const &loanKeylet, VerifyLoanStatus const &verifyLoanStatus, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, PaymentParameters const &paymentParams=PaymentParameters::defaults())
std::optional< std::tuple< BrokerInfo, Keylet, jtx::Account > > createLoan(jtx::Env &env, AssetType assetType, BrokerParameters const &brokerParams, LoanParameters const &loanParams, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower)
static void topUpBorrower(jtx::Env &env, BrokerInfo const &broker, jtx::Account const &issuer, jtx::Account const &borrower, LoanState const &state, std::optional< Number > const &servFee)
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
std::string const & name() const
Return the name.
Definition jtx/Account.h:75
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:752
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
bool enabled(UInt256 feature) const
Definition Env.h:886
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:1058
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:723
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
beast::unit_test::Suite & test
Definition Env.h:163
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
NetClock::time_point now()
Returns the current network time.
Definition Env.h:326
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
Set the flags on a JTx.
Definition txflags.h:14
T endl(T... args)
T make_shared(T... args)
T max(T... args)
T min(T... args)
constexpr Zero kZero
Definition Zero.h:30
Number loanPrincipalFromPeriodicPayment(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentsRemaining)
PaymentComponents computePaymentComponents(Rules const &rules, Asset const &asset, std::int32_t scale, Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentRemaining, TenthBips16 managementFeeRate)
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:591
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
json::Value set(AccountID const &account, UInt256 const &vaultId, uint32_t flags)
json::Value coverDeposit(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value pay(AccountID const &account, UInt256 const &loanID, STAmount const &amount, std::uint32_t flags)
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
std::vector< FeatureBitset > amendmentCombinations(std::initializer_list< UInt256 > features, FeatureBitset seed)
Returns all 2^N permutations of a seed FeatureBitset with each subset of the given features excluded.
Definition Env.h:123
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value noop(Account const &account)
The null transaction.
Definition noop.h:14
XRPAmount txFee(Env const &env, std::uint16_t n)
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
constexpr FlagValue spfSponsorFee
Definition TxFlags.h:465
@ telENV_RPC_FAILED
Definition TER.h:54
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
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
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
BaseUInt< 256 > UInt256
Definition base_uint.h:580
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:735
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecTOO_SOON
Definition TER.h:326
@ tecEXPIRED
Definition TER.h:322
@ tesSUCCESS
Definition TER.h:250
Number computeFullPaymentInterest(Number const &theoreticalPrincipalOutstanding, Number const &periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, std::uint32_t prevPaymentDate, std::uint32_t startDate, TenthBips32 closeInterestRate)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
UInt256 key
Definition Keylet.h:21
Helper class to compare the expected state of a loan and loan broker against the data in the ledger.
STAmount const & value() const