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/mpt.h>
9#include <test/jtx/pay.h>
10#include <test/jtx/permissioned_domains.h>
11#include <test/jtx/ter.h>
12#include <test/jtx/trust.h>
13#include <test/jtx/vault.h>
14
15#include <xrpl/basics/Number.h>
16#include <xrpl/beast/unit_test/suite.h>
17#include <xrpl/json/json_value.h>
18#include <xrpl/json/to_string.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/SField.h>
24#include <xrpl/protocol/SeqProxy.h>
25#include <xrpl/protocol/TER.h>
26#include <xrpl/protocol/TxFlags.h>
27#include <xrpl/protocol/Units.h>
28
29#include <cstdint>
30
31namespace xrpl::test {
32
34{
35private:
36 void
38 {
39 testcase << "First-Loss Capital Depletion on Sequential Defaults";
40
41 using namespace jtx;
42 using namespace loan;
43 using namespace loan_broker;
44
45 Env env{*this, features};
46
47 Account const issuer{"issuer"};
48 Account const lender{"lender"};
49 Account const borrowerA{"borrowerA"};
50 Account const borrowerB{"borrowerB"};
51
52 env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB);
53 env.close();
54
55 PrettyAsset const asset = xrpIssue();
56 auto const vaultDepositAmount =
57 asset(200'000); // Enough for 2 x 50k loans plus interest/fees
58
59 auto const brokerInfo = createVaultAndBroker(
60 env,
61 asset,
62 lender,
63 {
64 .vaultDeposit = vaultDepositAmount.value(),
65 .debtMax = 0,
66 .coverRateMin = TenthBips32(20000), // 20%
67 .coverDeposit = 21'000,
68 .managementFeeRate = TenthBips16(100), // 0.1%
69 .coverRateLiquidation = TenthBips32(100000),
70 });
71 auto const brokerKeylet = brokerInfo.brokerKeylet();
72
73 // Create two identical loans: each 50,000 XRP principal (scaled down to
74 // avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal
75 // + interest) Formula will calculate cover as: 100% × (20% × 100,000) =
76 // 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first
77 // default
78 auto const principalAmount = Number(50'000);
79 auto const loanPaymentInterval = 2592000; // 30 days
80 auto const loanGracePeriod = 604800; // 7 days
81
82 // Create Loan A
83 auto loanATx = env.jt(
84 set(borrowerA, brokerKeylet.key, principalAmount),
85 Sig(sfCounterpartySignature, lender),
86 kInterestRate(TenthBips32(500)), // 5%
87 kPaymentTotal(12),
88 loan::kPaymentInterval(loanPaymentInterval),
89 loan::kGracePeriod(loanGracePeriod),
90 Fee(XRP(10))); // Sufficient fee for multi-sig transaction
91 env(loanATx);
92 env.close();
93
94 auto const loanAKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
95
96 // Create Loan B
97 auto loanBTx = env.jt(
98 set(borrowerB, brokerKeylet.key, principalAmount),
99 Sig(sfCounterpartySignature, lender),
100 kInterestRate(TenthBips32(500)), // 5%
101 kPaymentTotal(12),
102 loan::kPaymentInterval(loanPaymentInterval),
103 loan::kGracePeriod(loanGracePeriod),
104 Fee(XRP(10))); // Sufficient fee for multi-sig transaction
105 env(loanBTx);
106 env.close();
107
108 auto const loanBKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(2));
109
110 auto loanASle = env.le(loanAKeylet);
111 if (!BEAST_EXPECT(loanASle))
112 return;
113
114 // Advance time past grace period for both loans to be defaultable
115 auto const loanANextDue = loanASle->at(sfNextPaymentDueDate);
116 auto const loanAGrace = loanASle->at(sfGracePeriod);
117 env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60});
118
119 env(manage(lender, loanAKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
120 env.close();
121
122 // Verify Loan A is defaulted
123 loanASle = env.le(loanAKeylet);
124 if (!BEAST_EXPECT(loanASle))
125 return;
126 BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault));
127 BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0);
128
129 // Check broker state after first default (from committed ledger)
130 auto brokerSle = env.le(brokerKeylet);
131 if (!BEAST_EXPECT(brokerSle))
132 return;
133 auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal);
134 auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable);
135
136 // DebtTotal should have decreased by Loan A's debt
137 BEAST_EXPECT(afterFirstDebtTotal == 50'134);
138
139 // CoverAvailable should have decreased significantly
140 BEAST_EXPECT(afterFirstCoverAvailable == 946);
141
142 env(manage(lender, loanBKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
143
144 brokerSle = env.le(brokerKeylet);
145 if (!BEAST_EXPECT(brokerSle))
146 return;
147 auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal);
148 auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable);
149
150 BEAST_EXPECT(afterSecondDebtTotal == 0);
151
152 BEAST_EXPECT(afterSecondCoverAvailable == 0);
153 }
154
155 // Tests that vault withdrawals work correctly when the vault has unrealized
156 // loss from an impaired loan, ensuring the invariant check properly
157 // accounts for the loss.
158 void
160 {
161 using namespace jtx;
162 using namespace loan;
163 using namespace std::chrono_literals;
164
165 testcase("Vault withdraw reflects sfLossUnrealized");
166
167 // Test constants
168 static constexpr std::int64_t kInitialFunding = 1'000'000;
169 static constexpr std::int64_t kLenderInitialIou = 5'000'000;
170 static constexpr std::int64_t kDepositorInitialIou = 1'000'000;
171 static constexpr std::int64_t kBorrowerInitialIou = 100'000;
172 static constexpr std::int64_t kDepositAmount = 5'000;
173 static constexpr std::int64_t kPrincipalAmount = 99;
174 static constexpr std::uint64_t kExpectedSharesPerDepositor = 5'000'000'000;
175 static constexpr std::uint32_t kLocalPaymentInterval = 600;
176 static constexpr std::uint32_t kLocalPaymentTotal = 2;
177
178 Env env{*this, features};
179
180 // Setup accounts
181 Account const issuer{"issuer"};
182 Account const lender{"lender"};
183 Account const depositorA{"lpA"};
184 Account const depositorB{"lpB"};
185 Account const borrower{"borrowerA"};
186
187 env.fund(XRP(kInitialFunding), issuer, lender, depositorA, depositorB, borrower);
188 env.close();
189
190 // Setup trust lines
191 PrettyAsset const iouAsset = issuer[iouCurrency_];
192 env(trust(lender, iouAsset(10'000'000)));
193 env(trust(depositorA, iouAsset(10'000'000)));
194 env(trust(depositorB, iouAsset(10'000'000)));
195 env(trust(borrower, iouAsset(10'000'000)));
196 env.close();
197
198 // Fund accounts with IOUs
199 env(pay(issuer, lender, iouAsset(kLenderInitialIou)));
200 env(pay(issuer, depositorA, iouAsset(kDepositorInitialIou)));
201 env(pay(issuer, depositorB, iouAsset(kDepositorInitialIou)));
202 env(pay(issuer, borrower, iouAsset(kBorrowerInitialIou)));
203 env.close();
204
205 // Create vault and broker, then add deposits from two depositors
206 auto const broker = createVaultAndBroker(env, iouAsset, lender);
207 Vault v{env};
208
209 env(v.deposit({
210 .depositor = depositorA,
211 .id = broker.vaultKeylet().key,
212 .amount = iouAsset(kDepositAmount),
213 }),
214 Ter(tesSUCCESS));
215 env(v.deposit({
216 .depositor = depositorB,
217 .id = broker.vaultKeylet().key,
218 .amount = iouAsset(kDepositAmount),
219 }),
220 Ter(tesSUCCESS));
221 env.close();
222
223 // Create a loan
224 auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
225 if (!BEAST_EXPECT(sleBroker))
226 return;
227
228 auto const loanKeylet =
229 keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
230
231 env(set(borrower, broker.brokerID, kPrincipalAmount),
232 Sig(sfCounterpartySignature, lender),
233 kPaymentTotal(kLocalPaymentTotal),
234 kPaymentInterval(kLocalPaymentInterval),
235 Fee(env.current()->fees().base * 2),
236 Ter(tesSUCCESS));
237 env.close();
238
239 // Impair the loan to create unrealized loss
240 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
241 env.close();
242
243 // Verify unrealized loss is recorded in the vault
244 auto const vaultAfterImpair = env.le(broker.vaultKeylet());
245 if (!BEAST_EXPECT(vaultAfterImpair))
246 return;
247
248 BEAST_EXPECT(
249 vaultAfterImpair->at(sfLossUnrealized) == broker.asset(kPrincipalAmount).value());
250
251 // Helper to get share balance for a depositor
252 auto const shareAsset = vaultAfterImpair->at(sfShareMPTID);
253 auto const getShareBalance = [&](Account const& depositor) -> std::uint64_t {
254 auto const token = env.le(keylet::mptoken(shareAsset, depositor.id()));
255 return token ? token->getFieldU64(sfMPTAmount) : 0;
256 };
257
258 // Verify both depositors have equal shares
259 auto const sharesLpA = getShareBalance(depositorA);
260 auto const sharesLpB = getShareBalance(depositorB);
261 BEAST_EXPECT(sharesLpA == kExpectedSharesPerDepositor);
262 BEAST_EXPECT(sharesLpB == kExpectedSharesPerDepositor);
263 BEAST_EXPECT(sharesLpA == sharesLpB);
264
265 // Helper to attempt withdrawal
266 auto const attemptWithdrawShares = [&](Account const& depositor,
267 std::uint64_t shareAmount,
268 TER expected) {
269 STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)};
270 env(v.withdraw(
271 {.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}),
272 Ter(expected));
273 env.close();
274 };
275
276 // Regression test: Both depositors should successfully withdraw despite
277 // unrealized loss. Previously failed with invariant violation:
278 // "withdrawal must change vault and destination balance by equal
279 // amount". This was caused by sharesToAssetsWithdraw rounding down,
280 // creating a mismatch where vaultDeltaAssets * -1 != destinationDelta
281 // when unrealized loss exists.
282 attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS);
283 attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
284 }
285
286 void
288 {
289 testcase << "Service Fee On Broker Deep Freeze";
290 using namespace jtx;
291 using namespace loan;
292 Account const issuer("issuer");
293 Account const borrower("borrower");
294 Account const broker("broker");
295 auto const iou = issuer["IOU"];
296
297 for (bool const deepFreeze : {true, false})
298 {
299 Env env(*this);
300
301 auto getCoverBalance = [&](BrokerInfo const& brokerInfo, auto const& accountField) {
302 if (auto const le = env.le(keylet::loanBroker(brokerInfo.brokerID));
303 BEAST_EXPECT(le))
304 {
305 auto const account = le->at(accountField);
306 if (auto const sleLine = env.le(keylet::trustLine(account, iou));
307 BEAST_EXPECT(sleLine))
308 {
309 STAmount balance = sleLine->at(sfBalance);
310 if (account > issuer.id())
311 balance.negate();
312 return balance;
313 }
314 }
315 return STAmount{iou};
316 };
317
318 env.fund(XRP(20'000), issuer, broker, borrower);
319 env.close();
320
321 env(trust(broker, iou(20'000'000)));
322 env(pay(issuer, broker, iou(10'000'000)));
323 env.close();
324
325 auto const brokerInfo = createVaultAndBroker(env, iou, broker);
326
327 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
328
329 auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
330
331 env(set(borrower, brokerInfo.brokerID, 10'000),
332 Sig(sfCounterpartySignature, broker),
333 kLoanServiceFee(iou(100).value()),
334 kPaymentInterval(100),
335 Fee(XRP(100)));
336 env.close();
337
338 env(trust(borrower, iou(20'000'000)));
339 // The borrower increases their limit and acquires some IOU so
340 // they can pay interest
341 env(pay(issuer, borrower, iou(500)));
342 env.close();
343
344 if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le))
345 {
346 if (deepFreeze)
347 {
348 env(trust(issuer, broker["IOU"](0), tfSetFreeze | tfSetDeepFreeze));
349 env.close();
350 }
351
352 env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)));
353 env.close();
354
355 if (deepFreeze)
356 {
357 // The fee goes to the broker pseudo-account
358 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'100));
359 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'000));
360 }
361 else
362 {
363 // The fee goes to the broker account
364 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'100));
365 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
366 }
367 }
368 };
369 }
370
371 void
373 {
374 testcase << "LoanPay Broker Owner Missing Trustline (PoC)";
375 using namespace jtx;
376 using namespace loan;
377 Account const issuer("issuer");
378 Account const borrower("borrower");
379 Account const broker("broker");
380 auto const iou = issuer["IOU"];
381 Env env(*this, features);
382 env.fund(XRP(20'000), issuer, broker, borrower);
383 env.close();
384 // Set up trustlines and fund accounts
385 env(trust(broker, iou(20'000'000)));
386 env(trust(borrower, iou(20'000'000)));
387 env(pay(issuer, broker, iou(10'000'000)));
388 env(pay(issuer, borrower, iou(1'000)));
389 env.close();
390 // Create vault and broker
391 auto const brokerInfo = createVaultAndBroker(env, iou, broker);
392 // Create a loan first (this creates debt)
393 auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
394 env(set(borrower, brokerInfo.brokerID, 10'000),
395 Sig(sfCounterpartySignature, broker),
396 kLoanServiceFee(iou(100).value()),
397 kPaymentInterval(100),
398 Fee(XRP(100)));
399 env.close();
400 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
401 // We need coverAvailable >= (debtTotal * coverRateMinimum)
402 // Deposit enough cover to ensure the fee goes to broker owner
403 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
404 // at least 1,000 cover. Default cover is 1,000, so we add more to be
405 // safe.
406 auto const additionalCover = iou(50'000).value();
407 env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover}));
408 env.close();
409 // Verify broker owner has a trustline
410 auto const brokerTrustline = keylet::trustLine(broker, iou);
411 BEAST_EXPECT(env.le(brokerTrustline) != nullptr);
412 // Broker owner deletes their trustline
413 // First, pay any positive balance to issuer to zero it out
414 auto const brokerBalance = env.balance(broker, iou);
415 env(pay(broker, issuer, brokerBalance));
416 env.close();
417 // Remove the trustline by setting limit to 0
418 env(trust(broker, iou(0)));
419 env.close();
420 // Verify trustline is deleted
421 BEAST_EXPECT(env.le(brokerTrustline) == nullptr);
422 // Now borrower tries to make a payment
423 // We should get a tesSUCCESS instead of a tecNO_LINE.
424 env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)), Ter(tesSUCCESS));
425 env.close();
426 // Verify trustline is still deleted
427 BEAST_EXPECT(env.le(brokerTrustline) == nullptr);
428 // Verify the service fee went to the broker pseudo-account
429 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
430 BEAST_EXPECT(brokerSle))
431 {
432 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
433 auto const balance = env.balance(pseudo, iou);
434 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
435 BEAST_EXPECTS(balance == iou(51'100), to_string(json::Value(balance)));
436 }
437 }
438
439 void
441 {
442 testcase << "LoanPay Broker Owner MPT unauthorized";
443 using namespace jtx;
444 using namespace loan;
445
446 Account const issuer("issuer");
447 Account const borrower("borrower");
448 Account const broker("broker");
449
450 Env env{*this, features};
451 env.fund(XRP(20'000), issuer, broker, borrower);
452 env.close();
453
454 MPTTester mptt{env, issuer, kMptInitNoFund};
455 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
456
457 PrettyAsset const mpt{mptt.issuanceID()};
458
459 // Authorize broker and borrower
460 mptt.authorize({.account = broker});
461 mptt.authorize({.account = borrower});
462
463 env.close();
464
465 // Fund accounts
466 env(pay(issuer, broker, mpt(10'000'000)));
467 env(pay(issuer, borrower, mpt(1'000)));
468 env.close();
469
470 // Create vault and broker
471 auto const brokerInfo = createVaultAndBroker(env, mpt, broker);
472 // Create a loan first (this creates debt)
473 auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
474 env(set(borrower, brokerInfo.brokerID, 10'000),
475 Sig(sfCounterpartySignature, broker),
476 kLoanServiceFee(mpt(100).value()),
477 kPaymentInterval(100),
478 Fee(XRP(100)));
479 env.close();
480 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
481 // We need coverAvailable >= (debtTotal * coverRateMinimum)
482 // Deposit enough cover to ensure the fee goes to broker owner
483 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
484 // at least 1,000 cover. Default cover is 1,000, so we add more to be
485 // safe.
486 auto const additionalCover = mpt(50'000).value();
487 env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover}));
488 env.close();
489 // Verify broker owner is authorized
490 auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker);
491 BEAST_EXPECT(env.le(brokerMpt) != nullptr);
492 // Broker owner unauthorizes.
493 // First, pay any positive balance to issuer to zero it out
494 auto const brokerBalance = env.balance(broker, mpt);
495 env(pay(broker, issuer, brokerBalance));
496 env.close();
497 // Then, unauthorize the MPT.
498 mptt.authorize({.account = broker, .flags = tfMPTUnauthorize});
499 env.close();
500 // Verify the MPT is unauthorized.
501 BEAST_EXPECT(env.le(brokerMpt) == nullptr);
502 // Now borrower tries to make a payment
503 // We should get a tesSUCCESS instead of a tecNO_AUTH.
504 env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS));
505 env.close();
506 // Verify the MPT is still unauthorized.
507 BEAST_EXPECT(env.le(brokerMpt) == nullptr);
508 // Verify the service fee went to the broker pseudo-account
509 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
510 BEAST_EXPECT(brokerSle))
511 {
512 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
513 auto const balance = env.balance(pseudo, mpt);
514 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
515 BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance)));
516 }
517 }
518
519 void
521 {
522 testcase << "LoanPay Broker Owner without permissioned domain of the MPT";
523 using namespace jtx;
524 using namespace loan;
525
526 Account const issuer("issuer");
527 Account const borrower("borrower");
528 Account const broker("broker");
529
530 Env env{*this, features};
531 env.fund(XRP(20'000), issuer, broker, borrower);
532 env.close();
533
534 auto credType = "credential1";
535
536 pdomain::Credentials const credentials1 = {{.issuer = issuer, .credType = credType}};
537 env(pdomain::setTx(issuer, credentials1));
538 env.close();
539
540 auto domainID = pdomain::getNewDomain(env.meta());
541
542 env(credentials::create(broker, issuer, credType));
543 env(credentials::accept(broker, issuer, credType));
544 env.close();
545
546 env(credentials::create(borrower, issuer, credType));
547 env(credentials::accept(borrower, issuer, credType));
548 env.close();
549
550 MPTTester mptt{env, issuer, kMptInitNoFund};
551 mptt.create({
552 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
553 .domainID = domainID,
554 });
555
556 PrettyAsset const mpt{mptt.issuanceID()};
557
558 // Authorize broker and borrower
559 mptt.authorize({.account = broker});
560 mptt.authorize({.account = borrower});
561
562 env.close();
563
564 // Fund accounts
565 env(pay(issuer, broker, mpt(10'000'000)));
566 env(pay(issuer, borrower, mpt(1'000)));
567 env.close();
568
569 // Create vault and broker
570 auto const brokerInfo = createVaultAndBroker(env, mpt, broker);
571 // Create a loan first (this creates debt)
572 auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
573 env(set(borrower, brokerInfo.brokerID, 10'000),
574 Sig(sfCounterpartySignature, broker),
575 kLoanServiceFee(mpt(100).value()),
576 kPaymentInterval(100),
577 Fee(XRP(100)));
578 env.close();
579 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
580 // We need coverAvailable >= (debtTotal * coverRateMinimum)
581 // Deposit enough cover to ensure the fee goes to broker owner
582 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
583 // at least 1,000 cover. Default cover is 1,000, so we add more to be
584 // safe.
585 auto const additionalCover = mpt(50'000).value();
586 env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover}));
587 env.close();
588 // Verify broker owner is authorized
589 auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker);
590 BEAST_EXPECT(env.le(brokerMpt) != nullptr);
591 // Remove the credentials for the Broker owner.
592 // First, pay any positive balance to issuer to zero it out
593 auto const brokerBalance = env.balance(broker, mpt);
594 env(pay(broker, issuer, brokerBalance));
595 env.close();
596
597 env(credentials::deleteCred(broker, broker, issuer, credType));
598 env.close();
599
600 // Make sure the broker is not authorized to hold the MPT after we
601 // deleted the credentials
602 env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH));
603
604 // Now borrower tries to make a payment
605 // We should get a tesSUCCESS instead of a tecNO_AUTH.
606 env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS));
607 env.close();
608 // Verify broker is still not authorized
609 env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH));
610 // Verify the service fee went to the broker pseudo-account
611 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
612 BEAST_EXPECT(brokerSle))
613 {
614 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
615 auto const balance = env.balance(pseudo, mpt);
616 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
617 BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance)));
618 }
619 }
620
621 void
623 {
624 testcase << "LoanSet Broker Owner without permissioned domain of the MPT";
625 using namespace jtx;
626 using namespace loan;
627
628 Account const issuer("issuer");
629 Account const borrower("borrower");
630 Account const broker("broker");
631
632 Env env{*this, features};
633 env.fund(XRP(20'000), issuer, broker, borrower);
634 env.close();
635
636 auto credType = "credential1";
637
638 pdomain::Credentials const credentials1{{.issuer = issuer, .credType = credType}};
639 env(pdomain::setTx(issuer, credentials1));
640 env.close();
641
642 auto domainID = pdomain::getNewDomain(env.meta());
643
644 // Add credentials for the broker and borrower
645 env(credentials::create(broker, issuer, credType));
646 env(credentials::accept(broker, issuer, credType));
647 env.close();
648
649 env(credentials::create(borrower, issuer, credType));
650 env(credentials::accept(borrower, issuer, credType));
651 env.close();
652
653 MPTTester mptt{env, issuer, kMptInitNoFund};
654 mptt.create({
655 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
656 .domainID = domainID,
657 });
658
659 PrettyAsset const mpt{mptt.issuanceID()};
660
661 // Authorize broker and borrower
662 mptt.authorize({.account = broker});
663 mptt.authorize({.account = borrower});
664 env.close();
665
666 // Fund accounts
667 env(pay(issuer, broker, mpt(10'000'000)));
668 env(pay(issuer, borrower, mpt(1'000)));
669 env.close();
670
671 // Create vault and broker
672 auto const brokerInfo = createVaultAndBroker(env, mpt, broker);
673
674 // Remove the credentials for the Broker owner.
675 // Clear the balance first.
676 auto const brokerBalance = env.balance(broker, mpt);
677 env(pay(broker, issuer, brokerBalance));
678 env.close();
679 // Delete the credentials
680 env(credentials::deleteCred(broker, broker, issuer, credType));
681 env.close();
682
683 // Create a loan, this should fail for tecNO_AUTH
684 env(set(borrower, brokerInfo.brokerID, 10'000),
685 Sig(sfCounterpartySignature, broker),
686 kLoanServiceFee(mpt(100).value()),
687 kPaymentInterval(100),
688 Fee(XRP(100)),
689 Ter(tecNO_AUTH));
690 env.close();
691 }
692
693 void
698
699 // Tests run under each entry in amendmentCombinations().
700 void
710
711public:
712 void
713 run() override
714 {
716 for (auto const& features : jtx::amendmentCombinations(
717 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
718 runAmendmentSensitive(features);
719 }
720};
721
722BEAST_DEFINE_TESTSUITE(LoanCoverFreezeAuth, tx, xrpl);
723
724} // namespace xrpl::test
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
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)
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
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:721
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
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:447
void create(MPTCreate const &arg=MPTCreate{})
Definition mpt.cpp:241
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:353
MPTID const & issuanceID() const
Definition mpt.h:641
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Keylet computation functions.
Definition Indexes.h:40
Keylet loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:573
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:543
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:567
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
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
json::Value setTx(AccountID const &account, Credentials const &credentials, std::optional< uint256 > domain)
uint256 getNewDomain(std::shared_ptr< STObject const > const &meta)
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
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
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
TERSubset< CanCvtToTER > TER
Definition TER.h:647
@ tecNO_AUTH
Definition TER.h:303
@ tesSUCCESS
Definition TER.h:245
static json::Value withdraw(WithdrawArgs const &args)
Definition vault.cpp:72
static json::Value deposit(DepositArgs const &args)
Definition vault.cpp:61