xrpld
Loading...
Searching...
No Matches
LoanSet_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/mpt.h>
9#include <test/jtx/multisign.h>
10#include <test/jtx/noop.h>
11#include <test/jtx/pay.h>
12#include <test/jtx/trust.h>
13#include <test/jtx/vault.h>
14
15#include <xrpl/basics/Number.h>
16#include <xrpl/basics/chrono.h>
17#include <xrpl/beast/unit_test/suite.h>
18#include <xrpl/json/json_value.h>
19#include <xrpl/protocol/Feature.h>
20#include <xrpl/protocol/Indexes.h>
21#include <xrpl/protocol/Issue.h>
22#include <xrpl/protocol/LedgerFormats.h>
23#include <xrpl/protocol/Protocol.h>
24#include <xrpl/protocol/SField.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#include <xrpl/protocol/XRPAmount.h>
30#include <xrpl/tx/transactors/lending/LoanSet.h>
31
32#include <array>
33#include <cstdint>
34#include <functional>
35#include <string>
36#include <utility>
37#include <vector>
38
39namespace xrpl::test {
40
42{
43private:
44 void
46 {
47 using namespace jtx;
48
49 Account const issuer{"issuer"};
50 Account const lender{"lender"};
51 Account const borrower{"borrower"};
52
53 struct CaseArgs
54 {
55 bool requireAuth = false;
56 bool authorizeBorrower = false;
57 int initialXRP = 1'000'000;
58 };
59
60 auto const testCase = [&, this](
61 std::function<void(Env&, BrokerInfo const&, MPTTester&)> mptTest,
62 std::function<void(Env&, BrokerInfo const&)> iouTest,
63 CaseArgs args = {}) {
64 Env env(*this, features);
65 env.fund(XRP(args.initialXRP), issuer, lender, borrower);
66 env.close();
67 if (args.requireAuth)
68 {
69 env(fset(issuer, asfRequireAuth));
70 env.close();
71 }
72
73 // We need two different asset types, MPT and IOU. Prepare MPT
74 // first
75 MPTTester mptt{env, issuer, kMptInitNoFund};
76
77 auto const kNone = LedgerSpecificFlags(0);
78 mptt.create(
79 {.flags = tfMPTCanTransfer | tfMPTCanLock |
80 (args.requireAuth ? tfMPTRequireAuth : kNone)});
81 env.close();
82 PrettyAsset const mptAsset = mptt.issuanceID();
83 mptt.authorize({.account = lender});
84 mptt.authorize({.account = borrower});
85 env.close();
86 if (args.requireAuth)
87 {
88 mptt.authorize({.account = issuer, .holder = lender});
89 if (args.authorizeBorrower)
90 mptt.authorize({.account = issuer, .holder = borrower});
91 env.close();
92 }
93
94 env(pay(issuer, lender, mptAsset(10'000'000)));
95 env.close();
96
97 // Prepare IOU
98 PrettyAsset const iouAsset = issuer[iouCurrency_];
99 env(trust(lender, iouAsset(10'000'000)));
100 env(trust(borrower, iouAsset(10'000'000)));
101 env.close();
102 if (args.requireAuth)
103 {
104 env(trust(issuer, iouAsset(0), lender, tfSetfAuth));
105 env(pay(issuer, lender, iouAsset(10'000'000)));
106 if (args.authorizeBorrower)
107 {
108 env(trust(issuer, iouAsset(0), borrower, tfSetfAuth));
109 env(pay(issuer, borrower, iouAsset(10'000)));
110 }
111 }
112 else
113 {
114 env(pay(issuer, lender, iouAsset(10'000'000)));
115 env(pay(issuer, borrower, iouAsset(10'000)));
116 }
117 env.close();
118
119 // Create vaults and loan brokers
120 std::array const assets{mptAsset, iouAsset};
122 brokers.reserve(assets.size());
123 for (auto const& asset : assets)
124 {
125 brokers.emplace_back(createVaultAndBroker(env, asset, lender));
126 }
127
128 if (mptTest)
129 mptTest(env, brokers[0], mptt);
130 if (iouTest)
131 iouTest(env, brokers[1]);
132 };
133
134 testCase(
135 [&, this](Env& env, BrokerInfo const& broker, auto&) {
136 using namespace loan;
137 Number const principalRequest = broker.asset(1'000).value();
138
139 testcase("MPT issuer is borrower, issuer submits");
140 env(set(issuer, broker.brokerID, principalRequest),
141 kCounterparty(lender),
142 Sig(sfCounterpartySignature, lender),
143 Fee(env.current()->fees().base * 5));
144
145 testcase("MPT issuer is borrower, lender submits");
146 env(set(lender, broker.brokerID, principalRequest),
147 kCounterparty(issuer),
148 Sig(sfCounterpartySignature, issuer),
149 Fee(env.current()->fees().base * 5));
150 },
151 [&, this](Env& env, BrokerInfo const& broker) {
152 using namespace loan;
153 Number const principalRequest = broker.asset(1'000).value();
154
155 testcase("IOU issuer is borrower, issuer submits");
156 env(set(issuer, broker.brokerID, principalRequest),
157 kCounterparty(lender),
158 Sig(sfCounterpartySignature, lender),
159 Fee(env.current()->fees().base * 5));
160
161 testcase("IOU issuer is borrower, lender submits");
162 env(set(lender, broker.brokerID, principalRequest),
163 kCounterparty(issuer),
164 Sig(sfCounterpartySignature, issuer),
165 Fee(env.current()->fees().base * 5));
166 },
167 CaseArgs{.requireAuth = true});
168
169 testCase(
170 [&, this](Env& env, BrokerInfo const& broker, auto&) {
171 using namespace loan;
172 Number const principalRequest = broker.asset(1'000).value();
173
174 testcase("MPT unauthorized borrower, borrower submits");
175 env(set(borrower, broker.brokerID, principalRequest),
176 kCounterparty(lender),
177 Sig(sfCounterpartySignature, lender),
178 Fee(env.current()->fees().base * 5),
179 Ter{tecNO_AUTH});
180
181 testcase("MPT unauthorized borrower, lender submits");
182 env(set(lender, broker.brokerID, principalRequest),
183 kCounterparty(borrower),
184 Sig(sfCounterpartySignature, borrower),
185 Fee(env.current()->fees().base * 5),
186 Ter{tecNO_AUTH});
187 },
188 [&, this](Env& env, BrokerInfo const& broker) {
189 using namespace loan;
190 Number const principalRequest = broker.asset(1'000).value();
191
192 testcase("IOU unauthorized borrower, borrower submits");
193 env(set(borrower, broker.brokerID, principalRequest),
194 kCounterparty(lender),
195 Sig(sfCounterpartySignature, lender),
196 Fee(env.current()->fees().base * 5),
197 Ter{tecNO_AUTH});
198
199 testcase("IOU unauthorized borrower, lender submits");
200 env(set(lender, broker.brokerID, principalRequest),
201 kCounterparty(borrower),
202 Sig(sfCounterpartySignature, borrower),
203 Fee(env.current()->fees().base * 5),
204 Ter{tecNO_AUTH});
205 },
206 CaseArgs{.requireAuth = true});
207
208 auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
209 Env const env{*this, testableAmendments()};
210 return {
211 env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(),
212 env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
213 }();
214
215 testCase(
216 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
217 using namespace loan;
218 Number const principalRequest = broker.asset(1'000).value();
219
220 testcase(
221 "MPT authorized borrower, borrower submits, borrower has "
222 "no reserve");
223 mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize});
224 env.close();
225
226 auto const mptoken = keylet::mptoken(mptt.issuanceID(), borrower);
227 auto const sleMPT1 = env.le(mptoken);
228 BEAST_EXPECT(sleMPT1 == nullptr);
229
230 // Burn some XRP
231 env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2))));
232 env.close();
233
234 // Cannot create loan, not enough reserve to create MPToken
235 env(set(borrower, broker.brokerID, principalRequest),
236 kCounterparty(lender),
237 Sig(sfCounterpartySignature, lender),
238 Fee(env.current()->fees().base * 5),
239 Ter{tecINSUFFICIENT_RESERVE});
240 env.close();
241
242 // Can create loan now, will implicitly create MPToken
243 env(pay(issuer, borrower, XRP(incReserve)));
244 env.close();
245 env(set(borrower, broker.brokerID, principalRequest),
246 kCounterparty(lender),
247 Sig(sfCounterpartySignature, lender),
248 Fee(env.current()->fees().base * 5));
249 env.close();
250
251 auto const sleMPT2 = env.le(mptoken);
252 BEAST_EXPECT(sleMPT2 != nullptr);
253 },
254 {},
255 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
256
257 testCase(
258 {},
259 [&, this](Env& env, BrokerInfo const& broker) {
260 using namespace loan;
261 Number const principalRequest = broker.asset(1'000).value();
262
263 testcase(
264 "IOU authorized borrower, borrower submits, borrower has "
265 "no reserve");
266 // Remove trust line from borrower to issuer
267 env.trust(broker.asset(0), borrower);
268 env.close();
269
270 env(pay(borrower, issuer, broker.asset(10'000)));
271 env.close();
272 auto const trustline = keylet::trustLine(borrower, broker.asset.raw().get<Issue>());
273 auto const sleLine1 = env.le(trustline);
274 BEAST_EXPECT(sleLine1 == nullptr);
275
276 // Burn some XRP
277 env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2))));
278 env.close();
279
280 // Cannot create loan, not enough reserve to create trust line
281 env(set(borrower, broker.brokerID, principalRequest),
282 kCounterparty(lender),
283 Sig(sfCounterpartySignature, lender),
284 Fee(env.current()->fees().base * 5),
285 Ter{tecNO_LINE_INSUF_RESERVE});
286 env.close();
287
288 // Can create loan now, will implicitly create trust line
289 env(pay(issuer, borrower, XRP(incReserve)));
290 env.close();
291 env(set(borrower, broker.brokerID, principalRequest),
292 kCounterparty(lender),
293 Sig(sfCounterpartySignature, lender),
294 Fee(env.current()->fees().base * 5));
295 env.close();
296
297 auto const sleLine2 = env.le(trustline);
298 BEAST_EXPECT(sleLine2 != nullptr);
299 },
300 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
301
302 testCase(
303 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
304 using namespace loan;
305 Number const principalRequest = broker.asset(1'000).value();
306
307 testcase(
308 "MPT authorized borrower, borrower submits, lender has "
309 "no reserve");
310 auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender);
311 auto const sleMPT1 = env.le(mptoken);
312 BEAST_EXPECT(sleMPT1 != nullptr);
313
314 env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount))));
315 env.close();
316
317 mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
318 env.close();
319
320 auto const sleMPT2 = env.le(mptoken);
321 BEAST_EXPECT(sleMPT2 == nullptr);
322
323 // Burn some XRP
324 env(noop(lender), Fee(XRP(incReserve)));
325 env.close();
326
327 // Cannot create loan, not enough reserve to create MPToken
328 env(set(borrower, broker.brokerID, principalRequest),
329 kLoanOriginationFee(broker.asset(1).value()),
330 kCounterparty(lender),
331 Sig(sfCounterpartySignature, lender),
332 Fee(env.current()->fees().base * 5),
333 Ter{tecINSUFFICIENT_RESERVE});
334 env.close();
335
336 // Can create loan now, will implicitly create MPToken
337 env(pay(issuer, lender, XRP(incReserve)));
338 env.close();
339 env(set(borrower, broker.brokerID, principalRequest),
340 kLoanOriginationFee(broker.asset(1).value()),
341 kCounterparty(lender),
342 Sig(sfCounterpartySignature, lender),
343 Fee(env.current()->fees().base * 5));
344 env.close();
345
346 auto const sleMPT3 = env.le(mptoken);
347 BEAST_EXPECT(sleMPT3 != nullptr);
348 },
349 {},
350 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
351
352 testCase(
353 {},
354 [&, this](Env& env, BrokerInfo const& broker) {
355 using namespace loan;
356 Number const principalRequest = broker.asset(1'000).value();
357
358 testcase(
359 "IOU authorized borrower, borrower submits, lender has no "
360 "reserve");
361 // Remove trust line from lender to issuer
362 env.trust(broker.asset(0), lender);
363 env.close();
364
365 auto const trustline = keylet::trustLine(lender, broker.asset.raw().get<Issue>());
366 auto const sleLine1 = env.le(trustline);
367 BEAST_EXPECT(sleLine1 != nullptr);
368
369 env(pay(lender, issuer, broker.asset(abs(sleLine1->at(sfBalance).value()))));
370 env.close();
371 auto const sleLine2 = env.le(trustline);
372 BEAST_EXPECT(sleLine2 == nullptr);
373
374 // Burn some XRP
375 env(noop(lender), Fee(XRP(incReserve)));
376 env.close();
377
378 // Cannot create loan, not enough reserve to create trust line
379 env(set(borrower, broker.brokerID, principalRequest),
380 kLoanOriginationFee(broker.asset(1).value()),
381 kCounterparty(lender),
382 Sig(sfCounterpartySignature, lender),
383 Fee(env.current()->fees().base * 5),
384 Ter{tecNO_LINE_INSUF_RESERVE});
385 env.close();
386
387 // Can create loan now, will implicitly create trust line
388 env(pay(issuer, lender, XRP(incReserve)));
389 env.close();
390 env(set(borrower, broker.brokerID, principalRequest),
391 kLoanOriginationFee(broker.asset(1).value()),
392 kCounterparty(lender),
393 Sig(sfCounterpartySignature, lender),
394 Fee(env.current()->fees().base * 5));
395 env.close();
396
397 auto const sleLine3 = env.le(trustline);
398 BEAST_EXPECT(sleLine3 != nullptr);
399 },
400 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
401
402 testCase(
403 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
404 using namespace loan;
405 Number const principalRequest = broker.asset(1'000).value();
406
407 testcase("MPT authorized borrower, unauthorized lender");
408 auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender);
409 auto const sleMPT1 = env.le(mptoken);
410 BEAST_EXPECT(sleMPT1 != nullptr);
411
412 env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount))));
413 env.close();
414
415 mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
416 env.close();
417
418 auto const sleMPT2 = env.le(mptoken);
419 BEAST_EXPECT(sleMPT2 == nullptr);
420
421 // Cannot create loan, lender not authorized to receive fee
422 env(set(borrower, broker.brokerID, principalRequest),
423 kLoanOriginationFee(broker.asset(1).value()),
424 kCounterparty(lender),
425 Sig(sfCounterpartySignature, lender),
426 Fee(env.current()->fees().base * 5),
427 Ter{tecNO_AUTH});
428 env.close();
429
430 // Cannot create loan, even without an origination fee
431 env(set(borrower, broker.brokerID, principalRequest),
432 kCounterparty(lender),
433 Sig(sfCounterpartySignature, lender),
434 Fee(env.current()->fees().base * 5),
435 Ter{tecNO_AUTH});
436 env.close();
437
438 // No MPToken for lender - no authorization and no payment
439 auto const sleMPT3 = env.le(mptoken);
440 BEAST_EXPECT(sleMPT3 == nullptr);
441 },
442 {},
443 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
444
445 testCase(
446 [&, this](Env& env, BrokerInfo const& broker, auto&) {
447 using namespace loan;
448 Number const principalRequest = broker.asset(1'000).value();
449
450 testcase("MPT authorized borrower, borrower submits");
451 env(set(borrower, broker.brokerID, principalRequest),
452 kCounterparty(lender),
453 Sig(sfCounterpartySignature, lender),
454 Fee(env.current()->fees().base * 5));
455 },
456 [&, this](Env& env, BrokerInfo const& broker) {
457 using namespace loan;
458 Number const principalRequest = broker.asset(1'000).value();
459
460 testcase("IOU authorized borrower, borrower submits");
461 env(set(borrower, broker.brokerID, principalRequest),
462 kCounterparty(lender),
463 Sig(sfCounterpartySignature, lender),
464 Fee(env.current()->fees().base * 5));
465 },
466 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
467
468 testCase(
469 [&, this](Env& env, BrokerInfo const& broker, auto&) {
470 using namespace loan;
471 Number const principalRequest = broker.asset(1'000).value();
472
473 testcase("MPT authorized borrower, lender submits");
474 env(set(lender, broker.brokerID, principalRequest),
475 kCounterparty(borrower),
476 Sig(sfCounterpartySignature, borrower),
477 Fee(env.current()->fees().base * 5));
478 },
479 [&, this](Env& env, BrokerInfo const& broker) {
480 using namespace loan;
481 Number const principalRequest = broker.asset(1'000).value();
482
483 testcase("IOU authorized borrower, lender submits");
484 env(set(lender, broker.brokerID, principalRequest),
485 kCounterparty(borrower),
486 Sig(sfCounterpartySignature, borrower),
487 Fee(env.current()->fees().base * 5));
488 },
489 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
490
491 jtx::Account const alice{"alice"};
492 jtx::Account const bella{"bella"};
493 auto const msigSetup = [&](Env& env, Account const& account) {
494 json::Value const tx1 = signers(account, 2, {{alice, 1}, {bella, 1}});
495 env(tx1);
496 env.close();
497 };
498
499 testCase(
500 [&, this](Env& env, BrokerInfo const& broker, auto&) {
501 using namespace loan;
502 msigSetup(env, lender);
503 Number const principalRequest = broker.asset(1'000).value();
504
505 testcase(
506 "MPT authorized borrower, borrower submits, lender "
507 "multisign");
508 env(set(borrower, broker.brokerID, principalRequest),
509 kCounterparty(lender),
510 Msig(sfCounterpartySignature, alice, bella),
511 Fee(env.current()->fees().base * 5));
512 },
513 [&, this](Env& env, BrokerInfo const& broker) {
514 using namespace loan;
515 msigSetup(env, lender);
516 Number const principalRequest = broker.asset(1'000).value();
517
518 testcase(
519 "IOU authorized borrower, borrower submits, lender "
520 "multisign");
521 env(set(borrower, broker.brokerID, principalRequest),
522 kCounterparty(lender),
523 Msig(sfCounterpartySignature, alice, bella),
524 Fee(env.current()->fees().base * 5));
525 },
526 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
527
528 testCase(
529 [&, this](Env& env, BrokerInfo const& broker, auto&) {
530 using namespace loan;
531 msigSetup(env, borrower);
532 Number const principalRequest = broker.asset(1'000).value();
533
534 testcase(
535 "MPT authorized borrower, lender submits, borrower "
536 "multisign");
537 env(set(lender, broker.brokerID, principalRequest),
538 kCounterparty(borrower),
539 Msig(sfCounterpartySignature, alice, bella),
540 Fee(env.current()->fees().base * 5));
541 },
542 [&, this](Env& env, BrokerInfo const& broker) {
543 using namespace loan;
544 msigSetup(env, borrower);
545 Number const principalRequest = broker.asset(1'000).value();
546
547 testcase(
548 "IOU authorized borrower, lender submits, borrower "
549 "multisign");
550 env(set(lender, broker.brokerID, principalRequest),
551 kCounterparty(borrower),
552 Msig(sfCounterpartySignature, alice, bella),
553 Fee(env.current()->fees().base * 5));
554 },
555 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
556
557 testCase(
558 [&, this](Env& env, BrokerInfo const& broker, auto&) {
559 using namespace loan;
560 Number const principalRequest = broker.asset(1'000).value();
561 Vault const vault{env};
562 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
563 tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit;
564 env(tx);
565 env.close();
566
567 testcase("Vault at maximum value");
568 env(set(issuer, broker.brokerID, principalRequest),
569 kCounterparty(lender),
570 kInterestRate(TenthBips32(10'000)),
571 Sig(sfCounterpartySignature, lender),
572 Fee(env.current()->fees().base * 5),
574 },
575 nullptr);
576
577 testCase(
578 [&, this](Env& env, BrokerInfo const& broker, auto&) {
579 using namespace loan;
580 Number const principalRequest = broker.asset(1'000).value();
581 Vault const vault{env};
582 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
583 tx[sfAssetsMaximum] =
584 BrokerParameters::defaults().vaultDeposit + broker.asset(1).number();
585 env(tx);
586 env.close();
587
588 testcase("Vault maximum value exceeded");
589 env(set(issuer, broker.brokerID, principalRequest),
590 kCounterparty(lender),
591 kInterestRate(TenthBips32(100'000)),
592 Sig(sfCounterpartySignature, lender),
593 Fee(env.current()->fees().base * 5),
594 kPaymentTotal(2),
595 kPaymentInterval(3600 * 24),
597 },
598 nullptr);
599 }
600
601 void
603 {
604 using namespace jtx;
605 using namespace loan;
606
607 bool const fix340Enabled = features[fixCleanup3_4_0];
608 testcase << "LoanSet: borrower and broker owner missing MPToken"
609 << (fix340Enabled ? "" : " pre-fixCleanup3_4_0");
610
611 Account const issuer{"issuer"};
612 Account const lender{"lender"};
613 Account const borrower{"borrower"};
614
615 Env env(*this, features);
616 env.fund(XRP(1'000'000), issuer, lender, borrower);
617 env.close();
618
619 MPTTester mptt{env, issuer, kMptInitNoFund};
620 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
621 env.close();
622 PrettyAsset const asset = mptt.issuanceID();
623 mptt.authorize({.account = lender});
624 mptt.authorize({.account = borrower});
625 env.close();
626
627 env(pay(issuer, lender, asset(10'000'000)));
628 env.close();
629
630 auto const broker = createVaultAndBroker(env, asset, lender);
631
632 // Delete borrower's asset MPToken.
633 mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize});
634 env.close();
635
636 // Pay out and delete the broker owner's asset MPToken.
637 auto const lenderMPToken = keylet::mptoken(mptt.issuanceID(), lender);
638 auto const sleLenderMPT = env.le(lenderMPToken);
639 if (!BEAST_EXPECT(sleLenderMPT))
640 return;
641 env(pay(lender, issuer, asset(sleLenderMPT->at(sfMPTAmount))));
642 env.close();
643 mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
644 env.close();
645
646 auto const borrowerMPToken = keylet::mptoken(mptt.issuanceID(), borrower);
647 auto const brokerKeylet = keylet::loanBroker(broker.brokerID);
648 auto const sleBrokerBefore = env.le(brokerKeylet);
649 if (!BEAST_EXPECT(sleBrokerBefore))
650 return;
651 auto const loanSequence = sleBrokerBefore->at(sfLoanSequence);
652 auto const debtTotalBefore = sleBrokerBefore->at(sfDebtTotal);
653 auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
654
655 auto const sleVaultBefore = env.le(keylet::vault(broker.vaultID));
656 if (!BEAST_EXPECT(sleVaultBefore))
657 return;
658 auto const assetsAvailableBefore = sleVaultBefore->at(sfAssetsAvailable);
659
660 env(set(borrower, broker.brokerID, asset(1'000).value()),
661 kLoanOriginationFee(asset(1).value()),
662 kCounterparty(lender),
663 Sig(sfCounterpartySignature, lender),
664 Fee(env.current()->fees().base * 5),
665 Ter{fix340Enabled ? TER{tesSUCCESS} : TER{tecINVARIANT_FAILED}});
666 env.close();
667
668 auto const sleBorrowerAfter = env.le(borrowerMPToken);
669 auto const sleLenderAfter = env.le(lenderMPToken);
670 auto const sleLoanAfter = env.le(loanKeylet);
671 auto const sleBrokerAfter = env.le(brokerKeylet);
672 auto const sleVaultAfter = env.le(keylet::vault(broker.vaultID));
673 if (!BEAST_EXPECT(sleVaultAfter))
674 return;
675 if (fix340Enabled)
676 {
677 if (!BEAST_EXPECT(sleBorrowerAfter && sleLenderAfter && sleLoanAfter && sleBrokerAfter))
678 return;
679 BEAST_EXPECT(sleBorrowerAfter->at(sfMPTAmount) == 999);
680 BEAST_EXPECT(sleLenderAfter->at(sfMPTAmount) == 1);
681 BEAST_EXPECT(sleLoanAfter->at(sfPrincipalOutstanding) == Number{1'000});
682 BEAST_EXPECT(sleBrokerAfter->at(sfLoanSequence) == loanSequence + 1);
683 BEAST_EXPECT(
684 sleVaultAfter->at(sfAssetsAvailable) == assetsAvailableBefore - Number{1'000});
685 }
686 else
687 {
688 // The whole transaction must roll back.
689 BEAST_EXPECT(!sleBorrowerAfter);
690 BEAST_EXPECT(!sleLenderAfter);
691 BEAST_EXPECT(!sleLoanAfter);
692 if (!BEAST_EXPECT(sleBrokerAfter))
693 return;
694 BEAST_EXPECT(sleBrokerAfter->at(sfLoanSequence) == loanSequence);
695 BEAST_EXPECT(sleBrokerAfter->at(sfDebtTotal) == debtTotalBefore);
696 BEAST_EXPECT(sleVaultAfter->at(sfAssetsAvailable) == assetsAvailableBefore);
697 }
698 }
699
700 // LoanSet in a closed-ended vault — phase gating and maturity bound.
701 void
703 {
704 testcase("LoanSet closed-ended: phase and maturity bound");
705 using namespace jtx;
706 using namespace loan;
707 using D = NetClock::duration;
708 using Tp = NetClock::time_point;
709
710 Account const issuer{"issuer"};
711 Account const lender{"lender"};
712 Account const borrower{"borrower"};
713
714 // Common loan schedule used by the phase-rejection cases below.
715 constexpr std::uint32_t kInterval = 3600u * 24u; // 1 day
716 constexpr std::uint32_t kTotal = 2u;
717
718 // featureLendingProtocolV1_1 is excluded from `all_` by convention (see the comment on
719 // `all_`), so callers must opt in. Closed-ended vaults are gated on this amendment; without
720 // it VaultCreate returns temDISABLED and every follow-on txn sees tecNO_ENTRY.
721 auto const withEnv = [&, this](auto&& body) {
722 Env env(*this, testableAmendments() | featureLendingProtocolV1_1);
723 env.fund(XRP(1'000'000'000), issuer, lender, borrower);
724 env.close();
725 PrettyAsset const asset{xrpIssue(), 1'000'000};
726 body(env, asset);
727 };
728
729 auto const setLoan = [&](Env& env, BrokerInfo const& broker, TER expected) {
730 env(set(lender, broker.brokerID, broker.asset(100).value()),
731 kCounterparty(borrower),
732 Sig(sfCounterpartySignature, borrower),
733 Fee(env.current()->fees().base * 5),
734 kPaymentTotal(kTotal),
735 kPaymentInterval(kInterval),
736 Ter(expected));
737 env.close();
738 };
739
740 // 1. Rejected during Subscription: the broker is created in Subscription (skipPhaseAdvance
741 // = true), then LoanSet is attempted before advancing past SubscriptionDate.
742 withEnv([&](Env& env, PrettyAsset const& asset) {
743 auto const broker = createVaultAndBroker(
744 env,
745 asset,
746 lender,
747 BrokerParameters{.vaultKind = VaultKind::ClosedEnded, .skipPhaseAdvance = true});
748 setLoan(env, broker, tecTOO_SOON);
749 });
750
751 // 2. Rejected during Redemption: broker is set up normally (which lands the vault in
752 // Investment), then advance the clock past RedemptionDate before attempting LoanSet.
753 withEnv([&](Env& env, PrettyAsset const& asset) {
754 auto const broker = createVaultAndBroker(
755 env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded});
756 BEAST_EXPECT(broker.redemptionDate.has_value());
757 using D = NetClock::duration;
758 using Tp = NetClock::time_point;
759 env.close(Tp{D{*broker.redemptionDate + 1}});
760 setLoan(env, broker, tecEXPIRED);
761 });
762
763 // 3. Accepted during Investment when the schedule comfortably fits before RedemptionDate.
764 withEnv([&](Env& env, PrettyAsset const& asset) {
765 auto const broker = createVaultAndBroker(
766 env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded});
767 setLoan(env, broker, tesSUCCESS);
768 });
769
770 // 4. Rejected during Investment when the loan's final payment would land fewer than
771 // kLoanRedemptionBuffer seconds before RedemptionDate. Use a tight redemptionOffset and a
772 // schedule whose final payment is well past that boundary.
773 withEnv([&](Env& env, PrettyAsset const& asset) {
774 constexpr std::uint32_t kRedemptionOffset = 3u * 24u * 3600u;
775 auto const broker = createVaultAndBroker(
776 env,
777 asset,
778 lender,
780 .vaultKind = VaultKind::ClosedEnded, .redemptionOffset = kRedemptionOffset});
781 env(set(lender, broker.brokerID, broker.asset(100).value()),
782 kCounterparty(borrower),
783 Sig(sfCounterpartySignature, borrower),
784 Fee(env.current()->fees().base * 5),
785 kPaymentTotal(10u),
786 kPaymentInterval(kInterval),
788 env.close();
789 });
790
791 // 5. Boundary: a finalPayment exactly kLoanRedemptionBuffer seconds before
792 // RedemptionDate is accepted; one second later is rejected. Uses payTotal = 1 so
793 // finalPayment = startDate + interval.
794 withEnv([&](Env& env, PrettyAsset const& asset) {
795 auto const broker = createVaultAndBroker(
796 env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded});
797 BEAST_EXPECT(broker.redemptionDate.has_value());
798
799 auto const startDate = env.now().time_since_epoch().count();
800 auto const acceptInterval = *broker.redemptionDate - kLoanRedemptionBuffer - startDate;
801 env(set(lender, broker.brokerID, broker.asset(100).value()),
802 kCounterparty(borrower),
803 Sig(sfCounterpartySignature, borrower),
804 Fee(env.current()->fees().base * 5),
805 kPaymentTotal(1u),
806 kPaymentInterval(acceptInterval),
807 Ter(tesSUCCESS));
808 env.close();
809
810 auto const rejectInterval = *broker.redemptionDate - (kLoanRedemptionBuffer - 1) -
811 env.now().time_since_epoch().count();
812 env(set(lender, broker.brokerID, broker.asset(100).value()),
813 kCounterparty(borrower),
814 Sig(sfCounterpartySignature, borrower),
815 Fee(env.current()->fees().base * 5),
816 kPaymentTotal(1u),
817 kPaymentInterval(rejectInterval),
819 env.close();
820 });
821
822 // 6. A vault whose Investment window is exactly kMinInvestmentPeriod can originate a
823 // minimum-interval, single-payment loan at the start of Investment, and rejects the same
824 // schedule once StartDate no longer leaves kLoanRedemptionBuffer before RedemptionDate.
825 // Do not pin an unrounded wall-clock instant: Env::close rounds to the close-time
826 // resolution. Read env.now() (the same clock LoanSet::preclaim uses) and assert the
827 // buffer relationship before each LoanSet.
828 withEnv([&](Env& env, PrettyAsset const& asset) {
829 auto const broker = createVaultAndBroker(
830 env,
831 asset,
832 lender,
834 .vaultKind = VaultKind::ClosedEnded,
835 .subscriptionOffset = 300u,
836 .redemptionOffset = kMinInvestmentPeriod,
837 .skipPhaseAdvance = true});
838 BEAST_EXPECT(broker.subscriptionDate.has_value());
839 BEAST_EXPECT(broker.redemptionDate.has_value());
840
841 auto const red = *broker.redemptionDate;
842 auto const startDate = [&]() { return env.now().time_since_epoch().count(); };
843 auto const minLoan = [&](TER expected) {
844 env(set(lender, broker.brokerID, broker.asset(100).value()),
845 kCounterparty(borrower),
846 Sig(sfCounterpartySignature, borrower),
847 Fee(env.current()->fees().base * 5),
848 kPaymentTotal(1u),
849 kPaymentInterval(LoanSet::kMinPaymentInterval),
850 Ter(expected));
851 env.close();
852 };
853
854 // First Investment ledger: the minimum schedule still clears the buffer.
855 env.close(Tp{D{*broker.subscriptionDate + 1}});
856 BEAST_EXPECT(startDate() > *broker.subscriptionDate);
857 BEAST_EXPECT(startDate() + LoanSet::kMinPaymentInterval + kLoanRedemptionBuffer <= red);
858 minLoan(tesSUCCESS);
859
860 // Still Investment, but the minimum schedule no longer clears the buffer.
861 while (startDate() + LoanSet::kMinPaymentInterval + kLoanRedemptionBuffer <= red)
862 env.close();
863 BEAST_EXPECT(startDate() < red);
864 minLoan(tecNO_PERMISSION);
865 });
866 }
867
868 // LoanSet used to call canAddHolding unconditionally, so an existing
869 // borrower line still failed with terNO_RIPPLE after the issuer cleared
870 // DefaultRipple. After fixCleanup3_4_0, skip that gate when the holding
871 // already exists.
872 void
874 {
875 using namespace jtx;
876 using namespace loan;
877
878 auto run = [this](FeatureBitset features, TER expected) {
879 testcase(
881 "LoanSet existing borrower line after issuer "
882 "clears asfDefaultRipple (") +
883 (features[fixCleanup3_4_0] ? "post" : "pre") + "-fixCleanup3_4_0)");
884
885 Env env(*this, features);
886 Account const issuer{"issuer"};
887 Account const lender{"lender"};
888 Account const borrower{"borrower"};
889
890 env.fund(XRP(10'000), issuer, lender, borrower);
891 env.close();
892 env(fset(issuer, asfDefaultRipple));
893 env.close();
894
895 PrettyAsset const usd{issuer["USD"]};
896 env(trust(lender, usd(10'000'000)));
897 env(trust(borrower, usd(10'000'000)));
898 env.close();
899 env(pay(issuer, lender, usd(2'000'000)));
900 env(pay(issuer, borrower, usd(1'000)));
901 env.close();
902 BEAST_EXPECT(env.le(keylet::trustLine(borrower.id(), usd.raw().get<Issue>())));
903
904 auto const broker = createVaultAndBroker(env, usd, lender);
905
906 env(fclear(issuer, asfDefaultRipple));
907 env.close();
908
909 Number const destBefore = env.balance(borrower, usd.raw()).number();
910 env(set(borrower, broker.brokerID, usd(100).value()),
911 Sig(sfCounterpartySignature, lender),
912 Fee(env.current()->fees().base * 2),
913 Ter(expected));
914 env.close();
915
916 Number const destAfter = env.balance(borrower, usd.raw()).number();
917 if (isTesSuccess(expected))
918 {
919 BEAST_EXPECT(destAfter == destBefore + Number{100});
920 }
921 else
922 {
923 BEAST_EXPECT(destAfter == destBefore);
924 }
925 };
926
927 run(all_ - fixCleanup3_4_0, terNO_RIPPLE);
929 }
930
931public:
932 void
933 run() override
934 {
935 for (auto const& features : jtx::amendmentCombinations(
936 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
937 testLoanSet(features);
938
943 }
944};
945
947
948} // namespace xrpl::test
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
constexpr TIss const & get() const
A currency issued by an account.
Definition Issue.h:18
static constexpr std::uint32_t kMinPaymentInterval
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
std::chrono::duration< rep, period > duration
Definition chrono.h:47
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
void run() override
Runs the suite.
void testLoanSetOriginationFeeTwoMptCreates(FeatureBitset features)
void testLoanSetExistingLineAfterIssuerClearsDefaultRipple()
void testLoanSet(FeatureBitset features)
Shared base for the Loan*_test family under src/test/app/lending/.
FeatureBitset const all_
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const &params=BrokerParameters::defaults())
std::string const iouCurrency_
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:354
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
NetClock::time_point now()
Returns the current network time.
Definition Env.h:326
Set the fee on a JTx.
Definition fee.h:20
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 a multisignature on a JTx.
Definition multisign.h:53
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 emplace_back(T... args)
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:591
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
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:275
static NoneT const kNone
Definition tags.h:9
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
std::vector< FeatureBitset > amendmentCombinations(std::initializer_list< UInt256 > features, FeatureBitset seed)
Returns all 2^N permutations of a seed FeatureBitset with each subset of the given features excluded.
Definition Env.h:123
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value noop(Account const &account)
The null transaction.
Definition noop.h:14
json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
Definition flags.h:110
FeatureBitset testableAmendments()
Definition Env.h:92
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value signers(Account const &account, std::uint32_t quorum, std::vector< Signer > const &v)
Definition multisign.cpp:31
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)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terNO_RIPPLE
Definition TER.h:225
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
int run(int argc, char **argv)
Definition Main.cpp:354
constexpr XRPAmount kDropsPerXrp
Number of drops per 1 XRP.
Definition XRPAmount.h:254
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
LedgerSpecificFlags
constexpr Number abs(Number x) noexcept
Definition Number.h:876
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
TERSubset< CanCvtToTER > TER
Definition TER.h:654
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, std::uint8_t depth=0)
Check if the account lacks required authorization for MPT.
@ tecTOO_SOON
Definition TER.h:326
@ tecEXPIRED
Definition TER.h:322
@ tecLIMIT_EXCEEDED
Definition TER.h:369
@ tecNO_PERMISSION
Definition TER.h:313
constexpr std::uint32_t kLoanRedemptionBuffer
Minimum gap between a closed-ended loan's final scheduled payment and the vault's RedemptionDate.
Definition Protocol.h:357
constexpr std::uint32_t kMinInvestmentPeriod
Bounds on the length of a closed-ended vault's Investment phase (RedemptionDate - SubscriptionDate).
Definition Protocol.h:369
@ tesSUCCESS
Definition TER.h:250
T reserve(T... args)
T size(T... args)
T time_since_epoch(T... args)