xrpld
Loading...
Searching...
No Matches
LendingBatch_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/JTx.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/batch.h>
8#include <test/jtx/fee.h>
9#include <test/jtx/flags.h>
10#include <test/jtx/offer.h>
11#include <test/jtx/owners.h>
12#include <test/jtx/pay.h>
13#include <test/jtx/seq.h>
14#include <test/jtx/sig.h>
15#include <test/jtx/tags.h>
16#include <test/jtx/ter.h>
17#include <test/jtx/trust.h>
18#include <test/jtx/vault.h>
19
20#include <xrpl/basics/Number.h>
21#include <xrpl/basics/base_uint.h>
22#include <xrpl/basics/chrono.h>
23#include <xrpl/basics/strHex.h>
24#include <xrpl/beast/unit_test/suite.h>
25#include <xrpl/json/json_value.h>
26#include <xrpl/protocol/Feature.h>
27#include <xrpl/protocol/Indexes.h>
28#include <xrpl/protocol/Issue.h>
29#include <xrpl/protocol/Keylet.h>
30#include <xrpl/protocol/LedgerFormats.h>
31#include <xrpl/protocol/Protocol.h>
32#include <xrpl/protocol/SField.h>
33#include <xrpl/protocol/STAmount.h>
34#include <xrpl/protocol/SeqProxy.h>
35#include <xrpl/protocol/TER.h>
36#include <xrpl/protocol/TxFlags.h>
37#include <xrpl/protocol/Units.h>
38
39#include <algorithm>
40#include <chrono>
41#include <cstdint>
42
43namespace xrpl::test {
44
45// Vault and Lending transactions used as Batch inners under LendingProtocolV1_2.
47{
48private:
49 // Asserts a loan has been paid off in full: no principal, value, or
50 // remaining payments left on the loan; no debt left on the broker; and the
51 // vault's available assets back to `depositValue`.
52 void
54 jtx::Env const& env,
55 Keylet const& brokerKeylet,
56 Keylet const& vaultKeylet,
57 Keylet const& loanKeylet,
58 STAmount const& depositValue)
59 {
60 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
61 {
62 BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == 0);
63 BEAST_EXPECT(loanSle->at(sfTotalValueOutstanding) == 0);
64 BEAST_EXPECT(loanSle->at(sfPaymentRemaining) == 0);
65 }
66 if (auto const brokerSle = env.le(brokerKeylet); BEAST_EXPECT(brokerSle))
67 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
68 if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
69 BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == depositValue);
70 }
71
72 // LoanSet as a batch inner: the counterparty signs the outer batch, so
73 // this inner carries no signature of its own. Pass paymentTotal >= 2
74 // wherever the same batch also submits an early tfLoanFullPayment:
75 // computeFullPayment refuses it with tecKILLED when only the final
76 // scheduled payment is left.
77 static json::Value
79 jtx::Env& env,
80 jtx::Account const& account,
81 uint256 const& brokerID,
82 jtx::Account const& counterparty,
83 STAmount const& principal,
84 std::uint32_t paymentTotal)
85 {
86 using namespace jtx;
87 using namespace jtx::loan;
88
89 return env.json(
90 set(account, brokerID, principal.value()),
91 kCounterparty(counterparty.id()),
92 kPaymentTotal(paymentTotal),
93 Sig(kNone),
94 Fee(kNone),
95 Seq(kNone));
96 }
97
98 // Runs VaultCreate -> Deposit -> Withdraw -> Delete in one tfAllOrNothing
99 // batch and checks the vault is gone, the owner paid only the batch fee,
100 // and both owner count and sequence are back where they belong.
101 void
103 {
104 using namespace jtx;
105
106 Vault const vault{env};
107
108 auto const seq = env.seq(owner);
109 auto const balanceBefore = env.balance(owner);
110 auto const ownerCountBefore = env.ownerCount(owner);
111
112 // The outer Batch consumes owner's current sequence, so the first
113 // inner (VaultCreate) lands on seq + 1.
114 auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(seq + 1));
115 auto const depositAmount = XRP(1'000);
116
117 auto const batchFee = batch::calcBatchFee(env, 0, 4);
118 env(batch::outer(owner, seq, batchFee, tfAllOrNothing),
119 batch::Inner(createTx, seq + 1),
121 vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = depositAmount}),
122 seq + 2),
124 vault.withdraw(
125 {.depositor = owner, .id = vaultKeylet.key, .amount = depositAmount}),
126 seq + 3),
127 batch::Inner(vault.del({.owner = owner, .id = vaultKeylet.key}), seq + 4),
128 Ter(tesSUCCESS));
129 env.close();
130
131 BEAST_EXPECT(!env.le(vaultKeylet));
132 BEAST_EXPECT(env.balance(owner) == balanceBefore - batchFee);
133 BEAST_EXPECT(env.ownerCount(owner) == ownerCountBefore);
134 BEAST_EXPECT(env.seq(owner) == seq + 5);
135 }
136
137 void
139 {
140 // A single tfAllOrNothing batch runs an open-ended XRP vault through
141 // its whole lifecycle: create, deposit, withdraw everything back out,
142 // and delete.
143 testcase("vault lifecycle in a batch");
144
145 using namespace jtx;
146
147 Env env(*this, features);
148
149 Account const owner{"owner"};
150 env.fund(XRP(100'000), owner);
151 env.close();
152
153 auto const createTx = std::get<0>(Vault{env}.create({.owner = owner, .asset = xrpIssue()}));
154 runVaultLifecycleBatch(env, owner, createTx);
155 }
156
157 void
159 {
160 // Same lifecycle as testVaultLifecycle, but on a closed-ended vault
161 // submitted while it is still in the Subscription phase, where
162 // LendingProtocolV1_1 allows both deposit and withdraw.
163 testcase("vault lifecycle in a batch, closed-ended vault, subscription phase");
164
165 using namespace jtx;
166
167 Env env(*this, features);
168
169 Account const owner{"owner"};
170 env.fund(XRP(100'000), owner);
171 env.close();
172
173 auto const createTx = std::get<0>(Vault{env}.createClosedEnded(
174 {.owner = owner, .asset = xrpIssue(), .subscriptionOffset = std::chrono::seconds{60}}));
175 runVaultLifecycleBatch(env, owner, createTx);
176 }
177
178 void
180 {
181 // With LendingProtocolV1_1 off, an open-ended vault can host a broker,
182 // so vault creation, deposit, broker setup, loan origination, and an
183 // early full payoff all fit in one atomic batch. The borrower signs the
184 // outer as counterparty to the LoanSet inner and as the account behind
185 // the LoanPay inner.
186 testcase("loan lifecycle in a batch, open-ended vault");
187
188 using namespace jtx;
189
190 Env env(*this, features);
191
192 Account const lender{"lender"};
193 Account const borrower{"borrower"};
194 env.fund(XRP(1'000'000), lender, borrower);
195 env.close();
196
197 PrettyAsset const asset{xrpIssue(), 1'000'000};
198 Vault const vault{env};
199
200 auto const lenderSeq = env.seq(lender);
201 auto const borrowerSeq = env.seq(borrower);
202 auto const lenderBalanceBefore = env.balance(lender);
203 auto const borrowerBalanceBefore = env.balance(borrower);
204
205 auto const depositAmount = asset(50'000);
206 auto const principal = asset(1'000);
207
208 auto const vaultKeylet = keylet::vault(lender.id(), SeqProxy::rawSequence(lenderSeq + 1));
209 auto const brokerKeylet =
210 keylet::loanBroker(lender.id(), SeqProxy::rawSequence(lenderSeq + 3));
211 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
212
213 auto const createTx = std::get<0>(vault.create({.owner = lender, .asset = xrpIssue()}));
214
215 // CoverRateMinimum/CoverRateLiquidation are left unset on the
216 // LoanBrokerSet inner below, which defaults both to zero, so this
217 // loan needs no first-loss cover deposit from the broker.
218 auto const loanSetTx = loanSetInner(env, lender, brokerKeylet.key, borrower, principal, 2);
219 auto const loanPayTx =
220 loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
221
222 auto const batchFee = batch::calcBatchFee(env, 1, 5);
223 env(batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
224 batch::Inner(createTx, lenderSeq + 1),
226 vault.deposit(
227 {.depositor = lender, .id = vaultKeylet.key, .amount = depositAmount.value()}),
228 lenderSeq + 2),
229 batch::Inner(loan_broker::set(lender, vaultKeylet.key), lenderSeq + 3),
230 batch::Inner(loanSetTx, lenderSeq + 4),
231 batch::Inner(loanPayTx, borrowerSeq),
232 batch::Sig(borrower),
233 Ter(tesSUCCESS));
234 env.close();
235
236 assertLoanFullyRepaid(env, brokerKeylet, vaultKeylet, loanKeylet, depositAmount.value());
237
238 // The borrower borrowed the principal and repaid it in full at zero
239 // interest and fees, so their balance nets to zero; the batch fee is
240 // paid entirely by the lender, who also funded the vault deposit.
241 BEAST_EXPECT(env.balance(borrower) == borrowerBalanceBefore);
242 BEAST_EXPECT(env.balance(lender) == lenderBalanceBefore - batchFee - depositAmount.value());
243 }
244
245 void
247 {
248 // Under LendingProtocolV1_1 a broker can attach only to a closed-ended
249 // vault, and LoanSet is refused outside the Investment phase, so the
250 // vault/broker setup and the loan/payoff run as two batches split at
251 // the Subscription -> Investment boundary.
252 testcase("loan lifecycle in a batch, closed-ended vault");
253
254 using namespace jtx;
255
256 Env env(*this, features);
257
258 Account const lender{"lender"};
259 Account const borrower{"borrower"};
260 env.fund(XRP(1'000'000), lender, borrower);
261 env.close();
262
263 PrettyAsset const asset{xrpIssue(), 1'000'000};
264 Vault const vault{env};
265
266 auto const lenderSeq1 = env.seq(lender);
267 auto const depositAmount = asset(50'000);
268
269 auto const vaultKeylet = keylet::vault(lender.id(), SeqProxy::rawSequence(lenderSeq1 + 1));
270 auto const brokerKeylet =
271 keylet::loanBroker(lender.id(), SeqProxy::rawSequence(lenderSeq1 + 3));
272
273 auto const created = vault.createClosedEnded(
274 {.owner = lender, .asset = xrpIssue(), .subscriptionOffset = std::chrono::seconds{60}});
275 auto const createTx = std::get<0>(created);
276 auto const subscriptionDate = std::get<2>(created);
277
278 // LoanBrokerSet checks only the vault's kind, not its phase, so setting
279 // up the broker is allowed here in the same batch, while the vault is
280 // still in the Subscription phase.
281 auto const batch1Fee = batch::calcBatchFee(env, 0, 3);
282 env(batch::outer(lender, lenderSeq1, batch1Fee, tfAllOrNothing),
283 batch::Inner(createTx, lenderSeq1 + 1),
285 vault.deposit(
286 {.depositor = lender, .id = vaultKeylet.key, .amount = depositAmount.value()}),
287 lenderSeq1 + 2),
288 batch::Inner(loan_broker::set(lender, vaultKeylet.key), lenderSeq1 + 3),
289 Ter(tesSUCCESS));
290 env.close();
291
292 if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
293 BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == depositAmount.value());
294 BEAST_EXPECT(env.le(brokerKeylet));
295
296 vault.closePastSubscription(subscriptionDate);
297
298 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
299 auto const principal = asset(1'000);
300
301 auto const lenderSeq2 = env.seq(lender);
302 auto const borrowerSeq = env.seq(borrower);
303 auto const lenderBalanceBefore = env.balance(lender);
304 auto const borrowerBalanceBefore = env.balance(borrower);
305
306 auto const loanSetTx = loanSetInner(env, lender, brokerKeylet.key, borrower, principal, 2);
307 auto const loanPayTx =
308 loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
309
310 auto const batch2Fee = batch::calcBatchFee(env, 1, 2);
311 env(batch::outer(lender, lenderSeq2, batch2Fee, tfAllOrNothing),
312 batch::Inner(loanSetTx, lenderSeq2 + 1),
313 batch::Inner(loanPayTx, borrowerSeq),
314 batch::Sig(borrower),
315 Ter(tesSUCCESS));
316 env.close();
317
318 assertLoanFullyRepaid(env, brokerKeylet, vaultKeylet, loanKeylet, depositAmount.value());
319
320 BEAST_EXPECT(env.balance(borrower) == borrowerBalanceBefore);
321 // The deposit already left the lender's balance in batch 1; batch 2
322 // only costs the batch fee, since LoanSet/LoanPay net to zero XRP.
323 BEAST_EXPECT(env.balance(lender) == lenderBalanceBefore - batch2Fee);
324 }
325
326 // Handles shared by testArbitrage and testArbitrageRollback.
337
338 // Broker with zero cover rates and no debt cap, so LoanSet needs no cover
339 // deposit. The borrower holds a trust line but no IOU, so the profit is
340 // exact. mm1 rests an offer selling 400 XRP for 1000 IOU; mm2 is funded
341 // but places its offer only inside testArbitrage's batch.
344 {
345 using namespace jtx;
346
347 Account const issuer{"issuer"};
348 Account const lender{"lender"};
349 Account const borrower{"borrower"};
350 Account const mm1{"mm1"};
351 Account const mm2{"mm2"};
352 env.fund(XRP(1'000'000), issuer, lender, borrower, mm1, mm2);
353 env(fset(issuer, asfDefaultRipple));
354 env.close();
355
356 PrettyAsset const iou = issuer[iouCurrency_];
357 STAmount const iouLimit = iou(1'000'000'000);
358 env(trust(lender, iouLimit));
359 env(trust(borrower, iouLimit));
360 env(trust(mm1, iouLimit));
361 env(trust(mm2, iouLimit));
362 env.close();
363
364 BrokerParameters params = BrokerParameters::defaults();
365 params.vaultDeposit = 100'000;
366 params.coverRateMin = TenthBips32{0};
367 params.coverDeposit = 0;
368 params.debtMax = 0;
370
371 env(pay(issuer, lender, iou(params.vaultDeposit)));
372 env(pay(issuer, mm2, iou(20'000)));
373 env.close();
374
375 BrokerInfo const broker{createVaultAndBroker(env, iou, lender, params)};
376
377 env(offer(mm1, iou(1'000), XRP(400)));
378 env.close();
379
380 return {
381 .iou = iou,
382 .issuer = issuer,
383 .lender = lender,
384 .borrower = borrower,
385 .mm1 = mm1,
386 .mm2 = mm2,
387 .broker = broker};
388 }
389
390 void
392 {
393 // A borrower profits risk-free from a loan and two crossing DEX
394 // offers in one atomic batch. Crossed offers execute the moment both
395 // rest on the book, so mm2's offer is itself a batch inner, placed
396 // right after the borrower's buy leg consumes mm1.
397 testcase("arbitrage across a loan and two crossing offers in a batch");
398
399 using namespace jtx;
400
401 Env env(*this, features);
402 auto const setup = setupArbitrage(env);
403 auto const& iou = setup.iou;
404 auto const& lender = setup.lender;
405 auto const& borrower = setup.borrower;
406 auto const& broker = setup.broker;
407 auto const& mm2 = setup.mm2;
408
409 auto const loanKeylet = nextLoanKeylet(env, broker);
410 auto const principal = iou(1'000);
411
412 BEAST_EXPECT(env.balance(borrower, iou) == iou(0));
413
414 auto const borrowerSeq = env.seq(borrower);
415 auto const mm2Seq = env.seq(mm2);
416
417 auto const loanSetTx = loanSetInner(env, borrower, broker.brokerID, lender, principal, 2);
418 auto const loanPayTx =
419 loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
420
421 auto const batchFee = batch::calcBatchFee(env, 2, 5);
422 env(batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
423 batch::Inner(loanSetTx, borrowerSeq + 1),
425 offer(borrower, XRP(400), principal.value(), tfFillOrKill), borrowerSeq + 2),
426 batch::Inner(offer(mm2, XRP(400), iou(1'008)), mm2Seq),
428 offer(borrower, iou(1'008).value(), XRP(400), tfFillOrKill), borrowerSeq + 3),
429 batch::Inner(loanPayTx, borrowerSeq + 4),
430 batch::Sig(lender, mm2),
431 Ter(tesSUCCESS));
432 env.close();
433
434 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
435 BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == 0);
436 BEAST_EXPECT(env.balance(borrower, iou) == iou(8));
437 env.require(Offers(setup.mm1, 0));
438 env.require(Offers(mm2, 0));
439 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
440 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
441 if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
442 {
443 BEAST_EXPECT(
444 vaultSle->at(sfAssetsAvailable) == iou(broker.params.vaultDeposit).value());
445 }
446 }
447
448 void
450 {
451 // Without a second market maker willing to buy XRP back at the
452 // higher price, the sell leg's tfFillOrKill offer cannot fill and
453 // returns tecKILLED. tfAllOrNothing then rolls back every inner,
454 // including the loan origination; the outer batch itself still
455 // returns tesSUCCESS.
456 testcase("arbitrage batch rolls back when the sell offer cannot fill");
457
458 using namespace jtx;
459
460 Env env(*this, features);
461 auto const setup = setupArbitrage(env);
462 auto const& iou = setup.iou;
463 auto const& lender = setup.lender;
464 auto const& borrower = setup.borrower;
465 auto const& broker = setup.broker;
466
467 auto const brokerSleBefore = env.le(broker.brokerKeylet());
468 auto const vaultSleBefore = env.le(broker.vaultKeylet());
469 if (!BEAST_EXPECT(brokerSleBefore) || !BEAST_EXPECT(vaultSleBefore))
470 return;
471 auto const loanSequenceBefore = brokerSleBefore->at(sfLoanSequence);
472 auto const assetsAvailableBefore = vaultSleBefore->at(sfAssetsAvailable);
473
474 auto const loanKeylet = nextLoanKeylet(env, broker);
475 auto const principal = iou(1'000);
476
477 auto const borrowerSeq = env.seq(borrower);
478 auto const borrowerBalanceBefore = env.balance(borrower);
479
480 auto const loanSetTx = loanSetInner(env, borrower, broker.brokerID, lender, principal, 2);
481 auto const loanPayTx =
482 loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment);
483
484 auto const batchFee = batch::calcBatchFee(env, 1, 4);
485 env(batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
486 batch::Inner(loanSetTx, borrowerSeq + 1),
488 offer(borrower, XRP(400), principal.value(), tfFillOrKill), borrowerSeq + 2),
490 offer(borrower, iou(1'008).value(), XRP(400), tfFillOrKill), borrowerSeq + 3),
491 batch::Inner(loanPayTx, borrowerSeq + 4),
492 batch::Sig(lender),
493 Ter(tesSUCCESS));
494 env.close();
495
496 BEAST_EXPECT(!env.le(loanKeylet));
497 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
498 {
499 BEAST_EXPECT(brokerSle->at(sfLoanSequence) == loanSequenceBefore);
500 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
501 }
502 BEAST_EXPECT(env.balance(borrower, iou) == iou(0));
503 BEAST_EXPECT(env.balance(borrower) == borrowerBalanceBefore - batchFee);
504 env.require(Offers(setup.mm1, 1));
505 if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
506 BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == assetsAvailableBefore);
507 }
508
509 // Originates a loan outside any batch: one payment due in a day, a day of
510 // grace, zero interest. PaymentTotal 1 is fine because callers only
511 // impair or default the loan and never pay it off early.
512 static void
514 jtx::Env& env,
515 BrokerInfo const& broker,
516 jtx::Account const& lender,
517 jtx::Account const& borrower,
518 STAmount const& principal)
519 {
520 using namespace jtx;
521 using namespace jtx::loan;
522
523 env(set(lender, broker.brokerID, principal),
524 kCounterparty(borrower.id()),
525 kPaymentTotal(1),
526 kPaymentInterval(86400),
527 kGracePeriod(86400),
528 Sig(sfCounterpartySignature, borrower),
529 Fee(env.current()->fees().base * 2));
530 env.close();
531 }
532
533 void
535 {
536 // Impairing a loan records its exposure as the vault's unrealized
537 // loss; the broker tops up first-loss cover in the same batch. Under
538 // fixCleanup3_4_0 LoanManage refuses tfLoanImpair with tecTOO_SOON
539 // until a payment is late, so the impairment cannot share a batch
540 // with the LoanSet that creates the loan.
541 testcase("loan impairment and cover deposit in a batch");
542
543 using namespace jtx;
544
545 Env env(*this, features);
546
547 Account const lender{"lender"};
548 Account const borrower{"borrower"};
549 env.fund(XRP(100'000'000), lender, borrower);
550 env.close();
551
552 PrettyAsset const asset{xrpIssue(), 1'000'000};
553 auto const broker = createVaultAndBroker(env, asset, lender);
554
555 auto const loanKeylet = nextLoanKeylet(env, broker);
556 originateLoan(env, broker, lender, borrower, asset(1'000).value());
557
558 auto const loanSleBefore = env.le(loanKeylet);
559 if (!BEAST_EXPECT(loanSleBefore))
560 return;
561
562 // Advance past NextPaymentDueDate so the payment is late.
563 using D = NetClock::duration;
564 using Tp = NetClock::time_point;
565 env.close(Tp{D{loanSleBefore->at(sfNextPaymentDueDate) + 1}});
566
567 auto const brokerSleBefore = env.le(broker.brokerKeylet());
568 auto const vaultSleBefore = env.le(broker.vaultKeylet());
569 if (!BEAST_EXPECT(brokerSleBefore) || !BEAST_EXPECT(vaultSleBefore))
570 return;
571 auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
572 auto const lossUnrealizedBefore = vaultSleBefore->at(sfLossUnrealized);
573 auto const additionalCover = asset(500).value();
574
575 auto const lenderSeq = env.seq(lender);
576 auto const batchFee = batch::calcBatchFee(env, 0, 2);
577 env(batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
578 batch::Inner(loan::manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 1),
580 loan_broker::coverDeposit(lender, broker.brokerID, additionalCover), lenderSeq + 2),
581 Ter(tesSUCCESS));
582 env.close();
583
584 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
585 BEAST_EXPECT(loanSle->isFlag(lsfLoanImpaired));
586 // Zero interest and fees make the exposure equal the outstanding
587 // principal under both accounting modes.
588 if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
589 {
590 BEAST_EXPECT(
591 vaultSle->at(sfLossUnrealized) ==
592 lossUnrealizedBefore + loanSleBefore->at(sfPrincipalOutstanding));
593 }
594 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
595 BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == coverAvailableBefore + additionalCover);
596 }
597
598 void
600 {
601 // A defaulted loan burns first-loss cover per CoverRateLiquidation;
602 // the remainder can be withdrawn once DebtTotal drops to zero. The
603 // default runs in its own batch: LoanManage refuses tfLoanDefault
604 // with tecTOO_SOON until the grace period elapses, so it cannot share
605 // a batch with the LoanSet that creates the loan.
606 testcase("loan default and cover withdraw in a batch");
607
608 using namespace jtx;
609
610 Env env(*this, features);
611
612 Account const lender{"lender"};
613 Account const borrower{"borrower"};
614 env.fund(XRP(100'000'000), lender, borrower);
615 env.close();
616
617 PrettyAsset const asset{xrpIssue(), 1'000'000};
618 auto const broker = createVaultAndBroker(env, asset, lender);
619
620 auto const loanKeylet = nextLoanKeylet(env, broker);
621 originateLoan(env, broker, lender, borrower, asset(1'000).value());
622
623 auto const loanSleBefore = env.le(loanKeylet);
624 if (!BEAST_EXPECT(loanSleBefore))
625 return;
626
627 // Advance past NextPaymentDueDate + GracePeriod so the default is no
628 // longer refused with tecTOO_SOON.
629 auto const dueDate = loanSleBefore->at(sfNextPaymentDueDate);
630 auto const gracePeriod = loanSleBefore->at(sfGracePeriod);
631 using D = NetClock::duration;
632 using Tp = NetClock::time_point;
633 env.close(Tp{D{dueDate + gracePeriod + 1}});
634
635 auto const brokerSleBefore = env.le(broker.brokerKeylet());
636 auto const vaultSleBefore = env.le(broker.vaultKeylet());
637 if (!BEAST_EXPECT(brokerSleBefore) || !BEAST_EXPECT(vaultSleBefore))
638 return;
639
640 auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
641 auto const assetsAvailableBefore = vaultSleBefore->at(sfAssetsAvailable);
642 auto const assetsTotalBefore = vaultSleBefore->at(sfAssetsTotal);
643
644 TenthBips32 const coverRateMin{brokerSleBefore->at(sfCoverRateMinimum)};
645 TenthBips32 const coverRateLiquidation{brokerSleBefore->at(sfCoverRateLiquidation)};
646 Number const totalDefaultAmount{loanSleBefore->at(sfPrincipalOutstanding)};
647
648 // DefaultCovered = min(MinimumCover x CoverRateLiquidation,
649 // DefaultAmount, CoverAvailable), matching LoanManage::defaultLoan.
650 // Zero interest and fees make DefaultAmount equal the outstanding
651 // principal under both accounting modes, so no
652 // featureLendingProtocolV1_1 branch is needed here.
653 Number defaultCovered;
654 {
656 auto const minimumCover =
657 tenthBipsOfValue(Number{brokerSleBefore->at(sfDebtTotal)}, coverRateMin);
658 defaultCovered = std::min(
660 asset.raw(),
661 std::min(
662 tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount),
663 loanSleBefore->at(sfLoanScale)),
664 Number{coverAvailableBefore});
665 }
666 auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;
667
668 auto const lenderSeq = env.seq(lender);
669 auto const lenderBalanceBefore = env.balance(lender);
670 auto const withdrawAmount = STAmount{asset.raw(), coverAvailableBefore - defaultCovered};
671
672 auto const batchFee = batch::calcBatchFee(env, 0, 2);
673 env(batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
674 batch::Inner(loan::manage(lender, loanKeylet.key, tfLoanDefault), lenderSeq + 1),
676 loan_broker::coverWithdraw(lender, broker.brokerID, withdrawAmount), lenderSeq + 2),
677 Ter(tesSUCCESS));
678 env.close();
679
680 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
681 {
682 BEAST_EXPECT(loanSle->isFlag(lsfLoanDefault));
683 BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == 0);
684 }
685 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
686 {
687 BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == 0);
688 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
689 }
690 BEAST_EXPECT(env.balance(lender) == lenderBalanceBefore - batchFee + withdrawAmount);
691 // The default moves defaultCovered from the broker's cover to the
692 // vault; the cover withdraw pays out of the broker's own
693 // pseudo-account and does not touch the vault again.
694 if (auto const vaultSle = env.le(broker.vaultKeylet()); BEAST_EXPECT(vaultSle))
695 {
696 BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == assetsAvailableBefore + defaultCovered);
697 BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == assetsTotalBefore - vaultDefaultAmount);
698 }
699 }
700
701 void
703 {
704 // tfIndependent runs every inner regardless of earlier failures.
705 // VaultWithdraw fails before anything is deposited: the empty vault
706 // converts the amount to zero shares (tecPRECISION_LOSS). VaultDelete fails
707 // once the vault holds a deposit (tecHAS_OBLIGATIONS); both are
708 // tec-class results, so neither stops the inners that follow.
709 testcase("independent batch runs every inner despite failing inners");
710
711 using namespace jtx;
712
713 Env env(*this, features);
714
715 Account const owner{"owner"};
716 env.fund(XRP(100'000), owner);
717 env.close();
718
719 Vault const vault{env};
720
721 auto const seq = env.seq(owner);
722 auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(seq + 1));
723 auto const createTx = std::get<0>(vault.create({.owner = owner, .asset = xrpIssue()}));
724 auto const amount = XRP(1'000);
725
726 auto const batchFee = batch::calcBatchFee(env, 0, 4);
727 auto const batchTx = env.jt(
728 batch::outer(owner, seq, batchFee, tfIndependent),
729 batch::Inner(createTx, seq + 1),
731 vault.withdraw({.depositor = owner, .id = vaultKeylet.key, .amount = amount}),
732 seq + 2),
734 vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = amount}),
735 seq + 3),
736 batch::Inner(vault.del({.owner = owner, .id = vaultKeylet.key}), seq + 4));
737 env(batchTx, Ter(tesSUCCESS));
738 env.close();
739
740 auto const batchID = strHex(batchTx.stx->getTransactionID());
741 auto const& innerIDs = batchTx.stx->getBatchTransactionIDs();
742 if (!BEAST_EXPECT(innerIDs.size() == 4))
743 return;
744 batch::validateInnerTxn(env, batchID, strHex(innerIDs[0]), "VaultCreate", "tesSUCCESS");
746 env, batchID, strHex(innerIDs[1]), "VaultWithdraw", "tecPRECISION_LOSS");
747 batch::validateInnerTxn(env, batchID, strHex(innerIDs[2]), "VaultDeposit", "tesSUCCESS");
749 env, batchID, strHex(innerIDs[3]), "VaultDelete", "tecHAS_OBLIGATIONS");
750
751 if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
752 BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == amount.value());
753 BEAST_EXPECT(env.seq(owner) == seq + 5);
754 }
755
756 void
758 {
759 // Same claim as testIndependentVaultChain, on a loan chain. The
760 // LoanSet inner leaves the borrower holding only the 1,000 principal,
761 // so a LoanPay for 2,000 fails preclaim's balance check
762 // (tecINSUFFICIENT_FUNDS); the cover deposit submitted after it
763 // still lands.
764 testcase("independent batch runs every loan inner despite a failing payment");
765
766 using namespace jtx;
767
768 Env env(*this, features);
769
770 Account const issuer{"issuer"};
771 Account const lender{"lender"};
772 Account const borrower{"borrower"};
773 env.fund(XRP(1'000'000), issuer, lender, borrower);
774 env(fset(issuer, asfDefaultRipple));
775 env.close();
776
777 PrettyAsset const iou = issuer[iouCurrency_];
778 STAmount const iouLimit = iou(1'000'000'000);
779 env(trust(lender, iouLimit));
780 env(trust(borrower, iouLimit));
781 env.close();
782
783 env(pay(issuer, lender, iou(2'000'000)));
784 env.close();
785
786 auto const broker = createVaultAndBroker(env, iou, lender);
787
788 auto const loanKeylet = nextLoanKeylet(env, broker);
789 auto const principal = iou(1'000);
790 auto const brokerSleBefore = env.le(broker.brokerKeylet());
791 if (!BEAST_EXPECT(brokerSleBefore))
792 return;
793 auto const coverAvailableBefore = brokerSleBefore->at(sfCoverAvailable);
794
795 auto const lenderSeq = env.seq(lender);
796 auto const borrowerSeq = env.seq(borrower);
797
798 auto const loanSetTx = loanSetInner(env, lender, broker.brokerID, borrower, principal, 2);
799 auto const loanPayTx =
800 loan::pay(borrower, loanKeylet.key, iou(2'000).value(), tfLoanFullPayment);
801 auto const additionalCover = iou(500);
802
803 auto const batchFee = batch::calcBatchFee(env, 1, 3);
804 auto const batchTx = env.jt(
805 batch::outer(lender, lenderSeq, batchFee, tfIndependent),
806 batch::Inner(loanSetTx, lenderSeq + 1),
807 batch::Inner(loanPayTx, borrowerSeq),
809 loan_broker::coverDeposit(lender, broker.brokerID, additionalCover), lenderSeq + 2),
810 batch::Sig(borrower));
811 env(batchTx, Ter(tesSUCCESS));
812 env.close();
813
814 auto const batchID = strHex(batchTx.stx->getTransactionID());
815 auto const& innerIDs = batchTx.stx->getBatchTransactionIDs();
816 if (!BEAST_EXPECT(innerIDs.size() == 3))
817 return;
818 batch::validateInnerTxn(env, batchID, strHex(innerIDs[0]), "LoanSet", "tesSUCCESS");
820 env, batchID, strHex(innerIDs[1]), "LoanPay", "tecINSUFFICIENT_FUNDS");
822 env, batchID, strHex(innerIDs[2]), "LoanBrokerCoverDeposit", "tesSUCCESS");
823
824 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
825 BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == principal.value());
826 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
827 {
828 BEAST_EXPECT(
829 brokerSle->at(sfCoverAvailable) == coverAvailableBefore + additionalCover.value());
830 }
831 }
832
833public:
834 void
835 run() override
836 {
837 // Batch rejects Vault and Lending inners before LendingProtocolV1_2.
838 FeatureBitset const lendingBatch{all_ | featureLendingProtocolV1_2};
839 for (auto const& features : {lendingBatch, lendingBatch | featureLendingProtocolV1_1})
840 {
841 testVaultLifecycle(features);
842 testArbitrage(features);
843 testArbitrageRollback(features);
847 testIndependentLoanChain(features);
848 }
849 testClosedEndedVaultLifecycle(lendingBatch | featureLendingProtocolV1_1);
851 testLoanLifecycleClosedEndedVault(lendingBatch | featureLendingProtocolV1_1);
852 }
853};
854
855BEAST_DEFINE_TESTSUITE(LendingBatch, tx, xrpl);
856
857} // namespace xrpl::test
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
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
STAmount const & value() const noexcept
Definition STAmount.h:610
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
void testLoanLifecycleClosedEndedVault(FeatureBitset features)
void testVaultLifecycle(FeatureBitset features)
void assertLoanFullyRepaid(jtx::Env const &env, Keylet const &brokerKeylet, Keylet const &vaultKeylet, Keylet const &loanKeylet, STAmount const &depositValue)
void testLoanLifecycleOpenEndedVault(FeatureBitset features)
void testArbitrage(FeatureBitset features)
ArbitrageSetup setupArbitrage(jtx::Env &env)
void testIndependentLoanChain(FeatureBitset features)
void run() override
Runs the suite.
void testIndependentVaultChain(FeatureBitset features)
static void originateLoan(jtx::Env &env, BrokerInfo const &broker, jtx::Account const &lender, jtx::Account const &borrower, STAmount const &principal)
void testImpairAndCoverDeposit(FeatureBitset features)
void testClosedEndedVaultLifecycle(FeatureBitset features)
void testArbitrageRollback(FeatureBitset features)
void testDefaultAndCoverWithdraw(FeatureBitset features)
void runVaultLifecycleBatch(jtx::Env &env, jtx::Account const &owner, json::Value const &createTx)
static json::Value loanSetInner(jtx::Env &env, jtx::Account const &account, uint256 const &brokerID, jtx::Account const &counterparty, STAmount const &principal, std::uint32_t paymentTotal)
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())
Keylet nextLoanKeylet(jtx::Env const &env, BrokerInfo const &broker)
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
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:752
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
std::uint32_t ownerCount(Account const &account) const
Return the number of objects owned by an account.
Definition Env.cpp:266
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
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:723
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:766
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
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Adds an inner Batch transaction to a JTx and autofills it.
Definition batch.h:91
Sets the Batch transaction signers on a JTx.
Definition batch.h:142
T min(T... args)
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:591
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
void validateInnerTxn(Env &env, std::string const &batchID, std::string const &txHash, std::string const &txType, std::string const &result, std::source_location const &loc=std::source_location::current())
Expect an inner Batch transaction to be recorded with the given type and result, and with ParentBatch...
Definition batch.cpp:68
json::Value outer(jtx::Account const &account, uint32_t seq, STAmount const &fee, std::uint32_t flags)
Build an outer Batch transaction JSON object.
Definition batch.cpp:55
XRPAmount calcBatchFee(jtx::Env const &env, uint32_t const &numSigners, uint32_t const &txns=0)
Calculate the expected outer Batch transaction fee.
Definition batch.cpp:37
json::Value set(AccountID const &account, UInt256 const &vaultId, uint32_t flags)
json::Value coverWithdraw(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value coverDeposit(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value manage(AccountID const &account, UInt256 const &loanID, std::uint32_t flags)
json::Value pay(AccountID const &account, UInt256 const &loanID, STAmount const &amount, std::uint32_t flags)
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
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
OwnerCount< ltOFFER > Offers
Match the number of offers in the account's owner directory.
Definition owners.h:137
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
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
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
STTx createTx(bool disabling, LedgerIndex seq, PublicKey const &txKey)
Create ttUNL_MODIFY Tx.
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
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:13
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:139
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:735
@ tesSUCCESS
Definition TER.h:250
UInt256 uint256
Definition base_uint.h:586
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
STAmount const & value() const
Set the sequence number on a JTx.
Definition seq.h:16
std::tuple< json::Value, Keylet > create(CreateArgs const &args) const
Return a VaultCreate transaction and the Vault's expected keylet.
Definition vault.cpp:26
std::tuple< json::Value, Keylet, NetClock::time_point > createClosedEnded(CreateClosedEndedArgs const &args) const
Return a VaultCreate transaction for a closed-ended vault, its expected keylet, and the vault's Subsc...
Definition vault.cpp:48