xrpld
Loading...
Searching...
No Matches
LoanInvariants_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/flags.h>
8#include <test/jtx/jtx_json.h>
9#include <test/jtx/mpt.h>
10#include <test/jtx/pay.h>
11#include <test/jtx/ter.h>
12#include <test/jtx/trust.h>
13
14#include <xrpl/basics/Number.h>
15#include <xrpl/beast/unit_test/suite.h>
16#include <xrpl/json/json_value.h>
17#include <xrpl/ledger/helpers/LendingHelpers.h>
18#include <xrpl/protocol/Asset.h>
19#include <xrpl/protocol/Feature.h>
20#include <xrpl/protocol/Indexes.h>
21#include <xrpl/protocol/Issue.h>
22#include <xrpl/protocol/Protocol.h>
23#include <xrpl/protocol/SField.h>
24#include <xrpl/protocol/STAmount.h>
25#include <xrpl/protocol/SeqProxy.h>
26#include <xrpl/protocol/TER.h>
27#include <xrpl/protocol/TxFlags.h>
28#include <xrpl/protocol/Units.h>
29
30#include <algorithm>
31#include <cstdint>
32#include <optional>
33
34namespace xrpl::test {
35
37{
38private:
39 // Each of these regression tests reproduces a single fuzzer-found (FIND-*)
40 // scenario against xrpl::detail::computePeriodicPayment /
41 // loanComputePaymentParts. They're merged into one function, one block
42 // per finding, because each is a narrow, self-contained repro that
43 // shares little beyond the surrounding scaffold.
44 void
46 {
47 using namespace jtx;
48 using namespace std::chrono_literals;
49 using namespace lending;
50
51 // From FIND-012
52 {
53 testcase << "LoanPay xrpl::detail::computePeriodicPayment : "
54 "valid rate";
55
56 Env env(*this, features);
57
58 Account const issuer{"issuer"};
59 Account const lender{"lender"};
60 Account const borrower{"borrower"};
61
62 BrokerParameters const brokerParams;
63 env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, lender, borrower);
64 env.close();
65
66 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
67 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
68
69 using namespace loan;
70
71 auto const loanSetFee = Fee(env.current()->fees().base * 2);
72 Number const principalRequest{640562, -5};
73
74 Number const serviceFee{2462611968};
75 std::uint32_t const numPayments{4294967295 / 800};
76
77 auto createJson = env.json(
78 set(borrower, broker.brokerID, principalRequest),
79 Fee(loanSetFee),
80 kLoanServiceFee(serviceFee),
81 kPaymentTotal(numPayments),
82 Json(sfCounterpartySignature, json::ValueType::Object));
83
84 createJson["CloseInterestRate"] = 55374;
85 createJson["ClosePaymentFee"] = "3825205248";
86 createJson["LatePaymentFee"] = "237";
87 createJson["LoanOriginationFee"] = "0";
88 createJson["OverpaymentFee"] = 35167;
89 createJson["OverpaymentInterestRate"] = 1360;
90 createJson["PaymentInterval"] = 727;
91
92 auto const keylet = nextLoanKeylet(env, broker);
93
94 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
95 // Fails in preclaim because principal requested can't be
96 // represented as XRP
97 env(createJson, Ter(tecPRECISION_LOSS));
98 env.close();
99
100 BEAST_EXPECT(!env.le(keylet));
101
102 Number const actualPrincipal{6};
103
104 createJson[sfPrincipalRequested] = actualPrincipal;
105 createJson.removeMember(sfSequence.jsonName);
106 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
107 // Fails in doApply because the payment is too small to be
108 // represented as XRP.
109 env(createJson, Ter(tecPRECISION_LOSS));
110 env.close();
111 }
112
113 // From FIND-010
114 {
115 testcase << "xrpl::loanComputePaymentParts : valid total interest";
116
117 Env env(*this, features);
118
119 Account const issuer{"issuer"};
120 Account const lender{"lender"};
121 Account const borrower{"borrower"};
122
123 PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower);
124
125 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
126
127 using namespace loan;
128
129 auto const loanSetFee = Fee(env.current()->fees().base * 2);
130 Number const principalRequest{1, 3};
131
132 auto createJson = env.json(
133 set(borrower, broker.brokerID, principalRequest),
134 Fee(loanSetFee),
135 Json(sfCounterpartySignature, json::ValueType::Object));
136
137 createJson["CloseInterestRate"] = 47299;
138 createJson["ClosePaymentFee"] = "3985819770";
139 createJson["InterestRate"] = 92;
140 createJson["LatePaymentFee"] = "3866894865";
141 createJson["LoanOriginationFee"] = "0";
142 createJson["LoanServiceFee"] = "2348810240";
143 createJson["OverpaymentFee"] = 58545;
144 createJson["PaymentInterval"] = 60;
145 createJson["PaymentTotal"] = 1;
146 createJson["PrincipalRequested"] = "0.000763058";
147
148 auto const keylet = nextLoanKeylet(env, broker);
149
150 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
151 env(createJson);
152 env.close();
153
154 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
155 loanPayTx["Amount"]["value"] = "0.000281284125490196";
156 env(loanPayTx, Ter(tecINSUFFICIENT_PAYMENT));
157 env.close();
158 }
159
160 // From FIND-009
161 {
162 testcase << "xrpl::loanComputePaymentParts : totalPrincipalPaid "
163 "rounded";
164
165 Env env(*this, features);
166
167 Account const issuer{"issuer"};
168 Account const lender{"lender"};
169 Account const borrower{"borrower"};
170
171 PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower);
172
173 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
174
175 using namespace loan;
176
177 auto const loanSetFee = Fee(env.current()->fees().base * 2);
178 Number const principalRequest{1, 3};
179
180 auto createJson = env.json(
181 set(borrower, broker.brokerID, principalRequest),
182 Fee(loanSetFee),
183 Json(sfCounterpartySignature, json::ValueType::Object));
184
185 createJson["ClosePaymentFee"] = "0";
186 createJson["InterestRate"] = 24346;
187 createJson["LateInterestRate"] = 65535;
188 createJson["LatePaymentFee"] = "0";
189 createJson["LoanOriginationFee"] = "218";
190 createJson["LoanServiceFee"] = "0";
191 createJson["PaymentInterval"] = 60;
192 createJson["PaymentTotal"] = 5678;
193 createJson["PrincipalRequested"] = "9924.81";
194
195 auto const keylet = nextLoanKeylet(env, broker);
196
197 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
198 env(createJson, Ter(tesSUCCESS));
199 env.close();
200
201 auto const baseFee = env.current()->fees().base;
202
203 auto const stateBefore = getCurrentState(env, broker, keylet);
204
205 {
206 auto loanPayTx =
207 env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
208 Number const amount{3074'745'058'823'529, -12};
209 BEAST_EXPECT(to_string(amount) == "3074.745058823529");
210 XRPAmount const payFee{
211 baseFee *
212 (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)};
213 loanPayTx["Amount"]["value"] = to_string(amount);
214 env(loanPayTx, Fee(payFee), Ter(tesSUCCESS));
215 env.close();
216 }
217
218 {
219 auto loanPayTx =
220 env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
221 Number const amount{6732'118'170'944'051, -12};
222 BEAST_EXPECT(to_string(amount) == "6732.118170944051");
223 XRPAmount const payFee{
224 baseFee *
225 (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)};
226 loanPayTx["Amount"]["value"] = to_string(amount);
227 env(loanPayTx, Fee(payFee), Ter(tesSUCCESS));
228 env.close();
229 }
230
231 auto const stateAfter = getCurrentState(env, broker, keylet);
232 // Total interest outstanding is non-negative
233 BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding);
234 // Principal paid is non-negative
235 BEAST_EXPECT(stateBefore.principalOutstanding >= stateAfter.principalOutstanding);
236 // Total value change is non-negative
237 BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue);
238 // Value delta is larger or same as principal delta (meaning
239 // non-negative interest paid)
240 BEAST_EXPECT(
241 (stateBefore.totalValue - stateAfter.totalValue) >=
242 (stateBefore.principalOutstanding - stateAfter.principalOutstanding));
243 }
244
245 // From FIND-008
246 {
247 testcase << "xrpl::loanComputePaymentParts : loanValueChange rounded";
248
249 Env env(*this, features);
250
251 Account const issuer{"issuer"};
252 Account const lender{"lender"};
253 Account const borrower{"borrower"};
254
255 PrettyAsset const iouAsset =
256 createFundedIouAsset(env, issuer, lender, borrower, 100'000'000, 10'000'000);
257
258 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
259 {
260 auto const coverDepositValue =
261 broker.asset(broker.params.coverDeposit * 10).value();
262 env(loan_broker::coverDeposit(lender, broker.brokerID, coverDepositValue));
263 env.close();
264 }
265
266 using namespace loan;
267
268 auto const loanSetFee = Fee(env.current()->fees().base * 2);
269 Number const principalRequest{1, 3};
270
271 auto createJson = env.json(
272 set(borrower, broker.brokerID, principalRequest),
273 Fee(loanSetFee),
274 Json(sfCounterpartySignature, json::ValueType::Object));
275
276 createJson["ClosePaymentFee"] = "0";
277 createJson["InterestRate"] = 12833;
278 createJson["LateInterestRate"] = 77048;
279 createJson["LatePaymentFee"] = "0";
280 createJson["LoanOriginationFee"] = "218";
281 createJson["LoanServiceFee"] = "0";
282 createJson["PaymentInterval"] = 752;
283 createJson["PaymentTotal"] = 5678;
284 createJson["PrincipalRequested"] = "9924.81";
285
286 auto const keylet = nextLoanKeylet(env, broker);
287
288 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
289 env(createJson, Ter(tesSUCCESS));
290 env.close();
291
292 auto const baseFee = env.current()->fees().base;
293
294 auto const stateBefore = getCurrentState(env, broker, keylet);
295 BEAST_EXPECT(stateBefore.paymentRemaining == 5678);
296 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
297
298 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
299 Number const amount{9924'81, -2};
300 BEAST_EXPECT(to_string(amount) == "9924.81");
301 XRPAmount const payFee{
302 baseFee *
303 (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)};
304 loanPayTx["Amount"]["value"] = to_string(amount);
305 env(loanPayTx, Fee(payFee), Ter(tesSUCCESS));
306 env.close();
307
308 auto const stateAfter = getCurrentState(env, broker, keylet);
309 BEAST_EXPECT(
310 stateAfter.paymentRemaining ==
311 stateBefore.paymentRemaining - kLoanMaximumPaymentsPerTransaction);
312 }
313 }
314
315 void
317 {
318 // From FIND-007
319 testcase << "LoanPay xrpl::LoanPay::doApply : debtDecrease "
320 "rounding good";
321
322 using namespace jtx;
323 using namespace std::chrono_literals;
324 using namespace lending;
325 Env env(*this, features);
326
327 Account const issuer{"issuer"};
328 Account const lender{"lender"};
329 Account const borrower{"borrower"};
330
331 PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower);
332
333 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
334
335 using namespace loan;
336
337 auto const baseFee = env.current()->fees().base;
338 auto const loanSetFee = Fee(baseFee * 2);
339 Number const principalRequest{1, 3};
340
341 auto createJson = env.json(
342 set(borrower, broker.brokerID, principalRequest),
343 Fee(loanSetFee),
344 Json(sfCounterpartySignature, json::ValueType::Object));
345
346 createJson["ClosePaymentFee"] = "0";
347 createJson["GracePeriod"] = 60;
348 createJson["InterestRate"] = 24346;
349 createJson["LateInterestRate"] = 65535;
350 createJson["LatePaymentFee"] = "0";
351 createJson["LoanOriginationFee"] = "218";
352 createJson["LoanServiceFee"] = "0";
353 createJson["PaymentInterval"] = 60;
354 createJson["PaymentTotal"] = 5678;
355 createJson["PrincipalRequested"] = "9924.81";
356
357 auto const keylet = nextLoanKeylet(env, broker);
358
359 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
360 env(createJson, Ter(tesSUCCESS));
361 env.close();
362
363 auto const pseudoAcct = brokerPseudoAccount(env, broker, lender);
364
365 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet);
366 auto const originalState = getCurrentState(env, broker, keylet);
367 verifyLoanStatus(originalState);
368
369 Number const payment{3'269'349'176'470'588, -12};
370 XRPAmount const payFee{
371 baseFee *
372 ((payment / originalState.periodicPayment) / kLoanPaymentsPerFeeIncrement + 1)};
373 auto loanPayTx =
374 env.json(pay(borrower, keylet.key, STAmount{broker.asset, payment}), Fee(payFee));
375 BEAST_EXPECT(to_string(payment) == "3269.349176470588");
376 env(loanPayTx, Ter(tesSUCCESS));
377 env.close();
378
379 auto const newState = getCurrentState(env, broker, keylet);
380 BEAST_EXPECT(
381 isRounded(broker.asset, newState.managementFeeOutstanding, originalState.loanScale));
382 BEAST_EXPECT(newState.managementFeeOutstanding < originalState.managementFeeOutstanding);
383 BEAST_EXPECT(isRounded(broker.asset, newState.totalValue, originalState.loanScale));
384 BEAST_EXPECT(
385 isRounded(broker.asset, newState.principalOutstanding, originalState.loanScale));
386 }
387
388 // Verify an overpayment cannot reduce principal without covering and
389 // advancing at least one scheduled instalment: reject an extra-only amount,
390 // but accept an instalment plus extra. Enable V1_1 explicitly because
391 // LoanTestBase::all_ excludes it.
392 void
394 {
395 testcase("LoanPay overpayment schedule advancement");
396
397 using namespace jtx;
398 using namespace loan;
399
400 Env env{*this, features | featureLendingProtocolV1_1};
401
402 Account const lender{"lender"};
403 Account const borrower{"borrower"};
404
405 env.fund(XRP(10'000'000), lender, borrower);
406 env.close();
407
408 PrettyAsset const asset{xrpIssue(), 1000};
409
410 BrokerInfo const broker = createVaultAndBroker(
411 env,
412 asset,
413 lender,
414 {
415 .vaultDeposit = asset(100'000).value(),
416 .managementFeeRate = TenthBips16(10'000),
417 });
418
419 auto const loanSetFee = Fee(env.current()->fees().base * 2);
420
421 // Principal 10,000 over 3 payments, overpayment enabled. One scheduled
422 // payment is ~3,333, so an amount well below that cannot cover one.
423 auto const loanKeylet = nextLoanKeylet(env, broker);
424 env(loan::set(borrower, broker.brokerID, asset(10'000).value(), tfLoanOverpayment),
425 Sig(sfCounterpartySignature, lender),
426 loan::kPaymentInterval(86400 * 30),
429 loanSetFee);
430 env.close();
431
432 auto const before = getCurrentState(env, broker, loanKeylet);
433 BEAST_EXPECT(before.paymentRemaining == 3);
434
435 STAmount const belowOnePayment = asset(1'000).value();
436 BEAST_EXPECT((belowOnePayment < STAmount{asset, before.periodicPayment}));
437
438 auto const payFee = Fee(env.current()->fees().base * 2);
439
440 // The amount does not cover a scheduled payment, so makeRegularPayment makes zero scheduled
441 // payments and returns tecINSUFFICIENT_PAYMENT before the Extra branch runs. Were the
442 // payment to succeed while touching only principal, PaymentRemaining and NextPaymentDueDate
443 // would silently fail to advance.
444 env(pay(borrower, loanKeylet.key, belowOnePayment, tfLoanOverpayment),
445 payFee,
447 env.close();
448
449 auto const afterReject = getCurrentState(env, broker, loanKeylet);
450 BEAST_EXPECT(afterReject.paymentRemaining == before.paymentRemaining);
451 BEAST_EXPECT(afterReject.principalOutstanding == before.principalOutstanding);
452 BEAST_EXPECT(afterReject.nextPaymentDate == before.nextPaymentDate);
453
454 // PaymentRemaining drops by one, NextPaymentDueDate advances by one interval, and
455 // PrincipalOutstanding strictly decreases (by more than a plain payment thanks to the
456 // extra).
457 STAmount const onePaymentPlusExtra = asset(5'000).value();
458 env(pay(borrower, loanKeylet.key, onePaymentPlusExtra, tfLoanOverpayment), payFee);
459 env.close();
460
461 auto const afterPay = getCurrentState(env, broker, loanKeylet);
462 BEAST_EXPECT(afterPay.paymentRemaining == before.paymentRemaining - 1);
463 BEAST_EXPECT(afterPay.principalOutstanding < before.principalOutstanding);
464 BEAST_EXPECT(afterPay.nextPaymentDate == before.nextPaymentDate + before.paymentInterval);
465 }
466
467 void
469 {
470 // (From FIND-006)
471 testcase << "LoanSet trigger xrpl::accountSendMPT : minimum amount "
472 "and MPT";
473
474 using namespace jtx;
475 using namespace std::chrono_literals;
476 Env env(*this, features);
477
478 Account const issuer{"issuer"};
479 Account const lender{"lender"};
480 Account const borrower{"borrower"};
481
482 env.fund(XRP(1'000'000), issuer, lender, borrower);
483 env.close();
484
485 MPTTester mptt{env, issuer, kMptInitNoFund};
486 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
487 PrettyAsset const mptAsset = mptt.issuanceID();
488 mptt.authorize({.account = lender});
489 mptt.authorize({.account = borrower});
490 env(pay(issuer, lender, mptAsset(2'000'000)));
491 env(pay(issuer, borrower, mptAsset(1'000)));
492 env.close();
493
494 BrokerInfo const broker{createVaultAndBroker(env, mptAsset, lender)};
495
496 using namespace loan;
497
498 auto const loanSetFee = Fee(env.current()->fees().base * 2);
499 Number const principalRequest{1, 3};
500
501 auto createJson = env.json(
502 set(borrower, broker.brokerID, principalRequest),
503 Fee(loanSetFee),
504 Json(sfCounterpartySignature, json::ValueType::Object));
505
506 createJson["CloseInterestRate"] = 76671;
507 createJson["ClosePaymentFee"] = "2061925410";
508 createJson["GracePeriod"] = 434;
509 createJson["InterestRate"] = 50302;
510 createJson["LateInterestRate"] = 30322;
511 createJson["LatePaymentFee"] = "294427911";
512 createJson["LoanOriginationFee"] = "3250635102";
513 createJson["LoanServiceFee"] = "9557386";
514 createJson["OverpaymentFee"] = 51249;
515 createJson["OverpaymentInterestRate"] = 14304;
516 createJson["PaymentInterval"] = 434;
517 createJson["PaymentTotal"] = "2891743748";
518 createJson["PrincipalRequested"] = "8516.98";
519
520 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
521 env(createJson, Ter(temINVALID));
522 env.close();
523 }
524
525 // Verify that LoanPay, LoanBrokerCoverWithdraw, and LoanSet all use the
526 // same vault-scale minimum cover when fixCleanup3_2_0 is enabled.
527 // Before the amendment, each transactor computed its minimum cover at a
528 // different precision (loanScale, debtScale, or the raw unrounded
529 // tenthBipsOfValue), which could lead to inconsistent decisions for the
530 // same broker state. After the amendment all three use
531 // minimumBrokerCover at vaultScale.
532 void
534 {
535 using namespace jtx;
536 using namespace loan;
537 using namespace loan_broker;
538
539 bool const withAmendment = features[fixCleanup3_2_0];
540
541 struct Ctx
542 {
543 jtx::Account issuer;
544 jtx::Account lender;
545 jtx::Account borrower;
547 BrokerInfo broker;
548 BrokerParameters brokerParams;
549 };
550
551 // Shared setup, parametrized by vaultDeposit (the only varying setup
552 // field across the three scenarios). Each call runs in its own Env
553 // so multiple invocations within one scenario cannot interfere.
554 // The caller is responsible for invoking testcase(...) before the
555 // first runTest call of each scenario.
556 auto runTest = [&](Number vaultDeposit, auto&& body) {
557 Env env(*this, features);
558
559 Account const issuer{"issuer"};
560 Account const lender{"lender"};
561 Account const borrower{"borrower"};
562
563 env.fund(XRP(1'000'000'000), issuer, lender, borrower);
564 env.close();
565
566 // Enable clawback on the issuer *before* any trust lines exist
567 // (asfAllowTrustLineClawback requires an empty owner directory).
568 env(fset(issuer, asfAllowTrustLineClawback));
569 env.close();
570
571 PrettyAsset const iou = issuer[iouCurrency_];
572 env(trust(lender, iou(1'000'000'000)));
573 env(trust(borrower, iou(1'000'000'000)));
574 env.close();
575 env(pay(issuer, lender, iou(100'000'000)));
576 env(pay(issuer, borrower, iou(100'000'000)));
577 env.close();
578
579 // 13.37% — non-round rate produces a messier minimum.
580 BrokerParameters const brokerParams{
581 .vaultDeposit = vaultDeposit,
582 .debtMax = 0,
583 .coverRateMin = TenthBips32{13'370},
584 .coverDeposit = 5'000,
585 .managementFeeRate = TenthBips16{500}};
586
587 BrokerInfo const broker = createVaultAndBroker(env, iou, lender, brokerParams);
588
589 body(
590 env,
591 Ctx{.issuer = issuer,
592 .lender = lender,
593 .borrower = borrower,
594 .iou = iou,
595 .broker = broker,
596 .brokerParams = brokerParams});
597 };
598
599 // Scenario 1 — LoanPay
600 //
601 // Verify that LoanPay's minimum cover check uses vault scale (not
602 // loan scale). Before the amendment, different loans could produce
603 // different fee routing decisions for the same broker-level state.
604 // Small vault deposit => vaultScale = -12.
605 testcase("LoanPay minimum cover scale consistency");
606 {
607 struct LoanKeylets
608 {
609 Keylet tiny;
610 Keylet big;
611 };
612
613 // Create the tiny + big loans and reduce cover via clawback so
614 // that subsequent LoanPay calls hit the minimum-cover boundary.
615 // Used by the two pay-and-check sub-tests below so each can run
616 // in its own Env.
617 auto setupLoansAndClawback = [&](Env& env, Ctx const& c) -> std::optional<LoanKeylets> {
618 Asset const asset{c.iou};
619
620 // Create the TINY loan first (while vaultScale is still
621 // small). principal 0.01, 0% interest, 1 payment =>
622 // loanScale = vaultScale.
623 auto const brokerSle1 = env.le(keylet::loanBroker(c.broker.brokerID));
624 if (!BEAST_EXPECT(brokerSle1))
625 return std::nullopt;
626 auto const tinyLoanSeq = brokerSle1->at(sfLoanSequence);
627 auto const tinyLoanKeylet =
628 keylet::loan(c.broker.brokerID, SeqProxy::rawSequence(tinyLoanSeq));
629
630 env(set(c.borrower, c.broker.brokerID, Number{1, -2}),
631 Sig(sfCounterpartySignature, c.lender),
632 kInterestRate(TenthBips32{0}),
633 kPaymentTotal(1),
634 kPaymentInterval(86400 * 365),
635 Fee(XRP(10)));
636 env.close();
637
638 // Create the BIG loan second. 100% annual interest over 20
639 // payments pushes totalValueOutstanding high enough that
640 // loanScale > vaultScale.
641 auto const brokerSle2 = env.le(keylet::loanBroker(c.broker.brokerID));
642 if (!BEAST_EXPECT(brokerSle2))
643 return std::nullopt;
644 auto const bigLoanSeq = brokerSle2->at(sfLoanSequence);
645 auto const bigLoanKeylet =
646 keylet::loan(c.broker.brokerID, SeqProxy::rawSequence(bigLoanSeq));
647
648 env(set(c.borrower, c.broker.brokerID, Number{500}),
649 Sig(sfCounterpartySignature, c.lender),
650 kInterestRate(TenthBips32{100'000}),
651 kPaymentTotal(20),
652 kPaymentInterval(86400 * 365),
653 Fee(XRP(10)));
654 env.close();
655
656 // The tiny loan's scale is frozen at the vault's pre-big-loan
657 // scale, so it is strictly smaller than the big loan's.
658 // After the big loan is created the vault absorbs its value,
659 // pushing vaultScale up to match bigLoanScale.
660 auto const tinyLoanSle = env.le(tinyLoanKeylet);
661 auto const bigLoanSle = env.le(bigLoanKeylet);
662 auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
663 if (!BEAST_EXPECT(tinyLoanSle) || !BEAST_EXPECT(bigLoanSle) ||
664 !BEAST_EXPECT(vaultSle))
665 return std::nullopt;
666 if (!BEAST_EXPECT(tinyLoanSle->at(sfLoanScale) == -12) ||
667 !BEAST_EXPECT(bigLoanSle->at(sfLoanScale) == -11) ||
668 !BEAST_EXPECT(getAssetsTotalScale(vaultSle) == -11))
669 return std::nullopt;
670
671 // Use issuer clawback to reduce cover to the minimum the
672 // clawback transactor allows. Compute the amount as
673 // initialCover - expectedCoverAfter so we exercise the exact
674 // clawback rather than relying on the transactor to clip
675 // down.
676 //
677 // Before the amendment the clawback minimum is the
678 // *unrounded* tenthBipsOfValue — strictly less than the
679 // rounded-at-vaultScale minimum LoanPay uses for the big
680 // loan. After the amendment both clawback and LoanPay use
681 // the same rounded minimum (via minimumBrokerCover), so
682 // cover lands exactly at that threshold.
683 Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15}
684 : Number{1330651855688458000, -15};
685 Number const clawbackAmount =
686 Number{c.brokerParams.coverDeposit} - expectedCoverAfter;
687
688 env(coverClawback(c.issuer),
689 kLoanBrokerId(c.broker.brokerID),
691 env.close();
692
693 auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID));
694 if (!BEAST_EXPECT(brokerSle) ||
695 !BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCoverAfter))
696 return std::nullopt;
697
698 return LoanKeylets{.tiny = tinyLoanKeylet, .big = bigLoanKeylet};
699 };
700
701 // Pay one loan and report whether the fee went to the broker's
702 // pseudo account (the fallback when cover < minimum) rather
703 // than to the owner.
704 auto feeGoesToPseudo = [&](Env& env, Ctx const& c, Keylet const& loanKeylet) -> bool {
705 Asset const asset{c.iou};
706 auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID));
707 if (!BEAST_EXPECT(brokerSle))
708 return false;
709 auto const pseudoAcct = Account("pseudo", brokerSle->at(sfAccount));
710 auto const pseudoBefore = env.balance(pseudoAcct, c.iou);
711
712 auto const payLoan = env.le(loanKeylet);
713 if (!BEAST_EXPECT(payLoan))
714 return false;
715 auto const periodicPayment = payLoan->at(sfPeriodicPayment);
716 auto const serviceFee = payLoan->at(sfLoanServiceFee);
717 std::int32_t const loanScale = payLoan->at(sfLoanScale);
718
719 auto const payment = roundPeriodicPayment(asset, periodicPayment, loanScale);
720 auto const payAmt = STAmount{asset, payment + serviceFee};
721
722 env(loan::pay(c.borrower, loanKeylet.key, payAmt), Fee(XRP(10)));
723 env.close();
724
725 auto const pseudoAfter = env.balance(pseudoAcct, c.iou);
726 return pseudoAfter.number() > pseudoBefore.number();
727 };
728
729 // Pay the BIG loan in its own Env so its outcome cannot affect
730 // the TINY-loan check. With the fix, LoanPay and clawback use
731 // the same vaultScale minimum (cover == minAtVaultScale =>
732 // fee to owner). Without the fix, LoanPay uses bigLoanScale=-11,
733 // rounds up to a larger minimum than what clawback used =>
734 // cover < min => fee to pseudo.
735 runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) {
736 auto const loans = setupLoansAndClawback(env, c);
737 if (!loans)
738 return;
739 BEAST_EXPECT(feeGoesToPseudo(env, c, loans->big) == !withAmendment);
740 });
741
742 // Pay the TINY loan in its own Env. Fee goes to the owner
743 // either way:
744 // - With the fix: LoanPay uses vaultScale=-11 (same as
745 // clawback) => owner.
746 // - Without the fix: LoanPay uses tinyLoanScale=-12, rounds
747 // up at -12 (a no-op) => min == cover => owner.
748 runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) {
749 auto const loans = setupLoansAndClawback(env, c);
750 if (!loans)
751 return;
752 BEAST_EXPECT(!feeGoesToPseudo(env, c, loans->tiny));
753 });
754 }
755
756 // Scenario 2 — LoanBrokerCoverWithdraw
757 //
758 // Verify that CoverWithdraw's minimum cover check uses vault scale
759 // (not scale(debtTotal, asset)). Before the amendment, CoverWithdraw
760 // used:
761 // roundToAsset(asset, tenthBipsOfValue(debt, rate), scale(debt, asset))
762 // which could disagree with LoanPay's minimum (which used loanScale).
763 //
764 // Use a large vault deposit so that vaultScale (from AssetsTotal) is
765 // strictly larger than debtScale (from DebtTotal). With
766 // vaultDeposit = 100,000: after the big loan
767 // AssetsTotal ≈ 109,500 → vaultScale = -10
768 // DebtTotal ≈ 10,000 → debtScale = -11
769 // The one-order-of-magnitude gap makes roundToAsset at -10 truncate
770 // more aggressively than at -11, exposing the bug.
771 testcase("CoverWithdraw minimum cover scale consistency");
772 runTest(
773 /*vaultDeposit=*/100'000, [&](Env& env, Ctx const& c) {
774 Asset const asset{c.iou};
775
776 // Create only the big loan to push DebtTotal up to ~10,000
777 // while AssetsTotal stays around 109,500 (dominated by the
778 // large vault deposit).
779 env(set(c.borrower, c.broker.brokerID, Number{500}),
780 Sig(sfCounterpartySignature, c.lender),
781 kInterestRate(TenthBips32{100'000}),
782 kPaymentTotal(20),
783 kPaymentInterval(86400 * 365),
784 Fee(XRP(10)));
785 env.close();
786
787 // Read broker state and compute both old and new minimums.
788 auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID));
789 auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
790 if (!BEAST_EXPECT(brokerSle) || !BEAST_EXPECT(vaultSle))
791 return;
792
793 auto const coverAvail = brokerSle->at(sfCoverAvailable);
794 auto const debtTotal = brokerSle->at(sfDebtTotal);
795 auto const vaultScale = getAssetsTotalScale(vaultSle);
796 auto const debtScale = scale(debtTotal, asset);
797
798 // Sanity: debt scale differs from vault scale for this setup.
799 BEAST_EXPECT(debtScale < vaultScale);
800
801 auto const oldMin = [&]() {
803 return roundToAsset(
804 asset,
805 tenthBipsOfValue(debtTotal, TenthBips32{c.brokerParams.coverRateMin}),
806 debtScale);
807 }();
808 auto const newMin = minimumBrokerCover(
809 debtTotal, TenthBips32{c.brokerParams.coverRateMin}, vaultSle);
810
811 // The new (vaultScale) minimum must be strictly larger than
812 // the old (debtScale) minimum — that is the gap the amendment
813 // closes.
814 Number const expectedNewMin{1330650518688500000, -15};
815 Number const expectedOldMin{1330650518688472000, -15};
816 BEAST_EXPECT(newMin == expectedNewMin);
817 BEAST_EXPECT(oldMin == expectedOldMin);
818
819 // Try to withdraw so that remaining cover lands between the
820 // two minimums: oldMin < target < newMin.
821 auto const target = oldMin + (newMin - oldMin) / 2;
822 auto const withdrawAmount = STAmount{asset, coverAvail - target};
823
824 if (withAmendment)
825 {
826 // CoverWithdraw now uses vaultScale: target < newMin
827 // => FAILS.
828 env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount),
830 }
831 else
832 {
833 // Old CoverWithdraw uses debtScale: target > oldMin
834 // => SUCCEEDS.
835 env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount));
836 }
837 env.close();
838 });
839
840 // Scenario 3 — LoanSet
841 //
842 // Verify that LoanSet's minimum cover check uses vault scale (not the
843 // raw unrounded tenthBipsOfValue). Before the amendment, LoanSet
844 // used tenthBipsOfValue(newDebtTotal, coverRateMinimum) (no
845 // roundToAsset), while clawback/withdraw used different formulas.
846 // After the amendment all use minimumBrokerCover at vaultScale, and
847 // rounding at a coarser scale can absorb a tiny debt increase —
848 // allowing a loan that would otherwise be rejected.
849 testcase("LoanSet minimum cover scale consistency");
850 runTest(
851 /*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) {
852 // Create the tiny loan (scale -12) AND the big loan (scale
853 // -11). Both loans are needed so that DebtTotal has a full
854 // 16-digit mantissa — a "messy" value where roundToAsset at
855 // vaultScale actually truncates digits and produces a
856 // different result from the raw tenthBipsOfValue. With only
857 // the big loan, DebtTotal has ~4 significant digits and
858 // rounding at scale -11 is a no-op, masking the amendment's
859 // effect.
860 env(set(c.borrower, c.broker.brokerID, Number{1, -2}),
861 Sig(sfCounterpartySignature, c.lender),
862 kInterestRate(TenthBips32{0}),
863 kPaymentTotal(1),
864 kPaymentInterval(86400 * 365),
865 Fee(XRP(10)));
866 env.close();
867
868 env(set(c.borrower, c.broker.brokerID, Number{500}),
869 Sig(sfCounterpartySignature, c.lender),
870 kInterestRate(TenthBips32{100'000}),
871 kPaymentTotal(20),
872 kPaymentInterval(86400 * 365),
873 Fee(XRP(10)));
874 env.close();
875
876 // Clawback to reduce cover to the clawback transactor's
877 // minimum. Pass the exact amount rather than relying on the
878 // transactor to clip down; the setup matches Scenario 1 so
879 // the same residual-cover values apply.
880 Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15}
881 : Number{1330651855688458000, -15};
882 Number const clawbackAmount =
883 Number{c.brokerParams.coverDeposit} - expectedCoverAfter;
884 env(coverClawback(c.issuer),
885 kLoanBrokerId(c.broker.brokerID),
886 kAmount(c.iou(clawbackAmount)));
887 env.close();
888
889 // Verify scales.
890 auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
891 if (!BEAST_EXPECT(vaultSle))
892 return;
893 auto const vaultScale = getAssetsTotalScale(vaultSle);
894 BEAST_EXPECT(vaultScale == -11);
895
896 // Now try to create a tiny additional loan. Principal is
897 // 1e-11 (the smallest value that survives the precision
898 // check at loanScale = vaultScale = -11), with 0% interest
899 // and 1 payment.
900 //
901 // The tiny debt increase adds ~1.337e-12 to the unrounded
902 // minimum.
903 // - Without the amendment: the old LoanSet formula rounds
904 // up during tenthBipsOfValue (16-digit Number
905 // normalisation), pushing the minimum past the cover left
906 // by clawback => tecINSUFFICIENT_FUNDS.
907 // - With the amendment: minimumBrokerCover rounds at
908 // vaultScale=-11, which absorbs the tiny increase — the
909 // rounded minimum stays the same => tesSUCCESS.
910 auto const tinyPrincipal = Number{1, -11};
911
912 if (withAmendment)
913 {
914 env(set(c.borrower, c.broker.brokerID, tinyPrincipal),
915 Sig(sfCounterpartySignature, c.lender),
916 kInterestRate(TenthBips32{0}),
917 kPaymentTotal(1),
918 kPaymentInterval(86400 * 365),
919 Fee(XRP(10)));
920 }
921 else
922 {
923 env(set(c.borrower, c.broker.brokerID, tinyPrincipal),
924 Sig(sfCounterpartySignature, c.lender),
925 kInterestRate(TenthBips32{0}),
926 kPaymentTotal(1),
927 kPaymentInterval(86400 * 365),
928 Fee(XRP(10)),
930 }
931 env.close();
932 });
933 }
934
935 void
937 {
938 using namespace jtx;
939 using namespace loan;
940
941 auto const runCase = [&](bool coarseBorrower) {
942 testcase(
943 coarseBorrower ? "LoanSet borrower balance uses coarsest scale"
944 : "LoanSet broker owner balance uses coarsest scale");
945
946 Env env(*this, all_ | featureLendingProtocolV1_1);
947 Account const issuer{"issuer"};
948 Account const lender{"lender"};
949 Account const borrower{"borrower"};
950
951 Number const coarseBalance{100'000'000'000LL};
952 Number const regularBalance{100'000'000};
954 env,
955 issuer,
956 lender,
957 borrower,
958 coarseBorrower ? regularBalance : coarseBalance,
959 coarseBorrower ? coarseBalance : regularBalance);
960
961 BrokerParameters const brokerParams{
962 .vaultDeposit = 1'000'000,
963 .debtMax = 0,
964 .coverRateMin = TenthBips32{0},
965 .coverDeposit = 0,
966 .managementFeeRate = TenthBips16{0},
967 .coverRateLiquidation = TenthBips32{0}};
968 BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams);
969
970 Number const principal{1'012'345, -5};
971 Number const originationFee{123'456, -6};
972 Account const& recipient = coarseBorrower ? borrower : lender;
973 Number const expected = coarseBorrower ? principal : originationFee;
974 auto const before = env.balance(recipient, asset);
975
976 if (coarseBorrower)
977 {
978 env(set(borrower, broker.brokerID, principal),
979 kCounterparty(lender),
980 Sig(sfCounterpartySignature, lender),
981 kInterestRate(TenthBips32{0}),
982 kPaymentTotal(1),
983 Fee(env.current()->fees().base * 2),
984 Ter(tesSUCCESS));
985 }
986 else
987 {
988 env(set(borrower, broker.brokerID, principal),
989 kCounterparty(lender),
990 Sig(sfCounterpartySignature, lender),
991 kLoanOriginationFee(originationFee),
992 kInterestRate(TenthBips32{0}),
993 kPaymentTotal(1),
994 Fee(env.current()->fees().base * 2),
995 Ter(tesSUCCESS));
996 }
997 env.close();
998
999 auto const after = env.balance(recipient, asset);
1000 Number const received = after.number() - before.number();
1001 auto const recipientScale =
1002 std::max(before.value().exponent(), after.value().exponent());
1003 auto const vaultScale = broker.vaultScale(env);
1004 Number const tolerance{1, recipientScale};
1005
1006 BEAST_EXPECT(recipientScale > vaultScale);
1007 BEAST_EXPECT(received != expected);
1008 BEAST_EXPECT(
1009 abs(roundToAsset(asset, received, recipientScale) -
1010 roundToAsset(asset, expected, recipientScale)) <= tolerance);
1011 };
1012
1013 runCase(/*coarseBorrower=*/true);
1014 runCase(/*coarseBorrower=*/false);
1015 }
1016
1017 // Under featureLendingProtocolV1_1, ValidLoan::finalize enforces
1018 // TotalValueOutstanding >= PrincipalOutstanding + ManagementFeeOutstanding
1019 // ("interest due is non-negative"). This test drives the transactor
1020 // through a multi-payment scenario with a non-zero management fee and
1021 // messy IOU-scale rounding; if any rounding path in LoanPay were to
1022 // inflate PrincipalOutstanding or ManagementFeeOutstanding relative to
1023 // TotalValueOutstanding by even one ULP, the invariant would fire and
1024 // the LoanPay would return tecINVARIANT_FAILED instead of tesSUCCESS.
1025 void
1027 {
1028 testcase("LoanPay interest-due non-negative invariant");
1029
1030 using namespace jtx;
1031 using namespace loan;
1032
1033 Env env(*this, all_ | featureLendingProtocolV1_1);
1034
1035 Account const issuer{"issuer"};
1036 Account const lender{"lender"};
1037 Account const borrower{"borrower"};
1038
1039 PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower);
1040
1041 // Default broker params carry managementFeeRate = 100 tenth-bips
1042 // (1%), which is what makes managementFeeOutstanding accumulate
1043 // non-trivially through the payment schedule.
1044 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
1045
1046 auto const loanSetFee = Fee(env.current()->fees().base * 2);
1047 auto const loanKeylet = nextLoanKeylet(env, broker);
1048
1049 // Messy interest rate, non-trivial payment count. Values chosen so
1050 // that periodicPayment and each roundedInterest/managementFee share
1051 // are unlikely to be representable exactly at loanScale.
1052 env(set(borrower, broker.brokerID, Number{1'000}),
1053 Sig(sfCounterpartySignature, lender),
1054 kInterestRate(TenthBips32{24'346}),
1055 kPaymentTotal(24),
1056 kPaymentInterval(86400 * 30),
1057 loanSetFee);
1058 env.close();
1059
1060 auto const payFee = Fee(env.current()->fees().base * 2);
1061 // Boundary check up front on the freshly-created loan.
1062 {
1063 auto const initial = getCurrentState(env, broker, loanKeylet);
1064 BEAST_EXPECT(
1065 initial.totalValue >=
1066 initial.principalOutstanding + initial.managementFeeOutstanding);
1067 }
1068
1069 // Six regular scheduled payments. If the invariant fires the
1070 // Ter(tesSUCCESS) assertion below catches it; the identity check
1071 // then re-asserts it in the test for a clearer failure message.
1072 std::uint32_t prevPaymentRemaining = 24;
1073 for (int i = 0; i < 6; ++i)
1074 {
1075 auto const loanSle = env.le(loanKeylet);
1076 if (!BEAST_EXPECT(loanSle))
1077 return;
1078 // Match the amount LoanPay expects for a scheduled payment:
1079 // periodicPayment rounded at loanScale, plus the flat service
1080 // fee (0 here by default, but included for robustness).
1081 auto const payAmount = STAmount{
1082 iouAsset,
1084 iouAsset, loanSle->at(sfPeriodicPayment), loanSle->at(sfLoanScale)) +
1085 loanSle->at(sfLoanServiceFee)};
1086 env(pay(borrower, loanKeylet.key, payAmount), payFee, Ter(tesSUCCESS));
1087 env.close();
1088
1089 auto const state = getCurrentState(env, broker, loanKeylet);
1090 BEAST_EXPECT(
1091 state.totalValue >= state.principalOutstanding + state.managementFeeOutstanding);
1092 BEAST_EXPECT(state.paymentRemaining == prevPaymentRemaining - 1);
1093 prevPaymentRemaining = state.paymentRemaining;
1094 }
1095 }
1096
1097 // Tests run under each entry in amendmentCombinations().
1098 void
1107
1108public:
1109 void
1110 run() override
1111 {
1114 for (auto const& features : jtx::amendmentCombinations(
1115 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
1116 runAmendmentSensitive(features);
1117 }
1118};
1119
1120BEAST_DEFINE_TESTSUITE(LoanInvariants, tx, xrpl);
1121
1122} // namespace xrpl::test
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
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 runAmendmentSensitive(FeatureBitset features)
void testLoanPayDebtDecreaseInvariant(FeatureBitset features)
void testLoanPayComputePeriodicPaymentInvariants(FeatureBitset features)
void testAccountSendMptMinAmountInvariant(FeatureBitset features)
void run() override
Runs the suite.
void testLoanPayOverpaymentScheduleInvariant(FeatureBitset features)
void testMinimumBrokerCoverConsistency(FeatureBitset features)
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_
jtx::Account brokerPseudoAccount(jtx::Env const &env, BrokerInfo const &broker, jtx::Account const &fallback)
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.
Keylet nextLoanKeylet(jtx::Env const &env, BrokerInfo const &broker)
static jtx::PrettyAsset createFundedRippleIouAsset(jtx::Env &env, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, Number const &lenderPay=1 '000 '000, Number const &borrowerPay=1 '000 '000)
std::string const iouCurrency_
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
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
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
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
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
Inject raw JSON.
Definition jtx_json.h:16
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:512
MPTID const & issuanceID() const
Definition mpt.h:768
void create(MPTCreate const &arg=MPTCreate{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:343
void authorize(MPTAuthorize const &arg=MPTAuthorize{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:455
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 max(T... args)
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
Keylet computation functions.
Definition Indexes.h:40
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
Keylet keylet(test::jtx::Account const &subject, test::jtx::Account const &issuer, std::string_view credType)
Definition credentials.h:21
json::Value set(AccountID const &account, UInt256 const &loanBrokerID, Number principalRequested, std::uint32_t flags)
auto const kOverpaymentInterestRate
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 trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
static MPTInit const kMptInitNoFund
Definition mpt.h:201
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
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
STAmount clawbackAmount(SLE::ConstRef vault, std::optional< STAmount > const &maybeAmount, AccountID const &account)
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:127
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:139
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:794
int getAssetsTotalScale(SLE::ConstRef vaultSle)
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 minimumBrokerCover(Number const &debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
constexpr Number abs(Number x) noexcept
Definition Number.h:876
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 roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
@ temINVALID
Definition TER.h:98
@ tecINSUFFICIENT_FUNDS
Definition TER.h:333
@ tecPRECISION_LOSS
Definition TER.h:371
@ tecINSUFFICIENT_PAYMENT
Definition TER.h:335
@ tesSUCCESS
Definition TER.h:250
bool isRounded(Asset const &asset, Number const &value, std::int32_t scale)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
UInt256 key
Definition Keylet.h:21
int vaultScale(jtx::Env const &env) const
Helper class to compare the expected state of a loan and loan broker against the data in the ledger.
T to_string(T... args)