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>
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>
41 testcase <<
"First-Loss Capital Depletion on Sequential Defaults";
47 Env env{*
this, features};
51 Account const borrowerA{
"borrowerA"};
52 Account const borrowerB{
"borrowerB"};
54 env.
fund(
XRP(1'000'000), issuer, lender, borrowerA, borrowerB);
58 auto const vaultDepositAmount =
66 .vaultDeposit = vaultDepositAmount.value(),
69 .coverDeposit = 21'000,
80 auto const principalAmount =
Number(50'000);
81 auto const loanPaymentInterval = 2592000;
82 auto const loanGracePeriod = 604800;
85 auto loanATx = env.
jt(
86 set(borrowerA, brokerKeylet.key, principalAmount),
87 Sig(sfCounterpartySignature, lender),
99 auto loanBTx = env.
jt(
100 set(borrowerB, brokerKeylet.key, principalAmount),
101 Sig(sfCounterpartySignature, lender),
112 auto loanASle = env.
le(loanAKeylet);
113 if (!BEAST_EXPECT(loanASle))
117 auto const loanANextDue = loanASle->at(sfNextPaymentDueDate);
118 auto const loanAGrace = loanASle->at(sfGracePeriod);
121 env(manage(lender, loanAKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
125 loanASle = env.
le(loanAKeylet);
126 if (!BEAST_EXPECT(loanASle))
128 BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault));
129 BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0);
132 auto brokerSle = env.
le(brokerKeylet);
133 if (!BEAST_EXPECT(brokerSle))
135 auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal);
136 auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable);
139 BEAST_EXPECT(afterFirstDebtTotal == 50'134);
142 BEAST_EXPECT(afterFirstCoverAvailable == 946);
144 env(manage(lender, loanBKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
146 brokerSle = env.
le(brokerKeylet);
147 if (!BEAST_EXPECT(brokerSle))
149 auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal);
150 auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable);
152 BEAST_EXPECT(afterSecondDebtTotal == 0);
154 BEAST_EXPECT(afterSecondCoverAvailable == 0);
164 using namespace loan;
165 using namespace std::chrono_literals;
167 testcase(
"Vault withdraw reflects sfLossUnrealized");
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;
176 static constexpr std::uint64_t kExpectedSharesPerDepositor = 5'000'000'000;
180 Env env{*
this, features};
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"};
189 env.
fund(
XRP(kInitialFunding), issuer, lender, depositorA, depositorB, borrower);
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)));
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)));
212 .depositor = depositorA,
213 .id = broker.vaultKeylet().key,
214 .amount = iouAsset(kDepositAmount),
218 .depositor = depositorB,
219 .id = broker.vaultKeylet().key,
220 .amount = iouAsset(kDepositAmount),
227 if (!BEAST_EXPECT(sleBroker))
230 auto const loanKeylet =
233 env(
set(borrower, broker.brokerID, kPrincipalAmount),
234 Sig(sfCounterpartySignature, lender),
235 kPaymentTotal(kLocalPaymentTotal),
236 kPaymentInterval(kLocalPaymentInterval),
245 env(manage(lender, loanKeylet.key, tfLoanImpair),
Ter(
tesSUCCESS));
249 auto const vaultAfterImpair = env.
le(broker.vaultKeylet());
250 if (!BEAST_EXPECT(vaultAfterImpair))
254 vaultAfterImpair->at(sfLossUnrealized) == broker.asset(kPrincipalAmount).value());
257 auto const shareAsset = vaultAfterImpair->at(sfShareMPTID);
260 return token ?
token->getFieldU64(sfMPTAmount) : 0;
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);
271 auto const attemptWithdrawShares = [&](
Account const& depositor,
276 {.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}),
287 attemptWithdrawShares(depositorA, sharesLpA,
tesSUCCESS);
288 attemptWithdrawShares(depositorB, sharesLpB,
tesSUCCESS);
294 testcase <<
"Service Fee On Broker Deep Freeze";
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"];
302 for (
bool const deepFreeze : {
true,
false})
306 auto getCoverBalance = [&](
BrokerInfo const& brokerInfo,
auto const& accountField) {
310 auto const account = le->at(accountField);
312 BEAST_EXPECT(sleLine))
314 STAmount balance = sleLine->at(sfBalance);
315 if (account > issuer.
id())
323 env.
fund(
XRP(20'000), issuer, broker, borrower);
326 env(
trust(broker, iou(20'000'000)));
327 env(
pay(issuer, broker, iou(10'000'000)));
332 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
337 Sig(sfCounterpartySignature, broker),
338 kLoanServiceFee(iou(100).value()),
339 kPaymentInterval(100),
343 env(
trust(borrower, iou(20'000'000)));
346 env(
pay(issuer, borrower, iou(500)));
353 env(
trust(issuer, broker[
"IOU"](0), tfSetFreeze | tfSetDeepFreeze));
363 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'100));
364 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'000));
369 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'100));
370 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
379 testcase(
"LoanManage: default bypasses asset freeze");
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"];
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)));
393 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
394 STAmount const debtMaximumRequest = brokerInfo.
asset(1'000).value();
396 env(
set(borrower, brokerInfo.
brokerID, debtMaximumRequest),
397 Sig(sfCounterpartySignature, lender),
407 if (
auto loan = env.
le(loanKeylet); BEAST_EXPECT(
loan))
409 env.
close(Tp{D{
loan->at(sfNextPaymentDueDate) +
loan->at(sfGracePeriod) + 1}});
413 env(
fset(issuer, asfGlobalFreeze));
423 env(manage(lender, loanKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
434 testcase(
"LoanManage: default bypasses MPT lock after impairment");
436 using namespace loan;
438 Account const issuer{
"issuer"};
439 Account const lender{
"lender"};
440 Account const borrower{
"borrower"};
443 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
449 .holders = {lender, borrower},
450 .flags = tfMPTCanTransfer | tfMPTCanLock});
452 env(
pay(issuer, lender, asset(10'000'000)));
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),
468 env(manage(lender, loanKeylet.key, tfLoanImpair));
472 mptt.set({.account = issuer, .flags = tfMPTLock});
476 env(
pay(borrower, loanKeylet.key, debtMaximumRequest),
Ter(
tecLOCKED));
481 if (
auto loan = env.
le(loanKeylet); BEAST_EXPECT(
loan))
483 env.
close(Tp{D{
loan->at(sfNextPaymentDueDate) +
loan->at(sfGracePeriod) + 1}});
494 env(manage(lender, loanKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
504 testcase(
"LoanManage: default bypasses asset deep freeze");
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"];
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)));
518 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
519 STAmount const debtMaximumRequest = brokerInfo.
asset(1'000).value();
521 env(
set(borrower, brokerInfo.
brokerID, debtMaximumRequest),
522 Sig(sfCounterpartySignature, lender),
532 if (
auto loan = env.
le(loanKeylet); BEAST_EXPECT(
loan))
534 env.
close(Tp{D{
loan->at(sfNextPaymentDueDate) +
loan->at(sfGracePeriod) + 1}});
540 if (!BEAST_EXPECT(brokerSle))
542 Account const brokerPseudo{
"brokerPseudo", brokerSle->at(sfAccount)};
544 env(
trust(issuer, brokerPseudo[
"IOU"](0), tfSetFreeze | tfSetDeepFreeze));
554 env(manage(lender, loanKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
560 testcase <<
"LoanPay Broker Owner Missing Trustline (PoC)";
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);
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)));
580 env(
set(borrower, brokerInfo.brokerID, 10'000),
581 Sig(sfCounterpartySignature, broker),
582 kLoanServiceFee(iou(100).value()),
583 kPaymentInterval(100),
592 auto const additionalCover = iou(50'000).value();
597 BEAST_EXPECT(env.
le(brokerTrustline) !=
nullptr);
600 auto const brokerBalance = env.
balance(broker, iou);
601 env(
pay(broker, issuer, brokerBalance));
604 env(
trust(broker, iou(0)));
607 BEAST_EXPECT(env.
le(brokerTrustline) ==
nullptr);
613 BEAST_EXPECT(env.
le(brokerTrustline) ==
nullptr);
616 BEAST_EXPECT(brokerSle))
618 Account const pseudo(
"pseudo-account", brokerSle->at(sfAccount));
619 auto const balance = env.
balance(pseudo, iou);
628 testcase <<
"LoanPay Broker Owner MPT unauthorized";
630 using namespace loan;
632 Account const issuer(
"issuer");
633 Account const borrower(
"borrower");
634 Account const broker(
"broker");
636 Env env{*
this, features};
637 env.
fund(
XRP(20'000), issuer, broker, borrower);
641 mptt.
create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
652 env(
pay(issuer, broker, mpt(10'000'000)));
653 env(
pay(issuer, borrower, mpt(1'000)));
660 env(
set(borrower, brokerInfo.brokerID, 10'000),
661 Sig(sfCounterpartySignature, broker),
662 kLoanServiceFee(mpt(100).value()),
663 kPaymentInterval(100),
672 auto const additionalCover = mpt(50'000).value();
677 BEAST_EXPECT(env.
le(brokerMpt) !=
nullptr);
680 auto const brokerBalance = env.
balance(broker, mpt);
681 env(
pay(broker, issuer, brokerBalance));
684 mptt.
authorize({.account = broker, .flags = tfMPTUnauthorize});
687 BEAST_EXPECT(env.
le(brokerMpt) ==
nullptr);
693 BEAST_EXPECT(env.
le(brokerMpt) ==
nullptr);
696 BEAST_EXPECT(brokerSle))
698 Account const pseudo(
"pseudo-account", brokerSle->at(sfAccount));
699 auto const balance = env.
balance(pseudo, mpt);
708 testcase <<
"LoanPay Broker Owner without permissioned domain of the MPT";
710 using namespace loan;
712 Account const issuer(
"issuer");
713 Account const borrower(
"borrower");
714 Account const broker(
"broker");
716 Env env{*
this, features};
717 env.
fund(
XRP(20'000), issuer, broker, borrower);
720 auto credType =
"credential1";
738 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
739 .domainID = domainID,
751 env(
pay(issuer, broker, mpt(10'000'000)));
752 env(
pay(issuer, borrower, mpt(1'000)));
759 env(
set(borrower, brokerInfo.brokerID, 10'000),
760 Sig(sfCounterpartySignature, broker),
761 kLoanServiceFee(mpt(100).value()),
762 kPaymentInterval(100),
771 auto const additionalCover = mpt(50'000).value();
776 BEAST_EXPECT(env.
le(brokerMpt) !=
nullptr);
779 auto const brokerBalance = env.
balance(broker, mpt);
780 env(
pay(broker, issuer, brokerBalance));
798 BEAST_EXPECT(brokerSle))
800 Account const pseudo(
"pseudo-account", brokerSle->at(sfAccount));
801 auto const balance = env.
balance(pseudo, mpt);
810 testcase <<
"LoanSet Broker Owner without permissioned domain of the MPT";
812 using namespace loan;
814 Account const issuer(
"issuer");
815 Account const borrower(
"borrower");
816 Account const broker(
"broker");
818 Env env{*
this, features};
819 env.
fund(
XRP(20'000), issuer, broker, borrower);
822 auto credType =
"credential1";
841 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
842 .domainID = domainID,
853 env(
pay(issuer, broker, mpt(10'000'000)));
854 env(
pay(issuer, borrower, mpt(1'000)));
862 auto const brokerBalance = env.
balance(broker, mpt);
863 env(
pay(broker, issuer, brokerBalance));
870 env(
set(borrower, brokerInfo.brokerID, 10'000),
871 Sig(sfCounterpartySignature, broker),
872 kLoanServiceFee(mpt(100).value()),
873 kPaymentInterval(100),
906 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2},
all_))
TestcaseT testcase
Memberspace for declaring test cases.
std::chrono::time_point< NetClock > time_point
std::chrono::duration< rep, period > duration
Number is a floating point type that can represent a wide range of values.
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
void testLoanSetBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features)
void runAmendmentIndependent()
void testLoanDefaultBypassesFreeze()
void testWithdrawReflectsUnrealizedLoss(FeatureBitset features)
void runAmendmentSensitive(FeatureBitset features)
void testLoanPayBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features)
void testLoanPayBrokerOwnerUnauthorizedMPT(FeatureBitset features)
void testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features)
void testLoanDefaultBypassesDeepFreeze()
void testLoanDefaultBypassesMptLockAfterImpair()
void testServiceFeeOnBrokerDeepFreeze()
void run() override
Runs the suite.
void testSequentialFLCDepletion(FeatureBitset features)
Shared base for the Loan*_test family under src/test/app/lending/.
void advancePastDueDate(jtx::Env &env, Keylet const &loanKeylet)
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const ¶ms=BrokerParameters::defaults())
std::string const iouCurrency_
Immutable cryptographic account descriptor.
AccountID id() const
Returns the Account ID.
A transaction testing environment.
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
SLE::const_pointer le(Account const &account) const
Return an account root.
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
void disableFeature(UInt256 const feature)
void enableFeature(UInt256 const feature)
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
MPTID const & issuanceID() const
void create(MPTCreate const &arg=MPTCreate{}, std::source_location const &loc=std::source_location::current())
void authorize(MPTAuthorize const &arg=MPTAuthorize{}, std::source_location const &loc=std::source_location::current())
Set the regular signature on a JTx.
Set the expected result code for a JTx The test will fail if the code doesn't match.
Keylet computation functions.
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const ¤cy) noexcept
The index of a trust line for a given currency.
json::Value deleteCred(jtx::Account const &acc, jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
json::Value accept(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
json::Value create(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
json::Value coverDeposit(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
auto const kPaymentInterval
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.
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.
XrpT const XRP
Converts to XRP Issue or STAmount.
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
static MPTInit const kMptInitNoFund
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
bool set(T &target, std::string const &name, Section const §ion)
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.
TenthBips< std::uint32_t > TenthBips32
TenthBips< std::uint16_t > TenthBips16
std::string to_string(BaseUInt< Bits, Tag > const &a)
TERSubset< CanCvtToTER > TER
Keylet brokerKeylet() const
static json::Value withdraw(WithdrawArgs const &args)
static json::Value deposit(DepositArgs const &args)