xrpld
Loading...
Searching...
No Matches
LoanCoverFreezeAuth_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/credentials.h>
7#include <test/jtx/fee.h>
8#include <test/jtx/flags.h>
9#include <test/jtx/mpt.h>
10#include <test/jtx/pay.h>
11#include <test/jtx/permissioned_domains.h>
12#include <test/jtx/ter.h>
13#include <test/jtx/trust.h>
14#include <test/jtx/vault.h>
15
16#include <xrpl/basics/Number.h>
17#include <xrpl/basics/chrono.h>
18#include <xrpl/beast/unit_test/suite.h>
19#include <xrpl/json/json_value.h>
20#include <xrpl/json/to_string.h>
21#include <xrpl/protocol/Feature.h>
22#include <xrpl/protocol/Indexes.h>
23#include <xrpl/protocol/Issue.h>
24#include <xrpl/protocol/LedgerFormats.h>
25#include <xrpl/protocol/SField.h>
26#include <xrpl/protocol/SeqProxy.h>
27#include <xrpl/protocol/TER.h>
28#include <xrpl/protocol/TxFlags.h>
29#include <xrpl/protocol/Units.h>
30
31#include <cstdint>
32
33namespace xrpl::test {
34
36{
37private:
38 void
40 {
41 testcase << "First-Loss Capital Depletion on Sequential Defaults";
42
43 using namespace jtx;
44 using namespace loan;
45 using namespace loan_broker;
46
47 Env env{*this, features};
48
49 Account const issuer{"issuer"};
50 Account const lender{"lender"};
51 Account const borrowerA{"borrowerA"};
52 Account const borrowerB{"borrowerB"};
53
54 env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB);
55 env.close();
56
57 PrettyAsset const asset = xrpIssue();
58 auto const vaultDepositAmount =
59 asset(200'000); // Enough for 2 x 50k loans plus interest/fees
60
61 auto const brokerInfo = createVaultAndBroker(
62 env,
63 asset,
64 lender,
65 {
66 .vaultDeposit = vaultDepositAmount.value(),
67 .debtMax = 0,
68 .coverRateMin = TenthBips32(20000), // 20%
69 .coverDeposit = 21'000,
70 .managementFeeRate = TenthBips16(100), // 0.1%
71 .coverRateLiquidation = TenthBips32(100000),
72 });
73 auto const brokerKeylet = brokerInfo.brokerKeylet();
74
75 // Create two identical loans: each 50,000 XRP principal (scaled down to
76 // avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal
77 // + interest) Formula will calculate cover as: 100% × (20% × 100,000) =
78 // 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first
79 // default
80 auto const principalAmount = Number(50'000);
81 auto const loanPaymentInterval = 2592000; // 30 days
82 auto const loanGracePeriod = 604800; // 7 days
83
84 // Create Loan A
85 auto loanATx = env.jt(
86 set(borrowerA, brokerKeylet.key, principalAmount),
87 Sig(sfCounterpartySignature, lender),
88 kInterestRate(TenthBips32(500)), // 5%
89 kPaymentTotal(12),
90 loan::kPaymentInterval(loanPaymentInterval),
91 loan::kGracePeriod(loanGracePeriod),
92 Fee(XRP(10))); // Sufficient fee for multi-sig transaction
93 env(loanATx);
94 env.close();
95
96 auto const loanAKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
97
98 // Create Loan B
99 auto loanBTx = env.jt(
100 set(borrowerB, brokerKeylet.key, principalAmount),
101 Sig(sfCounterpartySignature, lender),
102 kInterestRate(TenthBips32(500)), // 5%
103 kPaymentTotal(12),
104 loan::kPaymentInterval(loanPaymentInterval),
105 loan::kGracePeriod(loanGracePeriod),
106 Fee(XRP(10))); // Sufficient fee for multi-sig transaction
107 env(loanBTx);
108 env.close();
109
110 auto const loanBKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(2));
111
112 auto loanASle = env.le(loanAKeylet);
113 if (!BEAST_EXPECT(loanASle))
114 return;
115
116 // Advance time past grace period for both loans to be defaultable
117 auto const loanANextDue = loanASle->at(sfNextPaymentDueDate);
118 auto const loanAGrace = loanASle->at(sfGracePeriod);
119 env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60});
120
121 env(manage(lender, loanAKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
122 env.close();
123
124 // Verify Loan A is defaulted
125 loanASle = env.le(loanAKeylet);
126 if (!BEAST_EXPECT(loanASle))
127 return;
128 BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault));
129 BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0);
130
131 // Check broker state after first default (from committed ledger)
132 auto brokerSle = env.le(brokerKeylet);
133 if (!BEAST_EXPECT(brokerSle))
134 return;
135 auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal);
136 auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable);
137
138 // DebtTotal should have decreased by Loan A's debt
139 BEAST_EXPECT(afterFirstDebtTotal == 50'134);
140
141 // CoverAvailable should have decreased significantly
142 BEAST_EXPECT(afterFirstCoverAvailable == 946);
143
144 env(manage(lender, loanBKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
145
146 brokerSle = env.le(brokerKeylet);
147 if (!BEAST_EXPECT(brokerSle))
148 return;
149 auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal);
150 auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable);
151
152 BEAST_EXPECT(afterSecondDebtTotal == 0);
153
154 BEAST_EXPECT(afterSecondCoverAvailable == 0);
155 }
156
157 // Tests that vault withdrawals work correctly when the vault has unrealized
158 // loss from an impaired loan, ensuring the invariant check properly
159 // accounts for the loss.
160 void
162 {
163 using namespace jtx;
164 using namespace loan;
165 using namespace std::chrono_literals;
166
167 testcase("Vault withdraw reflects sfLossUnrealized");
168
169 // Test constants
170 static constexpr std::int64_t kInitialFunding = 1'000'000;
171 static constexpr std::int64_t kLenderInitialIou = 5'000'000;
172 static constexpr std::int64_t kDepositorInitialIou = 1'000'000;
173 static constexpr std::int64_t kBorrowerInitialIou = 100'000;
174 static constexpr std::int64_t kDepositAmount = 5'000;
175 static constexpr std::int64_t kPrincipalAmount = 99;
176 static constexpr std::uint64_t kExpectedSharesPerDepositor = 5'000'000'000;
177 static constexpr std::uint32_t kLocalPaymentInterval = 600;
178 static constexpr std::uint32_t kLocalPaymentTotal = 2;
179
180 Env env{*this, features};
181
182 // Setup accounts
183 Account const issuer{"issuer"};
184 Account const lender{"lender"};
185 Account const depositorA{"lpA"};
186 Account const depositorB{"lpB"};
187 Account const borrower{"borrowerA"};
188
189 env.fund(XRP(kInitialFunding), issuer, lender, depositorA, depositorB, borrower);
190 env.close();
191
192 // Setup trust lines
193 PrettyAsset const iouAsset = issuer[iouCurrency_];
194 env(trust(lender, iouAsset(10'000'000)));
195 env(trust(depositorA, iouAsset(10'000'000)));
196 env(trust(depositorB, iouAsset(10'000'000)));
197 env(trust(borrower, iouAsset(10'000'000)));
198 env.close();
199
200 // Fund accounts with IOUs
201 env(pay(issuer, lender, iouAsset(kLenderInitialIou)));
202 env(pay(issuer, depositorA, iouAsset(kDepositorInitialIou)));
203 env(pay(issuer, depositorB, iouAsset(kDepositorInitialIou)));
204 env(pay(issuer, borrower, iouAsset(kBorrowerInitialIou)));
205 env.close();
206
207 // Create vault and broker, then add deposits from two depositors
208 auto const broker = createVaultAndBroker(env, iouAsset, lender);
209 Vault v{env};
210
211 env(v.deposit({
212 .depositor = depositorA,
213 .id = broker.vaultKeylet().key,
214 .amount = iouAsset(kDepositAmount),
215 }),
216 Ter(tesSUCCESS));
217 env(v.deposit({
218 .depositor = depositorB,
219 .id = broker.vaultKeylet().key,
220 .amount = iouAsset(kDepositAmount),
221 }),
222 Ter(tesSUCCESS));
223 env.close();
224
225 // Create a loan
226 auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
227 if (!BEAST_EXPECT(sleBroker))
228 return;
229
230 auto const loanKeylet =
231 keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
232
233 env(set(borrower, broker.brokerID, kPrincipalAmount),
234 Sig(sfCounterpartySignature, lender),
235 kPaymentTotal(kLocalPaymentTotal),
236 kPaymentInterval(kLocalPaymentInterval),
237 Fee(env.current()->fees().base * 2),
238 Ter(tesSUCCESS));
239 env.close();
240
241 // Under fixCleanup3_4_0 impair requires the payment to be late.
242 advancePastDueDate(env, loanKeylet);
243
244 // Impair the loan to create unrealized loss
245 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
246 env.close();
247
248 // Verify unrealized loss is recorded in the vault
249 auto const vaultAfterImpair = env.le(broker.vaultKeylet());
250 if (!BEAST_EXPECT(vaultAfterImpair))
251 return;
252
253 BEAST_EXPECT(
254 vaultAfterImpair->at(sfLossUnrealized) == broker.asset(kPrincipalAmount).value());
255
256 // Helper to get share balance for a depositor
257 auto const shareAsset = vaultAfterImpair->at(sfShareMPTID);
258 auto const getShareBalance = [&](Account const& depositor) -> std::uint64_t {
259 auto const token = env.le(keylet::mptoken(shareAsset, depositor.id()));
260 return token ? token->getFieldU64(sfMPTAmount) : 0;
261 };
262
263 // Verify both depositors have equal shares
264 auto const sharesLpA = getShareBalance(depositorA);
265 auto const sharesLpB = getShareBalance(depositorB);
266 BEAST_EXPECT(sharesLpA == kExpectedSharesPerDepositor);
267 BEAST_EXPECT(sharesLpB == kExpectedSharesPerDepositor);
268 BEAST_EXPECT(sharesLpA == sharesLpB);
269
270 // Helper to attempt withdrawal
271 auto const attemptWithdrawShares = [&](Account const& depositor,
272 std::uint64_t shareAmount,
273 TER expected) {
274 STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)};
275 env(v.withdraw(
276 {.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}),
277 Ter(expected));
278 env.close();
279 };
280
281 // Regression test: Both depositors should successfully withdraw despite
282 // unrealized loss. Previously failed with invariant violation:
283 // "withdrawal must change vault and destination balance by equal
284 // amount". This was caused by sharesToAssetsWithdraw rounding down,
285 // creating a mismatch where vaultDeltaAssets * -1 != destinationDelta
286 // when unrealized loss exists.
287 attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS);
288 attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
289 }
290
291 void
293 {
294 testcase << "Service Fee On Broker Deep Freeze";
295 using namespace jtx;
296 using namespace loan;
297 Account const issuer("issuer");
298 Account const borrower("borrower");
299 Account const broker("broker");
300 auto const iou = issuer["IOU"];
301
302 for (bool const deepFreeze : {true, false})
303 {
304 Env env(*this);
305
306 auto getCoverBalance = [&](BrokerInfo const& brokerInfo, auto const& accountField) {
307 if (auto const le = env.le(keylet::loanBroker(brokerInfo.brokerID));
308 BEAST_EXPECT(le))
309 {
310 auto const account = le->at(accountField);
311 if (auto const sleLine = env.le(keylet::trustLine(account, iou));
312 BEAST_EXPECT(sleLine))
313 {
314 STAmount balance = sleLine->at(sfBalance);
315 if (account > issuer.id())
316 balance.negate();
317 return balance;
318 }
319 }
320 return STAmount{iou};
321 };
322
323 env.fund(XRP(20'000), issuer, broker, borrower);
324 env.close();
325
326 env(trust(broker, iou(20'000'000)));
327 env(pay(issuer, broker, iou(10'000'000)));
328 env.close();
329
330 auto const brokerInfo = createVaultAndBroker(env, iou, broker);
331
332 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
333
334 auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
335
336 env(set(borrower, brokerInfo.brokerID, 10'000),
337 Sig(sfCounterpartySignature, broker),
338 kLoanServiceFee(iou(100).value()),
339 kPaymentInterval(100),
340 Fee(XRP(100)));
341 env.close();
342
343 env(trust(borrower, iou(20'000'000)));
344 // The borrower increases their limit and acquires some IOU so
345 // they can pay interest
346 env(pay(issuer, borrower, iou(500)));
347 env.close();
348
349 if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le))
350 {
351 if (deepFreeze)
352 {
353 env(trust(issuer, broker["IOU"](0), tfSetFreeze | tfSetDeepFreeze));
354 env.close();
355 }
356
357 env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)));
358 env.close();
359
360 if (deepFreeze)
361 {
362 // The fee goes to the broker pseudo-account
363 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'100));
364 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'000));
365 }
366 else
367 {
368 // The fee goes to the broker account
369 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'100));
370 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
371 }
372 }
373 };
374 }
375
376 void
378 {
379 testcase("LoanManage: default bypasses asset freeze");
380 using namespace jtx;
381 using namespace loan;
382 Account const lender{"lender"};
383 Account const issuer{"issuer"};
384 Account const borrower{"borrower"};
385 auto const iou = issuer["IOU"];
386
387 Env env(*this);
388 env.fund(XRP(1'000), lender, issuer, borrower);
389 env(trust(lender, iou(10'000'000)));
390 env(pay(issuer, lender, iou(5'000'000)));
391 BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
392
393 auto const loanSetFee = Fee(env.current()->fees().base * 2);
394 STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
395
396 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
397 Sig(sfCounterpartySignature, lender),
398 loanSetFee);
399 env.close();
400
401 auto const loanKeylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
402
403 using Tp = NetClock::time_point;
404 using D = NetClock::duration;
405
406 // Get past the grace period so the loan is defaultable.
407 if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
408 {
409 env.close(Tp{D{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
410 }
411
412 // Global freeze trips the post-apply TransfersNotFrozen invariant.
413 env(fset(issuer, asfGlobalFreeze));
414 env.close();
415
416 // Pre-fixCleanup3_4_0, the invariant blocks the default.
417 env.disableFeature(fixCleanup3_4_0);
418 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecINVARIANT_FAILED));
419 env.close();
420
421 // Per XLS-0066, a default must succeed despite the freeze.
422 env.enableFeature(fixCleanup3_4_0);
423 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
424 }
425
426 // A default must bypass an MPT global lock the same way it bypasses IOU
427 // freeze, including when the loan was already impaired beforehand
428 // (a different defaultLoan() accounting branch than the un-impaired
429 // path exercised above) and after an ordinary LoanPay was correctly
430 // blocked by the same lock.
431 void
433 {
434 testcase("LoanManage: default bypasses MPT lock after impairment");
435 using namespace jtx;
436 using namespace loan;
437
438 Account const issuer{"issuer"};
439 Account const lender{"lender"};
440 Account const borrower{"borrower"};
441
442 Env env(*this);
443 env.fund(XRP(1'000'000), issuer, lender, borrower);
444 env.close();
445
446 MPTTester mptt(
447 {.env = env,
448 .issuer = issuer,
449 .holders = {lender, borrower},
450 .flags = tfMPTCanTransfer | tfMPTCanLock});
451 PrettyAsset const asset = mptt.issuanceID();
452 env(pay(issuer, lender, asset(10'000'000)));
453 env.close();
454
455 BrokerInfo const brokerInfo{createVaultAndBroker(env, asset, lender)};
456
457 auto const loanSetFee = Fee(env.current()->fees().base * 2);
458 STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
459 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
460 Sig(sfCounterpartySignature, lender),
461 loanSetFee);
462 env.close();
463
464 auto const loanKeylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
465
466 // Realize a loss via impairment before locking.
467 advancePastDueDate(env, loanKeylet);
468 env(manage(lender, loanKeylet.key, tfLoanImpair));
469 env.close();
470
471 // Issuer applies a global lock.
472 mptt.set({.account = issuer, .flags = tfMPTLock});
473 env.close();
474
475 // An ordinary payment is correctly blocked by the lock.
476 env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecLOCKED));
477 env.close();
478
479 using Tp = NetClock::time_point;
480 using D = NetClock::duration;
481 if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
482 {
483 env.close(Tp{D{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
484 }
485
486 // Pre-fixCleanup3_4_0 the ValidMPTTransfer invariant blocks the
487 // default, mirroring the IOU path above.
488 env.disableFeature(fixCleanup3_4_0);
489 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecINVARIANT_FAILED));
490 env.close();
491
492 // The default itself must succeed despite the lock.
493 env.enableFeature(fixCleanup3_4_0);
494 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
495 }
496
497 // The exemption must hold for an individually deep-frozen trust line, not
498 // just a global freeze: deep freeze is what the original report ran into,
499 // and it takes a different path through validateFrozenState (the frozen
500 // flag comes off the line rather than off the issuer).
501 void
503 {
504 testcase("LoanManage: default bypasses asset deep freeze");
505 using namespace jtx;
506 using namespace loan;
507 Account const lender{"lender"};
508 Account const issuer{"issuer"};
509 Account const borrower{"borrower"};
510 auto const iou = issuer["IOU"];
511
512 Env env(*this);
513 env.fund(XRP(1'000), lender, issuer, borrower);
514 env(trust(lender, iou(10'000'000)));
515 env(pay(issuer, lender, iou(5'000'000)));
516 BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
517
518 auto const loanSetFee = Fee(env.current()->fees().base * 2);
519 STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
520
521 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
522 Sig(sfCounterpartySignature, lender),
523 loanSetFee);
524 env.close();
525
526 auto const loanKeylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
527
528 using Tp = NetClock::time_point;
529 using D = NetClock::duration;
530
531 // Get past the grace period so the loan is defaultable.
532 if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
533 {
534 env.close(Tp{D{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
535 }
536
537 // The default moves First-Loss Capital off the broker pseudo-account,
538 // so that is the line to freeze.
539 auto const brokerSle = env.le(brokerInfo.brokerKeylet());
540 if (!BEAST_EXPECT(brokerSle))
541 return;
542 Account const brokerPseudo{"brokerPseudo", brokerSle->at(sfAccount)};
543
544 env(trust(issuer, brokerPseudo["IOU"](0), tfSetFreeze | tfSetDeepFreeze));
545 env.close();
546
547 // Pre-fixCleanup3_4_0, the invariant blocks the default.
548 env.disableFeature(fixCleanup3_4_0);
549 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecINVARIANT_FAILED));
550 env.close();
551
552 // Per XLS-0066, a default must succeed despite the deep freeze.
553 env.enableFeature(fixCleanup3_4_0);
554 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
555 }
556
557 void
559 {
560 testcase << "LoanPay Broker Owner Missing Trustline (PoC)";
561 using namespace jtx;
562 using namespace loan;
563 Account const issuer("issuer");
564 Account const borrower("borrower");
565 Account const broker("broker");
566 auto const iou = issuer["IOU"];
567 Env env(*this, features);
568 env.fund(XRP(20'000), issuer, broker, borrower);
569 env.close();
570 // Set up trustlines and fund accounts
571 env(trust(broker, iou(20'000'000)));
572 env(trust(borrower, iou(20'000'000)));
573 env(pay(issuer, broker, iou(10'000'000)));
574 env(pay(issuer, borrower, iou(1'000)));
575 env.close();
576 // Create vault and broker
577 auto const brokerInfo = createVaultAndBroker(env, iou, broker);
578 // Create a loan first (this creates debt)
579 auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
580 env(set(borrower, brokerInfo.brokerID, 10'000),
581 Sig(sfCounterpartySignature, broker),
582 kLoanServiceFee(iou(100).value()),
583 kPaymentInterval(100),
584 Fee(XRP(100)));
585 env.close();
586 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
587 // We need coverAvailable >= (debtTotal * coverRateMinimum)
588 // Deposit enough cover to ensure the fee goes to broker owner
589 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
590 // at least 1,000 cover. Default cover is 1,000, so we add more to be
591 // safe.
592 auto const additionalCover = iou(50'000).value();
593 env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover}));
594 env.close();
595 // Verify broker owner has a trustline
596 auto const brokerTrustline = keylet::trustLine(broker, iou);
597 BEAST_EXPECT(env.le(brokerTrustline) != nullptr);
598 // Broker owner deletes their trustline
599 // First, pay any positive balance to issuer to zero it out
600 auto const brokerBalance = env.balance(broker, iou);
601 env(pay(broker, issuer, brokerBalance));
602 env.close();
603 // Remove the trustline by setting limit to 0
604 env(trust(broker, iou(0)));
605 env.close();
606 // Verify trustline is deleted
607 BEAST_EXPECT(env.le(brokerTrustline) == nullptr);
608 // Now borrower tries to make a payment
609 // We should get a tesSUCCESS instead of a tecNO_LINE.
610 env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)), Ter(tesSUCCESS));
611 env.close();
612 // Verify trustline is still deleted
613 BEAST_EXPECT(env.le(brokerTrustline) == nullptr);
614 // Verify the service fee went to the broker pseudo-account
615 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
616 BEAST_EXPECT(brokerSle))
617 {
618 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
619 auto const balance = env.balance(pseudo, iou);
620 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
621 BEAST_EXPECTS(balance == iou(51'100), to_string(json::Value(balance)));
622 }
623 }
624
625 void
627 {
628 testcase << "LoanPay Broker Owner MPT unauthorized";
629 using namespace jtx;
630 using namespace loan;
631
632 Account const issuer("issuer");
633 Account const borrower("borrower");
634 Account const broker("broker");
635
636 Env env{*this, features};
637 env.fund(XRP(20'000), issuer, broker, borrower);
638 env.close();
639
640 MPTTester mptt{env, issuer, kMptInitNoFund};
641 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
642
643 PrettyAsset const mpt{mptt.issuanceID()};
644
645 // Authorize broker and borrower
646 mptt.authorize({.account = broker});
647 mptt.authorize({.account = borrower});
648
649 env.close();
650
651 // Fund accounts
652 env(pay(issuer, broker, mpt(10'000'000)));
653 env(pay(issuer, borrower, mpt(1'000)));
654 env.close();
655
656 // Create vault and broker
657 auto const brokerInfo = createVaultAndBroker(env, mpt, broker);
658 // Create a loan first (this creates debt)
659 auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
660 env(set(borrower, brokerInfo.brokerID, 10'000),
661 Sig(sfCounterpartySignature, broker),
662 kLoanServiceFee(mpt(100).value()),
663 kPaymentInterval(100),
664 Fee(XRP(100)));
665 env.close();
666 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
667 // We need coverAvailable >= (debtTotal * coverRateMinimum)
668 // Deposit enough cover to ensure the fee goes to broker owner
669 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
670 // at least 1,000 cover. Default cover is 1,000, so we add more to be
671 // safe.
672 auto const additionalCover = mpt(50'000).value();
673 env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover}));
674 env.close();
675 // Verify broker owner is authorized
676 auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker);
677 BEAST_EXPECT(env.le(brokerMpt) != nullptr);
678 // Broker owner unauthorizes.
679 // First, pay any positive balance to issuer to zero it out
680 auto const brokerBalance = env.balance(broker, mpt);
681 env(pay(broker, issuer, brokerBalance));
682 env.close();
683 // Then, unauthorize the MPT.
684 mptt.authorize({.account = broker, .flags = tfMPTUnauthorize});
685 env.close();
686 // Verify the MPT is unauthorized.
687 BEAST_EXPECT(env.le(brokerMpt) == nullptr);
688 // Now borrower tries to make a payment
689 // We should get a tesSUCCESS instead of a tecNO_AUTH.
690 env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS));
691 env.close();
692 // Verify the MPT is still unauthorized.
693 BEAST_EXPECT(env.le(brokerMpt) == nullptr);
694 // Verify the service fee went to the broker pseudo-account
695 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
696 BEAST_EXPECT(brokerSle))
697 {
698 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
699 auto const balance = env.balance(pseudo, mpt);
700 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
701 BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance)));
702 }
703 }
704
705 void
707 {
708 testcase << "LoanPay Broker Owner without permissioned domain of the MPT";
709 using namespace jtx;
710 using namespace loan;
711
712 Account const issuer("issuer");
713 Account const borrower("borrower");
714 Account const broker("broker");
715
716 Env env{*this, features};
717 env.fund(XRP(20'000), issuer, broker, borrower);
718 env.close();
719
720 auto credType = "credential1";
721
722 pdomain::Credentials const credentials1 = {{.issuer = issuer, .credType = credType}};
723 env(pdomain::setTx(issuer, credentials1));
724 env.close();
725
726 auto domainID = pdomain::getNewDomain(env.meta());
727
728 env(credentials::create(broker, issuer, credType));
729 env(credentials::accept(broker, issuer, credType));
730 env.close();
731
732 env(credentials::create(borrower, issuer, credType));
733 env(credentials::accept(borrower, issuer, credType));
734 env.close();
735
736 MPTTester mptt{env, issuer, kMptInitNoFund};
737 mptt.create({
738 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
739 .domainID = domainID,
740 });
741
742 PrettyAsset const mpt{mptt.issuanceID()};
743
744 // Authorize broker and borrower
745 mptt.authorize({.account = broker});
746 mptt.authorize({.account = borrower});
747
748 env.close();
749
750 // Fund accounts
751 env(pay(issuer, broker, mpt(10'000'000)));
752 env(pay(issuer, borrower, mpt(1'000)));
753 env.close();
754
755 // Create vault and broker
756 auto const brokerInfo = createVaultAndBroker(env, mpt, broker);
757 // Create a loan first (this creates debt)
758 auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
759 env(set(borrower, brokerInfo.brokerID, 10'000),
760 Sig(sfCounterpartySignature, broker),
761 kLoanServiceFee(mpt(100).value()),
762 kPaymentInterval(100),
763 Fee(XRP(100)));
764 env.close();
765 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
766 // We need coverAvailable >= (debtTotal * coverRateMinimum)
767 // Deposit enough cover to ensure the fee goes to broker owner
768 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
769 // at least 1,000 cover. Default cover is 1,000, so we add more to be
770 // safe.
771 auto const additionalCover = mpt(50'000).value();
772 env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover}));
773 env.close();
774 // Verify broker owner is authorized
775 auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker);
776 BEAST_EXPECT(env.le(brokerMpt) != nullptr);
777 // Remove the credentials for the Broker owner.
778 // First, pay any positive balance to issuer to zero it out
779 auto const brokerBalance = env.balance(broker, mpt);
780 env(pay(broker, issuer, brokerBalance));
781 env.close();
782
783 env(credentials::deleteCred(broker, broker, issuer, credType));
784 env.close();
785
786 // Make sure the broker is not authorized to hold the MPT after we
787 // deleted the credentials
788 env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH));
789
790 // Now borrower tries to make a payment
791 // We should get a tesSUCCESS instead of a tecNO_AUTH.
792 env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS));
793 env.close();
794 // Verify broker is still not authorized
795 env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH));
796 // Verify the service fee went to the broker pseudo-account
797 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
798 BEAST_EXPECT(brokerSle))
799 {
800 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
801 auto const balance = env.balance(pseudo, mpt);
802 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
803 BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance)));
804 }
805 }
806
807 void
809 {
810 testcase << "LoanSet Broker Owner without permissioned domain of the MPT";
811 using namespace jtx;
812 using namespace loan;
813
814 Account const issuer("issuer");
815 Account const borrower("borrower");
816 Account const broker("broker");
817
818 Env env{*this, features};
819 env.fund(XRP(20'000), issuer, broker, borrower);
820 env.close();
821
822 auto credType = "credential1";
823
824 pdomain::Credentials const credentials1{{.issuer = issuer, .credType = credType}};
825 env(pdomain::setTx(issuer, credentials1));
826 env.close();
827
828 auto domainID = pdomain::getNewDomain(env.meta());
829
830 // Add credentials for the broker and borrower
831 env(credentials::create(broker, issuer, credType));
832 env(credentials::accept(broker, issuer, credType));
833 env.close();
834
835 env(credentials::create(borrower, issuer, credType));
836 env(credentials::accept(borrower, issuer, credType));
837 env.close();
838
839 MPTTester mptt{env, issuer, kMptInitNoFund};
840 mptt.create({
841 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
842 .domainID = domainID,
843 });
844
845 PrettyAsset const mpt{mptt.issuanceID()};
846
847 // Authorize broker and borrower
848 mptt.authorize({.account = broker});
849 mptt.authorize({.account = borrower});
850 env.close();
851
852 // Fund accounts
853 env(pay(issuer, broker, mpt(10'000'000)));
854 env(pay(issuer, borrower, mpt(1'000)));
855 env.close();
856
857 // Create vault and broker
858 auto const brokerInfo = createVaultAndBroker(env, mpt, broker);
859
860 // Remove the credentials for the Broker owner.
861 // Clear the balance first.
862 auto const brokerBalance = env.balance(broker, mpt);
863 env(pay(broker, issuer, brokerBalance));
864 env.close();
865 // Delete the credentials
866 env(credentials::deleteCred(broker, broker, issuer, credType));
867 env.close();
868
869 // Create a loan, this should fail for tecNO_AUTH
870 env(set(borrower, brokerInfo.brokerID, 10'000),
871 Sig(sfCounterpartySignature, broker),
872 kLoanServiceFee(mpt(100).value()),
873 kPaymentInterval(100),
874 Fee(XRP(100)),
875 Ter(tecNO_AUTH));
876 env.close();
877 }
878
879 void
887
888 // Tests run under each entry in amendmentCombinations().
889 void
899
900public:
901 void
902 run() override
903 {
905 for (auto const& features : jtx::amendmentCombinations(
906 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
907 runAmendmentSensitive(features);
908 }
909};
910
911BEAST_DEFINE_TESTSUITE(LoanCoverFreezeAuth, tx, xrpl);
912
913} // 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
void negate()
Definition STAmount.h:586
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
void testLoanSetBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features)
void testWithdrawReflectsUnrealizedLoss(FeatureBitset features)
void runAmendmentSensitive(FeatureBitset features)
void testLoanPayBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features)
void testLoanPayBrokerOwnerUnauthorizedMPT(FeatureBitset features)
void testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features)
void testSequentialFLCDepletion(FeatureBitset features)
Shared base for the Loan*_test family under src/test/app/lending/.
FeatureBitset const all_
void advancePastDueDate(jtx::Env &env, Keylet const &loanKeylet)
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
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 disableFeature(UInt256 const feature)
Definition Env.cpp:717
void enableFeature(UInt256 const feature)
Definition Env.cpp:709
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:538
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
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:512
MPTID const & issuanceID() const
Definition mpt.h:768
void create(MPTCreate const &arg=MPTCreate{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:343
void authorize(MPTAuthorize const &arg=MPTAuthorize{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:455
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Keylet computation functions.
Definition Indexes.h:40
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
json::Value deleteCred(jtx::Account const &acc, jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:40
json::Value accept(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:29
json::Value create(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:16
json::Value coverDeposit(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
std::vector< Credential > Credentials
UInt256 getNewDomain(std::shared_ptr< STObject const > const &meta)
json::Value setTx(AccountID const &account, Credentials const &credentials, std::optional< UInt256 > domain)
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 trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
static MPTInit const kMptInitNoFund
Definition mpt.h:201
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
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
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecLOCKED
Definition TER.h:366
@ tecNO_AUTH
Definition TER.h:308
@ tecINVARIANT_FAILED
Definition TER.h:321
@ tesSUCCESS
Definition TER.h:250
static json::Value withdraw(WithdrawArgs const &args)
Definition vault.cpp:100
static json::Value deposit(DepositArgs const &args)
Definition vault.cpp:89