xrpld
Loading...
Searching...
No Matches
LoanLifecycle_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/batch.h>
7#include <test/jtx/fee.h>
8#include <test/jtx/flags.h>
9#include <test/jtx/jtx_json.h>
10#include <test/jtx/mpt.h>
11#include <test/jtx/pay.h>
12#include <test/jtx/ter.h>
13#include <test/jtx/trust.h>
14#include <test/jtx/utility.h>
15#include <test/jtx/vault.h>
16
17#include <xrpl/basics/Number.h>
18#include <xrpl/basics/base_uint.h>
19#include <xrpl/basics/strHex.h>
20#include <xrpl/beast/unit_test/suite.h>
21#include <xrpl/json/json_value.h>
22#include <xrpl/json/to_string.h>
23#include <xrpl/protocol/Feature.h>
24#include <xrpl/protocol/HashPrefix.h>
25#include <xrpl/protocol/Indexes.h>
26#include <xrpl/protocol/Issue.h>
27#include <xrpl/protocol/SField.h>
28#include <xrpl/protocol/SecretKey.h>
29#include <xrpl/protocol/SeqProxy.h>
30#include <xrpl/protocol/Serializer.h>
31#include <xrpl/protocol/TER.h>
32#include <xrpl/protocol/TxFlags.h>
33#include <xrpl/protocol/jss.h>
34
35#include <array>
36#include <cstddef>
37#include <cstdint>
38#include <map>
39#include <string_view>
40#include <vector>
41
42namespace xrpl::test {
43
45{
46private:
47 void
49 {
50 testcase("Lifecycle");
51 using namespace jtx;
52
53 // Create 3 loan brokers: one for XRP, one for an IOU, and one for
54 // an MPT. That'll require three corresponding SAVs.
55 Env env(*this, features);
56
57 Account const issuer{"issuer"};
58 // For simplicity, lender will be the sole actor for the vault &
59 // brokers.
60 Account const lender{"lender"};
61 // Borrower only wants to borrow
62 Account const borrower{"borrower"};
63 // Evan will attempt to be naughty
64 Account const evan{"evan"};
65 // Do not fund alice
66 Account const alice{"alice"};
67
68 // Fund the accounts and trust lines with the same amount so that
69 // tests can use the same values regardless of the asset.
70 env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan));
71 env.close();
72
73 // Create assets
74 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
75 PrettyAsset const iouAsset = issuer[iouCurrency_];
76 env(trust(lender, iouAsset(10'000'000)));
77 env(trust(borrower, iouAsset(10'000'000)));
78 env(trust(evan, iouAsset(10'000'000)));
79 env(pay(issuer, evan, iouAsset(1'000'000)));
80 env(pay(issuer, lender, iouAsset(10'000'000)));
81 // Fund the borrower with enough to cover interest and fees
82 env(pay(issuer, borrower, iouAsset(10'000)));
83 env.close();
84
85 MPTTester mptt{env, issuer, kMptInitNoFund};
86 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
87 // Scale the MPT asset a little bit so we can get some interest
88 PrettyAsset const mptAsset{mptt.issuanceID(), 100};
89 mptt.authorize({.account = lender});
90 mptt.authorize({.account = borrower});
91 mptt.authorize({.account = evan});
92 env(pay(issuer, lender, mptAsset(10'000'000)));
93 env(pay(issuer, evan, mptAsset(1'000'000)));
94 // Fund the borrower with enough to cover interest and fees
95 env(pay(issuer, borrower, mptAsset(10'000)));
96 env.close();
97
98 std::array const assets{iouAsset, xrpAsset, mptAsset};
99
100 // Create vaults and loan brokers
102 brokers.reserve(assets.size());
103 for (auto const& asset : assets)
104 {
106 env, asset, lender, BrokerParameters{.data = "spam spam spam spam"}));
107 }
108
109 // Create and update Loans
110 for (auto const& broker : brokers)
111 {
112 for (int amountExponent = 3; amountExponent >= 3; --amountExponent)
113 {
114 Number const loanAmount{1, amountExponent};
115 for (int interestExponent = 0; interestExponent >= 0; --interestExponent)
116 {
117 testCaseWrapper(env, mptt, assets, broker, loanAmount, interestExponent);
118 }
119 }
120
121 if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID));
122 BEAST_EXPECT(brokerSle))
123 {
124 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
125 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
126
127 auto const coverAvailable = brokerSle->at(sfCoverAvailable);
129 lender, broker.brokerID, STAmount(broker.asset, coverAvailable)));
130 env.close();
131
132 brokerSle = env.le(keylet::loanBroker(broker.brokerID));
133 BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0);
134 }
135 // Verify we can delete the loan broker
136 env(loan_broker::del(lender, broker.brokerID));
137 env.close();
138 }
139 }
140
141 void
143 {
144 testcase << "Self Loan";
145
146 using namespace jtx;
147 using namespace std::chrono_literals;
148 // Create 3 loan brokers: one for XRP, one for an IOU, and one for
149 // an MPT. That'll require three corresponding SAVs.
150 Env env(*this, features);
151
152 Account const issuer{"issuer"};
153 // For simplicity, lender will be the sole actor for the vault &
154 // brokers.
155 Account const lender{"lender"};
156
157 // Fund the accounts and trust lines with the same amount so that
158 // tests can use the same values regardless of the asset.
159 env.fund(XRP(100'000'000), issuer, noripple(lender));
160 env.close();
161
162 // Use an XRP asset for simplicity
163 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
164
165 // Create vaults and loan brokers
166 BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)};
167
168 using namespace loan;
169
170 auto const loanSetFee = Fee(env.current()->fees().base * 2);
171 Number const principalRequest{1, 3};
172
173 // The LoanSet json can be created without a counterparty signature,
174 // but it will not pass preflight
175 auto createJson = env.json(
176 set(lender, broker.brokerID, broker.asset(principalRequest).value()), Fee(loanSetFee));
177 env(createJson, Ter(temBAD_SIGNER));
178
179 // Adding an empty counterparty signature object also fails, but
180 // at the RPC level.
181 createJson = env.json(createJson, Json(sfCounterpartySignature, json::ValueType::Object));
182 env(createJson, Ter(telENV_RPC_FAILED));
183
184 if (auto const jt = env.jt(createJson); BEAST_EXPECT(jt.stx))
185 {
186 Serializer s;
187 jt.stx->add(s);
188 auto const jr = env.rpc("submit", strHex(s.slice()));
189
190 BEAST_EXPECT(jr.isMember(jss::result));
191 auto const jResult = jr[jss::result];
192 BEAST_EXPECT(jResult[jss::error] == "invalidTransaction");
193 BEAST_EXPECT(
194 jResult[jss::error_exception] ==
195 "fails local checks: Transaction has bad signature.");
196 }
197
198 // Copy the transaction signature into the counterparty signature.
199 json::Value counterpartyJson{json::ValueType::Object};
200 counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
201 counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
202 if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
203 counterpartyJson[sfSigners] = createJson[sfSigners];
204
205 // The duplicated signature does not work: the counterparty signs a
206 // different prefix than the account.
207 env(env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)),
209
210 // Signing the counterparty field itself works, even though the lender
211 // is both the borrower and the counterparty.
212 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
213 env(createJson);
214
215 env.close();
216
217 auto const startDate = env.current()->header().parentCloseTime;
218
219 // Loan is successfully created
220 {
221 auto const res = env.rpc("account_objects", lender.human());
222 auto const objects = res[jss::result][jss::account_objects];
223
225 BEAST_EXPECT(objects.size() == 4);
226 for (auto const& object : objects)
227 {
228 ++types[object[sfLedgerEntryType].asString()];
229 }
230 BEAST_EXPECT(types.size() == 4);
231 for (std::string const type : {"MPToken", "Vault", "LoanBroker", "Loan"})
232 {
233 BEAST_EXPECT(types[type] == 1);
234 }
235 }
236 auto const loanID = [&]() {
238 params[jss::account] = lender.human();
239 params[jss::type] = "Loan";
240 auto const res = env.rpc("json", "account_objects", to_string(params));
241 auto const objects = res[jss::result][jss::account_objects];
242
243 BEAST_EXPECT(objects.size() == 1);
244
245 auto const loan = objects[0u];
246 BEAST_EXPECT(loan[sfBorrower] == lender.human());
247 // soeDEFAULT fields are not returned if they're in the default
248 // state
249 BEAST_EXPECT(!loan.isMember(sfCloseInterestRate));
250 BEAST_EXPECT(!loan.isMember(sfClosePaymentFee));
251 BEAST_EXPECT(loan[sfFlags] == 0);
252 BEAST_EXPECT(loan[sfGracePeriod] == 60);
253 BEAST_EXPECT(!loan.isMember(sfInterestRate));
254 BEAST_EXPECT(!loan.isMember(sfLateInterestRate));
255 BEAST_EXPECT(!loan.isMember(sfLatePaymentFee));
256 BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID));
257 BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee));
258 BEAST_EXPECT(loan[sfLoanSequence] == 1);
259 BEAST_EXPECT(!loan.isMember(sfLoanServiceFee));
260 BEAST_EXPECT(loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60);
261 BEAST_EXPECT(!loan.isMember(sfOverpaymentFee));
262 BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate));
263 BEAST_EXPECT(loan[sfPaymentInterval] == 60);
264 BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000");
265 BEAST_EXPECT(loan[sfPaymentRemaining] == 1);
266 BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDueDate));
267 BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000");
268 BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000");
269 BEAST_EXPECT(!loan.isMember(sfLoanScale));
270 BEAST_EXPECT(loan[sfStartDate].asUInt() == startDate.time_since_epoch().count());
271
272 return loan["index"].asString();
273 }();
274 auto const loanKeylet{keylet::loan(UInt256{std::string_view(loanID)})};
275
276 env.close(startDate);
277
278 // Make a payment
279 env(pay(lender, loanKeylet.key, broker.asset(1000)));
280 }
281
282 void
284 {
285 testcase << "Issuer Loan";
286
287 using namespace jtx;
288 using namespace loan;
289 Account const issuer("issuer");
290 Account const borrower = issuer;
291 Account const lender("lender");
292 Env env(*this);
293
294 env.fund(XRP(1'000), issuer, lender);
295
296 static constexpr std::int64_t kIssuerBalance = 10'000'000;
297 MPTTester const asset(
298 {.env = env, .issuer = issuer, .holders = {lender}, .pay = kIssuerBalance});
299
300 BrokerParameters const brokerParams{
301 .debtMax = 200,
302 };
303 auto const broker = createVaultAndBroker(env, asset, lender, brokerParams);
304 auto const loanSetFee = Fee(env.current()->fees().base * 2);
305 // Create Loan
306 env(set(borrower, broker.brokerID, 200), Sig(sfCounterpartySignature, lender), loanSetFee);
307 env.close();
308 // Issuer should not create MPToken
309 BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer)));
310 // Issuer "borrowed" 200, OutstandingAmount decreased by 200
311 BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance + 200));
312 // Pay Loan
313 auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(1));
314 env(pay(borrower, loanKeylet.key, asset(200)));
315 env.close();
316 // Issuer "re-payed" 200, OutstandingAmount increased by 200
317 BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance));
318 }
319
320 void
322 {
323 testcase("Test Borrower is Broker");
324 using namespace jtx;
325 using namespace loan;
326 Account const broker{"broker"};
327 Account const issuer{"issuer"};
328 Account const borrower{"borrower"};
329 Account const depositor{"depositor"};
330
331 auto testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) {
332 Env env(*this);
333 Vault const vault(env);
334
335 if (borrower == broker)
336 {
337 env.fund(XRP(10'000), broker, issuer, depositor);
338 }
339 else
340 {
341 env.fund(XRP(10'000), broker, borrower, issuer, depositor);
342 }
343 env.close();
344
345 auto const xrpFee = XRP(100);
346 auto const txFee = Fee(xrpFee);
347
348 STAmount const debtMaximumRequest = getMaxDebt(env);
349
350 auto const& asset = debtMaximumRequest.asset();
351 auto const initialVault = asset(debtMaximumRequest * 100);
352
353 // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim
354 // only accepts closed-ended vaults, so build one and advance
355 // past SubscriptionDate before creating broker/loan.
356 auto [tx, vaultKeylet, subscriptionDate] =
357 vault.createClosedEnded({.owner = broker, .asset = asset});
358 env(tx, txFee);
359 env.close();
360
361 env(vault.deposit(
362 {.depositor = depositor, .id = vaultKeylet.key, .amount = initialVault}),
363 txFee);
364 env.close();
365
366 vault.closePastSubscription(subscriptionDate);
367
368 auto const brokerKeylet =
369 keylet::loanBroker(broker.id(), SeqProxy::rawSequence(env.seq(broker)));
370
371 env(loan_broker::set(broker, vaultKeylet.key), txFee);
372 env.close();
373
374 auto const serviceFee = 101;
375
376 env(set(broker, brokerKeylet.key, debtMaximumRequest),
377 kCounterparty(borrower),
378 Sig(sfCounterpartySignature, borrower),
379 kLoanServiceFee(serviceFee),
380 kPaymentTotal(10),
381 txFee);
382 env.close();
383
384 std::uint32_t const loanSequence = 1;
385 auto const loanKeylet =
386 keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(loanSequence));
387
388 auto const brokerBalanceBefore = env.balance(broker, asset);
389
390 if (auto const loanSle = env.le(loanKeylet); env.test.BEAST_EXPECT(loanSle))
391 {
392 auto const payment = loanSle->at(sfPeriodicPayment);
393 auto const totalPayment = payment + serviceFee;
394 env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)), txFee);
395 env.close();
396 if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
397 {
398 auto const expected = [&]() {
399 // The service fee is transferred to the broker if
400 // a borrower is not the broker
401 if (borrower != broker)
402 return brokerBalanceBefore.number() + serviceFee;
403 // Since a borrower is the broker, the payment is
404 // transferred to the Vault from the broker but not
405 // the service fee.
406 // If the asset is XRP then the broker pays the txFee.
407 if (asset.native())
408 return brokerBalanceBefore.number() - payment - xrpFee.number();
409 return brokerBalanceBefore.number() - payment;
410 }();
411 BEAST_EXPECT(env.balance(broker, asset).value() == asset(expected).value());
412 }
413 }
414 };
415 // Test when a borrower is the broker and is not to verify correct
416 // service fee transfer in both cases.
417 for (auto const& borrowerAcct : {broker, borrower})
418 {
419 testLoanAsset(
420 [&](Env&) -> STAmount { return STAmount{XRPAmount{200'000}}; }, borrowerAcct);
421 testLoanAsset(
422 [&](Env& env) -> STAmount {
423 auto const iou = issuer["USD"];
424 env(trust(broker, iou(1'000'000'000)));
425 env(trust(depositor, iou(1'000'000'000)));
426 env(pay(issuer, broker, iou(100'000'000)));
427 env(pay(issuer, depositor, iou(100'000'000)));
428 env.close();
429 return iou(200'000);
430 },
431 borrowerAcct);
432 testLoanAsset(
433 [&](Env& env) -> STAmount {
434 MPTTester const mpt(
435 {.env = env,
436 .issuer = issuer,
437 .holders = {broker, depositor},
438 .pay = 100'000'000});
439 return mpt(200'000);
440 },
441 borrowerAcct);
442 }
443 }
444
445 void
447 {
448 testcase("RIPD-4096 - Issuer as borrower");
449
450 using namespace jtx;
451
452 Account const issuer("issuer");
453 Account const lender("lender");
454
455 BrokerParameters const brokerParams{
456 .vaultDeposit = 100'000,
457 .debtMax = 0,
458 .coverRateMin = TenthBips32{0},
459 .managementFeeRate = TenthBips16{0},
460 .coverRateLiquidation = TenthBips32{0}};
461 LoanParameters const loanParams{
462 .account = lender, .counter = issuer, .principalRequest = Number{10000}};
463
464 auto const assetType = AssetType::IOU;
465
466 Env env{*this, features};
467
468 auto loanResult =
469 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer);
470
471 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
472 return;
473
474 auto broker = std::get<BrokerInfo>(*loanResult);
475 auto loanKeylet = std::get<Keylet>(*loanResult);
476 auto pseudoAcct = std::get<Account>(*loanResult);
477
478 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
479
481 env,
482 broker,
483 loanParams,
484 loanKeylet,
485 verifyLoanStatus,
486 issuer,
487 lender,
488 issuer,
489 PaymentParameters{.showStepBalances = true});
490 }
491
492 void
494 {
495 // From FIND-001
496 testcase << "Batch Bypass Counterparty";
497
498 bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
499
500 using namespace jtx;
501 using namespace std::chrono_literals;
502 Env env(*this, features);
503
504 Account const lender{"lender"};
505 Account const borrower{"borrower"};
506
507 BrokerParameters const brokerParams;
508 env.fund(XRP(brokerParams.vaultDeposit * 100), lender, borrower);
509 env.close();
510
511 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
512
513 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
514
515 using namespace loan;
516
517 auto const loanSetFee = Fee(env.current()->fees().base * 2);
518 Number const principalRequest{1, 3};
519
520 auto forgedLoanSet = set(borrower, broker.brokerID, principalRequest, 0);
521
523 randomData[jss::SigningPubKey] = json::StaticString{"2600"};
525 sigObject[jss::SigningPubKey] = strHex(lender.pk().slice());
526 Serializer ss;
528 parse(randomData).addWithoutSigningFields(ss);
529 auto const sig = xrpl::sign(borrower.pk(), borrower.sk(), ss.slice());
530 sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()});
531
532 forgedLoanSet[json::StaticString{"CounterpartySignature"}] = sigObject;
533
534 // ? Fails because the lender hasn't signed the tx
535 env(env.json(forgedLoanSet, Fee(loanSetFee)), Ter(telENV_RPC_FAILED));
536
537 auto const seq = env.seq(borrower);
538 auto const batchFee = batch::calcBatchFee(env, 1, 2);
539 // ! Should fail because the lender hasn't signed the tx
540 env(batch::outer(borrower, seq, batchFee, tfAllOrNothing),
541 batch::Inner(forgedLoanSet, seq + 1),
542 batch::Inner(pay(borrower, lender, XRP(1)), seq + 2),
543 Ter(lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH));
544 env.close();
545
546 // ? Check that the loan was NOT created
547 {
549 params[jss::account] = borrower.human();
550 params[jss::type] = "Loan";
551 auto const res = env.rpc("json", "account_objects", to_string(params));
552 auto const objects = res[jss::result][jss::account_objects];
553 BEAST_EXPECT(objects.size() == 0);
554 }
555 }
556
557 // Integration test: full lifecycle of a $1B loan in the bug regime.
558 // Verifies that the vault collects the economically-correct interest
559 // income and that conservation holds at the trust-line level.
560 //
561 // Pre-fix (closed-form `power(1+r, n) - 1`): vault collected only
562 // ~$0.058 per $1B due to cancellation of `(1+r)^n - 1` at r*n ~ 5.7e-10.
563 // Post-fix (hybrid binomial path): vault collects ~$0.38 per $1B,
564 // matching the value computed independently with arbitrary-precision
565 // Decimal arithmetic.
566 void
568 {
569 testcase("integration: full loan lifecycle, vault interest at near-zero rate");
570
571 using namespace jtx;
572 using namespace jtx::loan;
573 using namespace std::chrono_literals;
574 Env env(*this, all_);
575
576 Account const issuer{"issuer"};
577 Account const lender{"lender"};
578 Account const borrower{"borrower"};
579
580 env.fund(XRP(1'000'000), issuer, lender, borrower);
581 env.close();
582 env(fset(issuer, asfDefaultRipple));
583 env.close();
584
585 PrettyAsset const iouAsset = issuer["USD"];
586 STAmount const trustLimit{iouAsset.raw(), Number{1, 17}};
587 env(trust(lender, trustLimit));
588 env(trust(borrower, trustLimit));
589 env.close();
590 env(pay(issuer, lender, iouAsset(5'000'000'000LL)));
591 env(pay(issuer, borrower, iouAsset(5'000'000'000LL)));
592 env.close();
593
594 auto usdBalance = [&](Account const& a) {
595 return env.balance(a, iouAsset.raw().get<Issue>()).value();
596 };
597 STAmount const borrowerStartBal = usdBalance(borrower);
598
599 BrokerParameters const brokerParams{
600 .vaultDeposit = Number{2, 9},
601 .debtMax = Number{0},
602 .coverRateMin = TenthBips32{0},
603 .coverDeposit = 0,
604 .managementFeeRate = TenthBips16{0},
605 .coverRateLiquidation = TenthBips32{0}};
606 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)};
607
608 auto const vaultBefore = env.le(broker.vaultKeylet());
609 if (!BEAST_EXPECT(vaultBefore))
610 return;
611 Number const vaultAvailableBefore = vaultBefore->at(sfAssetsAvailable);
612
613 // Loan: $1B principal, 3 payments, 600s interval, rate=1 TenthBips32.
614 auto const loanSetFee = Fee(env.current()->fees().base * 2);
615 Number const principalRequest{1, 9};
616 auto createJson = env.json(
617 set(borrower, broker.brokerID, principalRequest),
618 Fee(loanSetFee),
619 Json(sfCounterpartySignature, json::ValueType::Object));
620 createJson["InterestRate"] = 1;
621 createJson["PaymentTotal"] = 3;
622 createJson["PaymentInterval"] = 600;
623
624 auto const loanKeylet = nextLoanKeylet(env, broker);
625 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
626 env(createJson, Ter(tesSUCCESS));
627 env.close();
628
629 auto const loanSle = env.le(loanKeylet);
630 if (!BEAST_EXPECT(loanSle))
631 return;
632 Number const expectedTotalInterest =
633 loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding);
634
635 env(pay(borrower, loanKeylet.key, iouAsset(1'500'000'000LL)), Ter(tesSUCCESS));
636 env.close();
637
638 auto const vaultAfter = env.le(broker.vaultKeylet());
639 if (!BEAST_EXPECT(vaultAfter))
640 return;
641 Number const vaultAvailableAfter = vaultAfter->at(sfAssetsAvailable);
642 Number const vaultGain = vaultAvailableAfter - vaultAvailableBefore;
643
644 STAmount const borrowerEndBal = usdBalance(borrower);
645 STAmount const borrowerNetOut = borrowerStartBal - borrowerEndBal;
646
647 // Self-consistency: vault gained exactly the expected interest
648 // computed at LoanSet, and the borrower's outflow matches.
649 BEAST_EXPECT(vaultGain == expectedTotalInterest);
650 BEAST_EXPECT(Number(borrowerNetOut) == expectedTotalInterest);
651
652 // Mathematical correctness: the total interest for this loan
653 // configuration is 0.38051750382930729983, calculated
654 // independently using 50-digit Decimal arithmetic (no
655 // cancellation possible at that precision). At Number's 19-digit
656 // mantissa this rounds to 0.38051750382930729 — the literal
657 // below. The vault's actual gain must agree to within
658 // sub-microcent precision.
659 Number const decimalReference{38051750382930729LL, -17};
660 Number const tolerance{1, -6}; // 1e-6 USD = sub-microcent
661 Number const error = abs(vaultGain - decimalReference);
662 BEAST_EXPECTS(
663 error < tolerance,
664 "vault gain " + to_string(vaultGain) + " differs from Decimal reference " +
665 to_string(decimalReference) + " by " + to_string(error) + " — exceeds tolerance " +
666 to_string(tolerance));
667 }
668
669 void
676
677 // Tests run under each entry in amendmentCombinations().
678 void
680 {
681 testLifecycle(features);
682 testSelfLoan(features);
683 testIssuerIsBorrower(features);
685 }
686
687public:
688 void
689 run() override
690 {
692 for (auto const& features : jtx::amendmentCombinations(
693 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
694 runAmendmentSensitive(features);
695 }
696};
697
698BEAST_DEFINE_TESTSUITE(LoanLifecycle, tx, xrpl);
699
700} // namespace xrpl::test
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Lightweight wrapper to tag static string.
Definition json_value.h:48
Represents a JSON value.
Definition json_value.h:117
constexpr TIss const & get() const
A currency issued by an account.
Definition Issue.h:18
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
Slice slice() const noexcept
Definition PublicKey.h:115
Asset const & asset() const
Definition STAmount.h:496
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
Slice slice() const noexcept
Definition Serializer.h:141
An immutable linear range of bytes.
Definition Slice.h:28
void runAmendmentSensitive(FeatureBitset features)
void testBatchBypassCounterparty(FeatureBitset features)
void testSelfLoan(FeatureBitset features)
void testIssuerIsBorrower(FeatureBitset features)
void run() override
Runs the suite.
void testLifecycle(FeatureBitset features)
Shared base for the Loan*_test family under src/test/app/lending/.
void testCaseWrapper(jtx::Env &env, jtx::MPTTester &mptt, std::array< TAsset, NAsset > const &assets, BrokerInfo const &broker, Number const &loanAmount, int interestExponent)
Wrapper to run a series of lifecycle tests for a given asset and loan amount.
FeatureBitset const all_
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const &params=BrokerParameters::defaults())
Keylet nextLoanKeylet(jtx::Env const &env, BrokerInfo const &broker)
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)
std::string const iouCurrency_
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
SecretKey const & sk() const
Return the secret key.
Definition jtx/Account.h:93
std::string const & human() const
Returns the human readable public key.
PublicKey const & pk() const
Return the public key.
Definition jtx/Account.h:84
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
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
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
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
Adds an inner Batch transaction to a JTx and autofills it.
Definition batch.h:91
T emplace_back(T... args)
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
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 outer(jtx::Account const &account, uint32_t seq, STAmount const &fee, std::uint32_t flags)
Build an outer Batch transaction JSON object.
Definition batch.cpp:55
XRPAmount calcBatchFee(jtx::Env const &env, uint32_t const &numSigners, uint32_t const &txns=0)
Calculate the expected outer Batch transaction fee.
Definition batch.cpp:37
json::Value set(AccountID const &account, UInt256 const &vaultId, uint32_t flags)
json::Value coverWithdraw(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value del(AccountID const &account, UInt256 const &brokerID, 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
XRPAmount txFee(Env const &env, std::uint16_t n)
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
Definition Env.h:86
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
@ 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
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:13
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
constexpr Number abs(Number x) noexcept
Definition Number.h:876
@ TxSign
inner transaction to sign
Definition HashPrefix.h:64
@ temBAD_SIGNATURE
Definition TER.h:93
@ temINVALID_INNER_BATCH
Definition TER.h:131
@ temBAD_SIGNER
Definition TER.h:103
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
@ tesSUCCESS
Definition TER.h:250
T parse(T... args)
T reserve(T... args)
T size(T... args)
Helper class to compare the expected state of a loan and loan broker against the data in the ledger.
STAmount const & value() const