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>
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>
39 testcase <<
"First-Loss Capital Depletion on Sequential Defaults";
45 Env env{*
this, features};
49 Account const borrowerA{
"borrowerA"};
50 Account const borrowerB{
"borrowerB"};
52 env.
fund(
XRP(1'000'000), issuer, lender, borrowerA, borrowerB);
56 auto const vaultDepositAmount =
64 .vaultDeposit = vaultDepositAmount.value(),
67 .coverDeposit = 21'000,
78 auto const principalAmount =
Number(50'000);
79 auto const loanPaymentInterval = 2592000;
80 auto const loanGracePeriod = 604800;
83 auto loanATx = env.
jt(
84 set(borrowerA, brokerKeylet.key, principalAmount),
85 Sig(sfCounterpartySignature, lender),
97 auto loanBTx = env.
jt(
98 set(borrowerB, brokerKeylet.key, principalAmount),
99 Sig(sfCounterpartySignature, lender),
110 auto loanASle = env.
le(loanAKeylet);
111 if (!BEAST_EXPECT(loanASle))
115 auto const loanANextDue = loanASle->at(sfNextPaymentDueDate);
116 auto const loanAGrace = loanASle->at(sfGracePeriod);
119 env(manage(lender, loanAKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
123 loanASle = env.
le(loanAKeylet);
124 if (!BEAST_EXPECT(loanASle))
126 BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault));
127 BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0);
130 auto brokerSle = env.
le(brokerKeylet);
131 if (!BEAST_EXPECT(brokerSle))
133 auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal);
134 auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable);
137 BEAST_EXPECT(afterFirstDebtTotal == 50'134);
140 BEAST_EXPECT(afterFirstCoverAvailable == 946);
142 env(manage(lender, loanBKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
144 brokerSle = env.
le(brokerKeylet);
145 if (!BEAST_EXPECT(brokerSle))
147 auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal);
148 auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable);
150 BEAST_EXPECT(afterSecondDebtTotal == 0);
152 BEAST_EXPECT(afterSecondCoverAvailable == 0);
162 using namespace loan;
163 using namespace std::chrono_literals;
165 testcase(
"Vault withdraw reflects sfLossUnrealized");
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;
174 static constexpr std::uint64_t kExpectedSharesPerDepositor = 5'000'000'000;
178 Env env{*
this, features};
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"};
187 env.
fund(
XRP(kInitialFunding), issuer, lender, depositorA, depositorB, borrower);
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)));
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)));
210 .depositor = depositorA,
211 .id = broker.vaultKeylet().key,
212 .amount = iouAsset(kDepositAmount),
216 .depositor = depositorB,
217 .id = broker.vaultKeylet().key,
218 .amount = iouAsset(kDepositAmount),
225 if (!BEAST_EXPECT(sleBroker))
228 auto const loanKeylet =
231 env(
set(borrower, broker.brokerID, kPrincipalAmount),
232 Sig(sfCounterpartySignature, lender),
233 kPaymentTotal(kLocalPaymentTotal),
234 kPaymentInterval(kLocalPaymentInterval),
240 env(manage(lender, loanKeylet.key, tfLoanImpair),
Ter(
tesSUCCESS));
244 auto const vaultAfterImpair = env.
le(broker.vaultKeylet());
245 if (!BEAST_EXPECT(vaultAfterImpair))
249 vaultAfterImpair->at(sfLossUnrealized) == broker.asset(kPrincipalAmount).value());
252 auto const shareAsset = vaultAfterImpair->at(sfShareMPTID);
255 return token ?
token->getFieldU64(sfMPTAmount) : 0;
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);
266 auto const attemptWithdrawShares = [&](
Account const& depositor,
271 {.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}),
282 attemptWithdrawShares(depositorA, sharesLpA,
tesSUCCESS);
283 attemptWithdrawShares(depositorB, sharesLpB,
tesSUCCESS);
289 testcase <<
"Service Fee On Broker Deep Freeze";
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"];
297 for (
bool const deepFreeze : {
true,
false})
301 auto getCoverBalance = [&](
BrokerInfo const& brokerInfo,
auto const& accountField) {
305 auto const account = le->at(accountField);
307 BEAST_EXPECT(sleLine))
309 STAmount balance = sleLine->at(sfBalance);
310 if (account > issuer.
id())
318 env.
fund(
XRP(20'000), issuer, broker, borrower);
321 env(
trust(broker, iou(20'000'000)));
322 env(
pay(issuer, broker, iou(10'000'000)));
327 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
332 Sig(sfCounterpartySignature, broker),
333 kLoanServiceFee(iou(100).value()),
334 kPaymentInterval(100),
338 env(
trust(borrower, iou(20'000'000)));
341 env(
pay(issuer, borrower, iou(500)));
348 env(
trust(issuer, broker[
"IOU"](0), tfSetFreeze | tfSetDeepFreeze));
358 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'100));
359 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'000));
364 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'100));
365 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
374 testcase <<
"LoanPay Broker Owner Missing Trustline (PoC)";
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);
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)));
394 env(
set(borrower, brokerInfo.brokerID, 10'000),
395 Sig(sfCounterpartySignature, broker),
396 kLoanServiceFee(iou(100).value()),
397 kPaymentInterval(100),
406 auto const additionalCover = iou(50'000).value();
411 BEAST_EXPECT(env.
le(brokerTrustline) !=
nullptr);
414 auto const brokerBalance = env.
balance(broker, iou);
415 env(
pay(broker, issuer, brokerBalance));
418 env(
trust(broker, iou(0)));
421 BEAST_EXPECT(env.
le(brokerTrustline) ==
nullptr);
427 BEAST_EXPECT(env.
le(brokerTrustline) ==
nullptr);
430 BEAST_EXPECT(brokerSle))
432 Account const pseudo(
"pseudo-account", brokerSle->at(sfAccount));
433 auto const balance = env.
balance(pseudo, iou);
442 testcase <<
"LoanPay Broker Owner MPT unauthorized";
444 using namespace loan;
446 Account const issuer(
"issuer");
447 Account const borrower(
"borrower");
448 Account const broker(
"broker");
450 Env env{*
this, features};
451 env.
fund(
XRP(20'000), issuer, broker, borrower);
455 mptt.
create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
466 env(
pay(issuer, broker, mpt(10'000'000)));
467 env(
pay(issuer, borrower, mpt(1'000)));
474 env(
set(borrower, brokerInfo.brokerID, 10'000),
475 Sig(sfCounterpartySignature, broker),
476 kLoanServiceFee(mpt(100).value()),
477 kPaymentInterval(100),
486 auto const additionalCover = mpt(50'000).value();
491 BEAST_EXPECT(env.
le(brokerMpt) !=
nullptr);
494 auto const brokerBalance = env.
balance(broker, mpt);
495 env(
pay(broker, issuer, brokerBalance));
498 mptt.
authorize({.account = broker, .flags = tfMPTUnauthorize});
501 BEAST_EXPECT(env.
le(brokerMpt) ==
nullptr);
507 BEAST_EXPECT(env.
le(brokerMpt) ==
nullptr);
510 BEAST_EXPECT(brokerSle))
512 Account const pseudo(
"pseudo-account", brokerSle->at(sfAccount));
513 auto const balance = env.
balance(pseudo, mpt);
522 testcase <<
"LoanPay Broker Owner without permissioned domain of the MPT";
524 using namespace loan;
526 Account const issuer(
"issuer");
527 Account const borrower(
"borrower");
528 Account const broker(
"broker");
530 Env env{*
this, features};
531 env.
fund(
XRP(20'000), issuer, broker, borrower);
534 auto credType =
"credential1";
552 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
553 .domainID = domainID,
565 env(
pay(issuer, broker, mpt(10'000'000)));
566 env(
pay(issuer, borrower, mpt(1'000)));
573 env(
set(borrower, brokerInfo.brokerID, 10'000),
574 Sig(sfCounterpartySignature, broker),
575 kLoanServiceFee(mpt(100).value()),
576 kPaymentInterval(100),
585 auto const additionalCover = mpt(50'000).value();
590 BEAST_EXPECT(env.
le(brokerMpt) !=
nullptr);
593 auto const brokerBalance = env.
balance(broker, mpt);
594 env(
pay(broker, issuer, brokerBalance));
612 BEAST_EXPECT(brokerSle))
614 Account const pseudo(
"pseudo-account", brokerSle->at(sfAccount));
615 auto const balance = env.
balance(pseudo, mpt);
624 testcase <<
"LoanSet Broker Owner without permissioned domain of the MPT";
626 using namespace loan;
628 Account const issuer(
"issuer");
629 Account const borrower(
"borrower");
630 Account const broker(
"broker");
632 Env env{*
this, features};
633 env.
fund(
XRP(20'000), issuer, broker, borrower);
636 auto credType =
"credential1";
655 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
656 .domainID = domainID,
667 env(
pay(issuer, broker, mpt(10'000'000)));
668 env(
pay(issuer, borrower, mpt(1'000)));
676 auto const brokerBalance = env.
balance(broker, mpt);
677 env(
pay(broker, issuer, brokerBalance));
684 env(
set(borrower, brokerInfo.brokerID, 10'000),
685 Sig(sfCounterpartySignature, broker),
686 kLoanServiceFee(mpt(100).value()),
687 kPaymentInterval(100),
717 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2},
all_))
TestcaseT testcase
Memberspace for declaring test cases.
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 testWithdrawReflectsUnrealizedLoss(FeatureBitset features)
void runAmendmentSensitive(FeatureBitset features)
void testLoanPayBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features)
void testLoanPayBrokerOwnerUnauthorizedMPT(FeatureBitset features)
void testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features)
void testServiceFeeOnBrokerDeepFreeze()
void run() override
Runs the suite.
void testSequentialFLCDepletion(FeatureBitset features)
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.
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.
void create(MPTCreate const &arg=MPTCreate{})
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
MPTID const & issuanceID() const
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 loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) 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
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.
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.
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)