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/TER.h>
26#include <xrpl/protocol/TxFlags.h>
27#include <xrpl/protocol/Units.h>
28#include <xrpl/protocol/XRPAmount.h>
29
30#include <array>
31#include <cstdint>
32#include <functional>
33#include <utility>
34#include <vector>
35
36namespace xrpl::test {
37
39{
40private:
41 void
43 {
44 using namespace jtx;
45
46 Account const issuer{"issuer"};
47 Account const lender{"lender"};
48 Account const borrower{"borrower"};
49
50 struct CaseArgs
51 {
52 bool requireAuth = false;
53 bool authorizeBorrower = false;
54 int initialXRP = 1'000'000;
55 };
56
57 auto const testCase = [&, this](
58 std::function<void(Env&, BrokerInfo const&, MPTTester&)> mptTest,
59 std::function<void(Env&, BrokerInfo const&)> iouTest,
60 CaseArgs args = {}) {
61 Env env(*this, features);
62 env.fund(XRP(args.initialXRP), issuer, lender, borrower);
63 env.close();
64 if (args.requireAuth)
65 {
66 env(fset(issuer, asfRequireAuth));
67 env.close();
68 }
69
70 // We need two different asset types, MPT and IOU. Prepare MPT
71 // first
72 MPTTester mptt{env, issuer, kMptInitNoFund};
73
74 auto const kNone = LedgerSpecificFlags(0);
75 mptt.create(
76 {.flags = tfMPTCanTransfer | tfMPTCanLock |
77 (args.requireAuth ? tfMPTRequireAuth : kNone)});
78 env.close();
79 PrettyAsset const mptAsset = mptt.issuanceID();
80 mptt.authorize({.account = lender});
81 mptt.authorize({.account = borrower});
82 env.close();
83 if (args.requireAuth)
84 {
85 mptt.authorize({.account = issuer, .holder = lender});
86 if (args.authorizeBorrower)
87 mptt.authorize({.account = issuer, .holder = borrower});
88 env.close();
89 }
90
91 env(pay(issuer, lender, mptAsset(10'000'000)));
92 env.close();
93
94 // Prepare IOU
95 PrettyAsset const iouAsset = issuer[iouCurrency_];
96 env(trust(lender, iouAsset(10'000'000)));
97 env(trust(borrower, iouAsset(10'000'000)));
98 env.close();
99 if (args.requireAuth)
100 {
101 env(trust(issuer, iouAsset(0), lender, tfSetfAuth));
102 env(pay(issuer, lender, iouAsset(10'000'000)));
103 if (args.authorizeBorrower)
104 {
105 env(trust(issuer, iouAsset(0), borrower, tfSetfAuth));
106 env(pay(issuer, borrower, iouAsset(10'000)));
107 }
108 }
109 else
110 {
111 env(pay(issuer, lender, iouAsset(10'000'000)));
112 env(pay(issuer, borrower, iouAsset(10'000)));
113 }
114 env.close();
115
116 // Create vaults and loan brokers
117 std::array const assets{mptAsset, iouAsset};
119 brokers.reserve(assets.size());
120 for (auto const& asset : assets)
121 {
122 brokers.emplace_back(createVaultAndBroker(env, asset, lender));
123 }
124
125 if (mptTest)
126 mptTest(env, brokers[0], mptt);
127 if (iouTest)
128 iouTest(env, brokers[1]);
129 };
130
131 testCase(
132 [&, this](Env& env, BrokerInfo const& broker, auto&) {
133 using namespace loan;
134 Number const principalRequest = broker.asset(1'000).value();
135
136 testcase("MPT issuer is borrower, issuer submits");
137 env(set(issuer, broker.brokerID, principalRequest),
138 kCounterparty(lender),
139 Sig(sfCounterpartySignature, lender),
140 Fee(env.current()->fees().base * 5));
141
142 testcase("MPT issuer is borrower, lender submits");
143 env(set(lender, broker.brokerID, principalRequest),
144 kCounterparty(issuer),
145 Sig(sfCounterpartySignature, issuer),
146 Fee(env.current()->fees().base * 5));
147 },
148 [&, this](Env& env, BrokerInfo const& broker) {
149 using namespace loan;
150 Number const principalRequest = broker.asset(1'000).value();
151
152 testcase("IOU issuer is borrower, issuer submits");
153 env(set(issuer, broker.brokerID, principalRequest),
154 kCounterparty(lender),
155 Sig(sfCounterpartySignature, lender),
156 Fee(env.current()->fees().base * 5));
157
158 testcase("IOU issuer is borrower, lender submits");
159 env(set(lender, broker.brokerID, principalRequest),
160 kCounterparty(issuer),
161 Sig(sfCounterpartySignature, issuer),
162 Fee(env.current()->fees().base * 5));
163 },
164 CaseArgs{.requireAuth = true});
165
166 testCase(
167 [&, this](Env& env, BrokerInfo const& broker, auto&) {
168 using namespace loan;
169 Number const principalRequest = broker.asset(1'000).value();
170
171 testcase("MPT unauthorized borrower, borrower submits");
172 env(set(borrower, broker.brokerID, principalRequest),
173 kCounterparty(lender),
174 Sig(sfCounterpartySignature, lender),
175 Fee(env.current()->fees().base * 5),
176 Ter{tecNO_AUTH});
177
178 testcase("MPT unauthorized borrower, lender submits");
179 env(set(lender, broker.brokerID, principalRequest),
180 kCounterparty(borrower),
181 Sig(sfCounterpartySignature, borrower),
182 Fee(env.current()->fees().base * 5),
183 Ter{tecNO_AUTH});
184 },
185 [&, this](Env& env, BrokerInfo const& broker) {
186 using namespace loan;
187 Number const principalRequest = broker.asset(1'000).value();
188
189 testcase("IOU unauthorized borrower, borrower submits");
190 env(set(borrower, broker.brokerID, principalRequest),
191 kCounterparty(lender),
192 Sig(sfCounterpartySignature, lender),
193 Fee(env.current()->fees().base * 5),
194 Ter{tecNO_AUTH});
195
196 testcase("IOU unauthorized borrower, lender submits");
197 env(set(lender, broker.brokerID, principalRequest),
198 kCounterparty(borrower),
199 Sig(sfCounterpartySignature, borrower),
200 Fee(env.current()->fees().base * 5),
201 Ter{tecNO_AUTH});
202 },
203 CaseArgs{.requireAuth = true});
204
205 auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
206 Env const env{*this, testableAmendments()};
207 return {
208 env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(),
209 env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
210 }();
211
212 testCase(
213 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
214 using namespace loan;
215 Number const principalRequest = broker.asset(1'000).value();
216
217 testcase(
218 "MPT authorized borrower, borrower submits, borrower has "
219 "no reserve");
220 mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize});
221 env.close();
222
223 auto const mptoken = keylet::mptoken(mptt.issuanceID(), borrower);
224 auto const sleMPT1 = env.le(mptoken);
225 BEAST_EXPECT(sleMPT1 == nullptr);
226
227 // Burn some XRP
228 env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2))));
229 env.close();
230
231 // Cannot create loan, not enough reserve to create MPToken
232 env(set(borrower, broker.brokerID, principalRequest),
233 kCounterparty(lender),
234 Sig(sfCounterpartySignature, lender),
235 Fee(env.current()->fees().base * 5),
236 Ter{tecINSUFFICIENT_RESERVE});
237 env.close();
238
239 // Can create loan now, will implicitly create MPToken
240 env(pay(issuer, borrower, XRP(incReserve)));
241 env.close();
242 env(set(borrower, broker.brokerID, principalRequest),
243 kCounterparty(lender),
244 Sig(sfCounterpartySignature, lender),
245 Fee(env.current()->fees().base * 5));
246 env.close();
247
248 auto const sleMPT2 = env.le(mptoken);
249 BEAST_EXPECT(sleMPT2 != nullptr);
250 },
251 {},
252 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
253
254 testCase(
255 {},
256 [&, this](Env& env, BrokerInfo const& broker) {
257 using namespace loan;
258 Number const principalRequest = broker.asset(1'000).value();
259
260 testcase(
261 "IOU authorized borrower, borrower submits, borrower has "
262 "no reserve");
263 // Remove trust line from borrower to issuer
264 env.trust(broker.asset(0), borrower);
265 env.close();
266
267 env(pay(borrower, issuer, broker.asset(10'000)));
268 env.close();
269 auto const trustline = keylet::trustLine(borrower, broker.asset.raw().get<Issue>());
270 auto const sleLine1 = env.le(trustline);
271 BEAST_EXPECT(sleLine1 == nullptr);
272
273 // Burn some XRP
274 env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2))));
275 env.close();
276
277 // Cannot create loan, not enough reserve to create trust line
278 env(set(borrower, broker.brokerID, principalRequest),
279 kCounterparty(lender),
280 Sig(sfCounterpartySignature, lender),
281 Fee(env.current()->fees().base * 5),
282 Ter{tecNO_LINE_INSUF_RESERVE});
283 env.close();
284
285 // Can create loan now, will implicitly create trust line
286 env(pay(issuer, borrower, XRP(incReserve)));
287 env.close();
288 env(set(borrower, broker.brokerID, principalRequest),
289 kCounterparty(lender),
290 Sig(sfCounterpartySignature, lender),
291 Fee(env.current()->fees().base * 5));
292 env.close();
293
294 auto const sleLine2 = env.le(trustline);
295 BEAST_EXPECT(sleLine2 != nullptr);
296 },
297 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
298
299 testCase(
300 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
301 using namespace loan;
302 Number const principalRequest = broker.asset(1'000).value();
303
304 testcase(
305 "MPT authorized borrower, borrower submits, lender has "
306 "no reserve");
307 auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender);
308 auto const sleMPT1 = env.le(mptoken);
309 BEAST_EXPECT(sleMPT1 != nullptr);
310
311 env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount))));
312 env.close();
313
314 mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
315 env.close();
316
317 auto const sleMPT2 = env.le(mptoken);
318 BEAST_EXPECT(sleMPT2 == nullptr);
319
320 // Burn some XRP
321 env(noop(lender), Fee(XRP(incReserve)));
322 env.close();
323
324 // Cannot create loan, not enough reserve to create MPToken
325 env(set(borrower, broker.brokerID, principalRequest),
326 kLoanOriginationFee(broker.asset(1).value()),
327 kCounterparty(lender),
328 Sig(sfCounterpartySignature, lender),
329 Fee(env.current()->fees().base * 5),
330 Ter{tecINSUFFICIENT_RESERVE});
331 env.close();
332
333 // Can create loan now, will implicitly create MPToken
334 env(pay(issuer, lender, XRP(incReserve)));
335 env.close();
336 env(set(borrower, broker.brokerID, principalRequest),
337 kLoanOriginationFee(broker.asset(1).value()),
338 kCounterparty(lender),
339 Sig(sfCounterpartySignature, lender),
340 Fee(env.current()->fees().base * 5));
341 env.close();
342
343 auto const sleMPT3 = env.le(mptoken);
344 BEAST_EXPECT(sleMPT3 != nullptr);
345 },
346 {},
347 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
348
349 testCase(
350 {},
351 [&, this](Env& env, BrokerInfo const& broker) {
352 using namespace loan;
353 Number const principalRequest = broker.asset(1'000).value();
354
355 testcase(
356 "IOU authorized borrower, borrower submits, lender has no "
357 "reserve");
358 // Remove trust line from lender to issuer
359 env.trust(broker.asset(0), lender);
360 env.close();
361
362 auto const trustline = keylet::trustLine(lender, broker.asset.raw().get<Issue>());
363 auto const sleLine1 = env.le(trustline);
364 BEAST_EXPECT(sleLine1 != nullptr);
365
366 env(pay(lender, issuer, broker.asset(abs(sleLine1->at(sfBalance).value()))));
367 env.close();
368 auto const sleLine2 = env.le(trustline);
369 BEAST_EXPECT(sleLine2 == nullptr);
370
371 // Burn some XRP
372 env(noop(lender), Fee(XRP(incReserve)));
373 env.close();
374
375 // Cannot create loan, not enough reserve to create trust line
376 env(set(borrower, broker.brokerID, principalRequest),
377 kLoanOriginationFee(broker.asset(1).value()),
378 kCounterparty(lender),
379 Sig(sfCounterpartySignature, lender),
380 Fee(env.current()->fees().base * 5),
381 Ter{tecNO_LINE_INSUF_RESERVE});
382 env.close();
383
384 // Can create loan now, will implicitly create trust line
385 env(pay(issuer, lender, XRP(incReserve)));
386 env.close();
387 env(set(borrower, broker.brokerID, principalRequest),
388 kLoanOriginationFee(broker.asset(1).value()),
389 kCounterparty(lender),
390 Sig(sfCounterpartySignature, lender),
391 Fee(env.current()->fees().base * 5));
392 env.close();
393
394 auto const sleLine3 = env.le(trustline);
395 BEAST_EXPECT(sleLine3 != nullptr);
396 },
397 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
398
399 testCase(
400 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
401 using namespace loan;
402 Number const principalRequest = broker.asset(1'000).value();
403
404 testcase("MPT authorized borrower, unauthorized lender");
405 auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender);
406 auto const sleMPT1 = env.le(mptoken);
407 BEAST_EXPECT(sleMPT1 != nullptr);
408
409 env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount))));
410 env.close();
411
412 mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
413 env.close();
414
415 auto const sleMPT2 = env.le(mptoken);
416 BEAST_EXPECT(sleMPT2 == nullptr);
417
418 // Cannot create loan, lender not authorized to receive fee
419 env(set(borrower, broker.brokerID, principalRequest),
420 kLoanOriginationFee(broker.asset(1).value()),
421 kCounterparty(lender),
422 Sig(sfCounterpartySignature, lender),
423 Fee(env.current()->fees().base * 5),
424 Ter{tecNO_AUTH});
425 env.close();
426
427 // Cannot create loan, even without an origination fee
428 env(set(borrower, broker.brokerID, principalRequest),
429 kCounterparty(lender),
430 Sig(sfCounterpartySignature, lender),
431 Fee(env.current()->fees().base * 5),
432 Ter{tecNO_AUTH});
433 env.close();
434
435 // No MPToken for lender - no authorization and no payment
436 auto const sleMPT3 = env.le(mptoken);
437 BEAST_EXPECT(sleMPT3 == nullptr);
438 },
439 {},
440 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
441
442 testCase(
443 [&, this](Env& env, BrokerInfo const& broker, auto&) {
444 using namespace loan;
445 Number const principalRequest = broker.asset(1'000).value();
446
447 testcase("MPT authorized borrower, borrower submits");
448 env(set(borrower, broker.brokerID, principalRequest),
449 kCounterparty(lender),
450 Sig(sfCounterpartySignature, lender),
451 Fee(env.current()->fees().base * 5));
452 },
453 [&, this](Env& env, BrokerInfo const& broker) {
454 using namespace loan;
455 Number const principalRequest = broker.asset(1'000).value();
456
457 testcase("IOU authorized borrower, borrower submits");
458 env(set(borrower, broker.brokerID, principalRequest),
459 kCounterparty(lender),
460 Sig(sfCounterpartySignature, lender),
461 Fee(env.current()->fees().base * 5));
462 },
463 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
464
465 testCase(
466 [&, this](Env& env, BrokerInfo const& broker, auto&) {
467 using namespace loan;
468 Number const principalRequest = broker.asset(1'000).value();
469
470 testcase("MPT authorized borrower, lender submits");
471 env(set(lender, broker.brokerID, principalRequest),
472 kCounterparty(borrower),
473 Sig(sfCounterpartySignature, borrower),
474 Fee(env.current()->fees().base * 5));
475 },
476 [&, this](Env& env, BrokerInfo const& broker) {
477 using namespace loan;
478 Number const principalRequest = broker.asset(1'000).value();
479
480 testcase("IOU authorized borrower, lender submits");
481 env(set(lender, broker.brokerID, principalRequest),
482 kCounterparty(borrower),
483 Sig(sfCounterpartySignature, borrower),
484 Fee(env.current()->fees().base * 5));
485 },
486 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
487
488 jtx::Account const alice{"alice"};
489 jtx::Account const bella{"bella"};
490 auto const msigSetup = [&](Env& env, Account const& account) {
491 json::Value const tx1 = signers(account, 2, {{alice, 1}, {bella, 1}});
492 env(tx1);
493 env.close();
494 };
495
496 testCase(
497 [&, this](Env& env, BrokerInfo const& broker, auto&) {
498 using namespace loan;
499 msigSetup(env, lender);
500 Number const principalRequest = broker.asset(1'000).value();
501
502 testcase(
503 "MPT authorized borrower, borrower submits, lender "
504 "multisign");
505 env(set(borrower, broker.brokerID, principalRequest),
506 kCounterparty(lender),
507 Msig(sfCounterpartySignature, alice, bella),
508 Fee(env.current()->fees().base * 5));
509 },
510 [&, this](Env& env, BrokerInfo const& broker) {
511 using namespace loan;
512 msigSetup(env, lender);
513 Number const principalRequest = broker.asset(1'000).value();
514
515 testcase(
516 "IOU authorized borrower, borrower submits, lender "
517 "multisign");
518 env(set(borrower, broker.brokerID, principalRequest),
519 kCounterparty(lender),
520 Msig(sfCounterpartySignature, alice, bella),
521 Fee(env.current()->fees().base * 5));
522 },
523 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
524
525 testCase(
526 [&, this](Env& env, BrokerInfo const& broker, auto&) {
527 using namespace loan;
528 msigSetup(env, borrower);
529 Number const principalRequest = broker.asset(1'000).value();
530
531 testcase(
532 "MPT authorized borrower, lender submits, borrower "
533 "multisign");
534 env(set(lender, broker.brokerID, principalRequest),
535 kCounterparty(borrower),
536 Msig(sfCounterpartySignature, alice, bella),
537 Fee(env.current()->fees().base * 5));
538 },
539 [&, this](Env& env, BrokerInfo const& broker) {
540 using namespace loan;
541 msigSetup(env, borrower);
542 Number const principalRequest = broker.asset(1'000).value();
543
544 testcase(
545 "IOU authorized borrower, lender submits, borrower "
546 "multisign");
547 env(set(lender, broker.brokerID, principalRequest),
548 kCounterparty(borrower),
549 Msig(sfCounterpartySignature, alice, bella),
550 Fee(env.current()->fees().base * 5));
551 },
552 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
553
554 testCase(
555 [&, this](Env& env, BrokerInfo const& broker, auto&) {
556 using namespace loan;
557 Number const principalRequest = broker.asset(1'000).value();
558 Vault const vault{env};
559 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
560 tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit;
561 env(tx);
562 env.close();
563
564 testcase("Vault at maximum value");
565 env(set(issuer, broker.brokerID, principalRequest),
566 kCounterparty(lender),
567 kInterestRate(TenthBips32(10'000)),
568 Sig(sfCounterpartySignature, lender),
569 Fee(env.current()->fees().base * 5),
571 },
572 nullptr);
573
574 testCase(
575 [&, this](Env& env, BrokerInfo const& broker, auto&) {
576 using namespace loan;
577 Number const principalRequest = broker.asset(1'000).value();
578 Vault const vault{env};
579 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
580 tx[sfAssetsMaximum] =
581 BrokerParameters::defaults().vaultDeposit + broker.asset(1).number();
582 env(tx);
583 env.close();
584
585 testcase("Vault maximum value exceeded");
586 env(set(issuer, broker.brokerID, principalRequest),
587 kCounterparty(lender),
588 kInterestRate(TenthBips32(100'000)),
589 Sig(sfCounterpartySignature, lender),
590 Fee(env.current()->fees().base * 5),
591 kPaymentTotal(2),
592 kPaymentInterval(3600 * 24),
594 },
595 nullptr);
596 }
597
598 // LoanSet in a closed-ended vault — phase gating and maturity bound.
599 void
601 {
602 testcase("LoanSet closed-ended: phase and maturity bound");
603 using namespace jtx;
604 using namespace loan;
605
606 Account const issuer{"issuer"};
607 Account const lender{"lender"};
608 Account const borrower{"borrower"};
609
610 // Common loan schedule used by the phase-rejection cases below.
611 constexpr std::uint32_t kInterval = 3600u * 24u; // 1 day
612 constexpr std::uint32_t kTotal = 2u;
613
614 // featureLendingProtocolV1_1 is excluded from `all_` by convention (see the comment on
615 // `all_`), so callers must opt in. Closed-ended vaults are gated on this amendment; without
616 // it VaultCreate returns temDISABLED and every follow-on txn sees tecNO_ENTRY.
617 auto const withEnv = [&, this](auto&& body) {
618 Env env(*this, testableAmendments() | featureLendingProtocolV1_1);
619 env.fund(XRP(1'000'000'000), issuer, lender, borrower);
620 env.close();
621 PrettyAsset const asset{xrpIssue(), 1'000'000};
622 body(env, asset);
623 };
624
625 auto const setLoan = [&](Env& env, BrokerInfo const& broker, TER expected) {
626 env(set(lender, broker.brokerID, broker.asset(100).value()),
627 kCounterparty(borrower),
628 Sig(sfCounterpartySignature, borrower),
629 Fee(env.current()->fees().base * 5),
630 kPaymentTotal(kTotal),
631 kPaymentInterval(kInterval),
632 Ter(expected));
633 env.close();
634 };
635
636 // 1. Rejected during Subscription: the broker is created in Subscription (skipPhaseAdvance
637 // = true), then LoanSet is attempted before advancing past SubscriptionDate.
638 withEnv([&](Env& env, PrettyAsset const& asset) {
639 auto const broker = createVaultAndBroker(
640 env,
641 asset,
642 lender,
643 BrokerParameters{.vaultKind = VaultKind::ClosedEnded, .skipPhaseAdvance = true});
644 setLoan(env, broker, tecTOO_SOON);
645 });
646
647 // 2. Rejected during Redemption: broker is set up normally (which lands the vault in
648 // Investment), then advance the clock past RedemptionDate before attempting LoanSet.
649 withEnv([&](Env& env, PrettyAsset const& asset) {
650 auto const broker = createVaultAndBroker(
651 env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded});
652 BEAST_EXPECT(broker.redemptionDate.has_value());
653 using d = NetClock::duration;
654 using tp = NetClock::time_point;
655 env.close(tp{d{*broker.redemptionDate + 1}});
656 setLoan(env, broker, tecEXPIRED);
657 });
658
659 // 3. Accepted during Investment when the schedule comfortably fits before RedemptionDate.
660 withEnv([&](Env& env, PrettyAsset const& asset) {
661 auto const broker = createVaultAndBroker(
662 env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded});
663 setLoan(env, broker, tesSUCCESS);
664 });
665
666 // 4. Rejected during Investment when the loan's final payment would land on or after
667 // RedemptionDate. Use a tight redemptionOffset and a schedule whose final payment is well
668 // past that boundary.
669 withEnv([&](Env& env, PrettyAsset const& asset) {
670 constexpr std::uint32_t kRedemptionOffset = 3u * 24u * 3600u;
671 auto const broker = createVaultAndBroker(
672 env,
673 asset,
674 lender,
676 .vaultKind = VaultKind::ClosedEnded, .redemptionOffset = kRedemptionOffset});
677 env(set(lender, broker.brokerID, broker.asset(100).value()),
678 kCounterparty(borrower),
679 Sig(sfCounterpartySignature, borrower),
680 Fee(env.current()->fees().base * 5),
681 kPaymentTotal(10u),
682 kPaymentInterval(kInterval),
684 env.close();
685 });
686
687 // 5. Boundary: schedule whose finalPayment lands exactly (RedemptionDate - 1) is accepted,
688 // and one second later (== RedemptionDate) is rejected. Uses payTotal = 1 so the arithmetic
689 // is simple: finalPayment = startDate + interval.
690 withEnv([&](Env& env, PrettyAsset const& asset) {
691 auto const broker = createVaultAndBroker(
692 env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded});
693 BEAST_EXPECT(broker.redemptionDate.has_value());
694
695 auto const startDate = env.now().time_since_epoch().count();
696 auto const acceptInterval = *broker.redemptionDate - 1 - startDate;
697 env(set(lender, broker.brokerID, broker.asset(100).value()),
698 kCounterparty(borrower),
699 Sig(sfCounterpartySignature, borrower),
700 Fee(env.current()->fees().base * 5),
701 kPaymentTotal(1u),
702 kPaymentInterval(acceptInterval),
703 Ter(tesSUCCESS));
704 env.close();
705
706 auto const rejectInterval =
707 *broker.redemptionDate - env.now().time_since_epoch().count();
708 env(set(lender, broker.brokerID, broker.asset(100).value()),
709 kCounterparty(borrower),
710 Sig(sfCounterpartySignature, borrower),
711 Fee(env.current()->fees().base * 5),
712 kPaymentTotal(1u),
713 kPaymentInterval(rejectInterval),
715 env.close();
716 });
717 }
718
719public:
720 void
721 run() override
722 {
723 for (auto const& features : jtx::amendmentCombinations(
724 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
725 testLoanSet(features);
726
728 }
729};
730
732
733} // 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
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
void run() override
Runs the suite.
void testLoanSet(FeatureBitset features)
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
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: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 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 mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:543
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:253
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
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:172
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
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
TERSubset< CanCvtToTER > TER
Definition TER.h:647
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:321
@ tecEXPIRED
Definition TER.h:317
@ tecLIMIT_EXCEEDED
Definition TER.h:364
@ tecNO_PERMISSION
Definition TER.h:308
@ tesSUCCESS
Definition TER.h:245
T reserve(T... args)
T size(T... args)
T time_since_epoch(T... args)