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