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/batch.h>
7#include <test/jtx/fee.h>
8#include <test/jtx/flags.h>
9#include <test/jtx/jtx_json.h>
10#include <test/jtx/mpt.h>
11#include <test/jtx/pay.h>
12#include <test/jtx/ter.h>
13#include <test/jtx/trust.h>
14#include <test/jtx/utility.h>
15#include <test/jtx/vault.h>
17#include <xrpl/basics/Number.h>
18#include <xrpl/basics/base_uint.h>
19#include <xrpl/basics/strHex.h>
20#include <xrpl/beast/unit_test/suite.h>
21#include <xrpl/json/json_value.h>
22#include <xrpl/json/to_string.h>
23#include <xrpl/protocol/Feature.h>
24#include <xrpl/protocol/HashPrefix.h>
25#include <xrpl/protocol/Indexes.h>
26#include <xrpl/protocol/Issue.h>
27#include <xrpl/protocol/SField.h>
28#include <xrpl/protocol/SecretKey.h>
29#include <xrpl/protocol/SeqProxy.h>
30#include <xrpl/protocol/Serializer.h>
31#include <xrpl/protocol/TER.h>
32#include <xrpl/protocol/TxFlags.h>
33#include <xrpl/protocol/jss.h>
55 Env env(*
this, features);
62 Account const borrower{
"borrower"};
76 env(
trust(lender, iouAsset(10'000'000)));
77 env(
trust(borrower, iouAsset(10'000'000)));
78 env(
trust(evan, iouAsset(10'000'000)));
79 env(
pay(issuer, evan, iouAsset(1'000'000)));
80 env(
pay(issuer, lender, iouAsset(10'000'000)));
82 env(
pay(issuer, borrower, iouAsset(10'000)));
86 mptt.
create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
92 env(
pay(issuer, lender, mptAsset(10'000'000)));
93 env(
pay(issuer, evan, mptAsset(1'000'000)));
95 env(
pay(issuer, borrower, mptAsset(10'000)));
98 std::array const assets{iouAsset, xrpAsset, mptAsset};
103 for (
auto const& asset : assets)
110 for (
auto const& broker : brokers)
112 for (
int amountExponent = 3; amountExponent >= 3; --amountExponent)
114 Number const loanAmount{1, amountExponent};
115 for (
int interestExponent = 0; interestExponent >= 0; --interestExponent)
117 testCaseWrapper(env, mptt, assets, broker, loanAmount, interestExponent);
122 BEAST_EXPECT(brokerSle))
124 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
125 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
127 auto const coverAvailable = brokerSle->at(sfCoverAvailable);
129 lender, broker.brokerID,
STAmount(broker.asset, coverAvailable)));
133 BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0);
147 using namespace std::chrono_literals;
150 Env env(*
this, features);
152 Account const issuer{
"issuer"};
155 Account const lender{
"lender"};
168 using namespace loan;
170 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
171 Number const principalRequest{1, 3};
175 auto createJson = env.
json(
184 if (
auto const jt = env.
jt(createJson); BEAST_EXPECT(jt.stx))
190 BEAST_EXPECT(jr.isMember(jss::result));
191 auto const jResult = jr[jss::result];
192 BEAST_EXPECT(jResult[jss::error] ==
"invalidTransaction");
194 jResult[jss::error_exception] ==
195 "fails local checks: Transaction has bad signature.");
200 counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
201 counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
202 if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
203 counterpartyJson[sfSigners] = createJson[sfSigners];
207 env(env.
json(createJson,
Json(sfCounterpartySignature, counterpartyJson)),
212 createJson = env.
json(createJson,
Sig(sfCounterpartySignature, lender));
217 auto const startDate = env.
current()->header().parentCloseTime;
221 auto const res = env.
rpc(
"account_objects", lender.
human());
222 auto const objects = res[jss::result][jss::account_objects];
225 BEAST_EXPECT(objects.size() == 4);
226 for (
auto const&
object : objects)
228 ++types[
object[sfLedgerEntryType].asString()];
230 BEAST_EXPECT(types.
size() == 4);
231 for (
std::string const type : {
"MPToken",
"Vault",
"LoanBroker",
"Loan"})
233 BEAST_EXPECT(types[type] == 1);
236 auto const loanID = [&]() {
238 params[jss::account] = lender.
human();
239 params[jss::type] =
"Loan";
240 auto const res = env.
rpc(
"json",
"account_objects",
to_string(params));
241 auto const objects = res[jss::result][jss::account_objects];
243 BEAST_EXPECT(objects.size() == 1);
245 auto const loan = objects[0u];
246 BEAST_EXPECT(
loan[sfBorrower] == lender.
human());
249 BEAST_EXPECT(!
loan.isMember(sfCloseInterestRate));
250 BEAST_EXPECT(!
loan.isMember(sfClosePaymentFee));
251 BEAST_EXPECT(
loan[sfFlags] == 0);
252 BEAST_EXPECT(
loan[sfGracePeriod] == 60);
253 BEAST_EXPECT(!
loan.isMember(sfInterestRate));
254 BEAST_EXPECT(!
loan.isMember(sfLateInterestRate));
255 BEAST_EXPECT(!
loan.isMember(sfLatePaymentFee));
257 BEAST_EXPECT(!
loan.isMember(sfLoanOriginationFee));
258 BEAST_EXPECT(
loan[sfLoanSequence] == 1);
259 BEAST_EXPECT(!
loan.isMember(sfLoanServiceFee));
260 BEAST_EXPECT(
loan[sfNextPaymentDueDate] ==
loan[sfStartDate].asUInt() + 60);
261 BEAST_EXPECT(!
loan.isMember(sfOverpaymentFee));
262 BEAST_EXPECT(!
loan.isMember(sfOverpaymentInterestRate));
263 BEAST_EXPECT(
loan[sfPaymentInterval] == 60);
264 BEAST_EXPECT(
loan[sfPeriodicPayment] ==
"1000000000");
265 BEAST_EXPECT(
loan[sfPaymentRemaining] == 1);
266 BEAST_EXPECT(!
loan.isMember(sfPreviousPaymentDueDate));
267 BEAST_EXPECT(
loan[sfPrincipalOutstanding] ==
"1000000000");
268 BEAST_EXPECT(
loan[sfTotalValueOutstanding] ==
"1000000000");
269 BEAST_EXPECT(!
loan.isMember(sfLoanScale));
270 BEAST_EXPECT(
loan[sfStartDate].asUInt() == startDate.time_since_epoch().count());
272 return loan[
"index"].asString();
276 env.
close(startDate);
279 env(
pay(lender, loanKeylet.key, broker.
asset(1000)));
288 using namespace loan;
289 Account const issuer(
"issuer");
290 Account const borrower = issuer;
291 Account const lender(
"lender");
294 env.
fund(
XRP(1'000), issuer, lender);
296 static constexpr std::int64_t kIssuerBalance = 10'000'000;
298 {.env = env, .issuer = issuer, .holders = {lender}, .pay = kIssuerBalance});
304 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
306 env(
set(borrower, broker.brokerID, 200),
Sig(sfCounterpartySignature, lender), loanSetFee);
311 BEAST_EXPECT(env.
balance(issuer, asset) == asset(-kIssuerBalance + 200));
314 env(
pay(borrower, loanKeylet.key, asset(200)));
317 BEAST_EXPECT(env.
balance(issuer, asset) == asset(-kIssuerBalance));
323 testcase(
"Test Borrower is Broker");
325 using namespace loan;
326 Account const broker{
"broker"};
327 Account const issuer{
"issuer"};
328 Account const borrower{
"borrower"};
329 Account const depositor{
"depositor"};
331 auto testLoanAsset = [&](
auto&& getMaxDebt,
auto const& borrower) {
333 Vault const vault(env);
335 if (borrower == broker)
337 env.
fund(
XRP(10'000), broker, issuer, depositor);
341 env.
fund(
XRP(10'000), broker, borrower, issuer, depositor);
345 auto const xrpFee =
XRP(100);
348 STAmount const debtMaximumRequest = getMaxDebt(env);
350 auto const& asset = debtMaximumRequest.
asset();
351 auto const initialVault = asset(debtMaximumRequest * 100);
356 auto [tx, vaultKeylet, subscriptionDate] =
357 vault.createClosedEnded({.owner = broker, .asset = asset});
362 {.depositor = depositor, .id = vaultKeylet.key, .amount = initialVault}),
366 vault.closePastSubscription(subscriptionDate);
368 auto const brokerKeylet =
374 auto const serviceFee = 101;
376 env(
set(broker, brokerKeylet.key, debtMaximumRequest),
377 kCounterparty(borrower),
378 Sig(sfCounterpartySignature, borrower),
379 kLoanServiceFee(serviceFee),
385 auto const loanKeylet =
388 auto const brokerBalanceBefore = env.
balance(broker, asset);
390 if (
auto const loanSle = env.
le(loanKeylet); env.
test.BEAST_EXPECT(loanSle))
392 auto const payment = loanSle->at(sfPeriodicPayment);
393 auto const totalPayment = payment + serviceFee;
394 env(
loan::pay(borrower, loanKeylet.key, asset(totalPayment)),
txFee);
396 if (
auto const vaultSle = env.
le(vaultKeylet); BEAST_EXPECT(vaultSle))
398 auto const expected = [&]() {
401 if (borrower != broker)
402 return brokerBalanceBefore.number() + serviceFee;
408 return brokerBalanceBefore.number() - payment - xrpFee.number();
409 return brokerBalanceBefore.number() - payment;
411 BEAST_EXPECT(env.
balance(broker, asset).
value() == asset(expected).value());
417 for (
auto const& borrowerAcct : {broker, borrower})
423 auto const iou = issuer[
"USD"];
424 env(
trust(broker, iou(1'000'000'000)));
425 env(
trust(depositor, iou(1'000'000'000)));
426 env(
pay(issuer, broker, iou(100'000'000)));
427 env(
pay(issuer, depositor, iou(100'000'000)));
437 .holders = {broker, depositor},
438 .pay = 100'000'000});
448 testcase(
"RIPD-4096 - Issuer as borrower");
452 Account const issuer(
"issuer");
453 Account const lender(
"lender");
456 .vaultDeposit = 100'000,
462 .account = lender, .counter = issuer, .principalRequest =
Number{10000}};
466 Env env{*
this, features};
469 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer);
471 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
474 auto broker = std::get<BrokerInfo>(*loanResult);
475 auto loanKeylet = std::get<Keylet>(*loanResult);
476 auto pseudoAcct = std::get<Account>(*loanResult);
478 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
496 testcase <<
"Batch Bypass Counterparty";
498 bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
501 using namespace std::chrono_literals;
502 Env env(*
this, features);
504 Account const lender{
"lender"};
505 Account const borrower{
"borrower"};
515 using namespace loan;
517 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
518 Number const principalRequest{1, 3};
520 auto forgedLoanSet =
set(borrower, broker.
brokerID, principalRequest, 0);
528 parse(randomData).addWithoutSigningFields(ss);
530 sigObject[jss::TxnSignature] =
strHex(
Slice{sig.data(), sig.size()});
537 auto const seq = env.
seq(borrower);
540 env(
batch::outer(borrower, seq, batchFee, tfAllOrNothing),
549 params[jss::account] = borrower.
human();
550 params[jss::type] =
"Loan";
551 auto const res = env.
rpc(
"json",
"account_objects",
to_string(params));
552 auto const objects = res[jss::result][jss::account_objects];
553 BEAST_EXPECT(objects.size() == 0);
569 testcase(
"integration: full loan lifecycle, vault interest at near-zero rate");
573 using namespace std::chrono_literals;
576 Account const issuer{
"issuer"};
577 Account const lender{
"lender"};
578 Account const borrower{
"borrower"};
580 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
582 env(
fset(issuer, asfDefaultRipple));
587 env(
trust(lender, trustLimit));
588 env(
trust(borrower, trustLimit));
590 env(
pay(issuer, lender, iouAsset(5'000'000'000LL)));
591 env(
pay(issuer, borrower, iouAsset(5'000'000'000LL)));
594 auto usdBalance = [&](
Account const& a) {
597 STAmount const borrowerStartBal = usdBalance(borrower);
600 .vaultDeposit =
Number{2, 9},
609 if (!BEAST_EXPECT(vaultBefore))
611 Number const vaultAvailableBefore = vaultBefore->at(sfAssetsAvailable);
614 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
615 Number const principalRequest{1, 9};
616 auto createJson = env.
json(
620 createJson[
"InterestRate"] = 1;
621 createJson[
"PaymentTotal"] = 3;
622 createJson[
"PaymentInterval"] = 600;
625 createJson = env.
json(createJson,
Sig(sfCounterpartySignature, lender));
629 auto const loanSle = env.
le(loanKeylet);
630 if (!BEAST_EXPECT(loanSle))
632 Number const expectedTotalInterest =
633 loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding);
635 env(
pay(borrower, loanKeylet.key, iouAsset(1'500'000'000LL)),
Ter(
tesSUCCESS));
639 if (!BEAST_EXPECT(vaultAfter))
641 Number const vaultAvailableAfter = vaultAfter->at(sfAssetsAvailable);
642 Number const vaultGain = vaultAvailableAfter - vaultAvailableBefore;
644 STAmount const borrowerEndBal = usdBalance(borrower);
645 STAmount const borrowerNetOut = borrowerStartBal - borrowerEndBal;
649 BEAST_EXPECT(vaultGain == expectedTotalInterest);
650 BEAST_EXPECT(
Number(borrowerNetOut) == expectedTotalInterest);
659 Number const decimalReference{38051750382930729LL, -17};
660 Number const tolerance{1, -6};
661 Number const error =
abs(vaultGain - decimalReference);
664 "vault gain " +
to_string(vaultGain) +
" differs from Decimal reference " +
693 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2},
all_))
TestcaseT testcase
Memberspace for declaring test cases.
Lightweight wrapper to tag static string.
constexpr TIss const & get() const
A currency issued by an account.
Number is a floating point type that can represent a wide range of values.
Slice slice() const noexcept
Asset const & asset() const
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Slice slice() const noexcept
An immutable linear range of bytes.
void runAmendmentSensitive(FeatureBitset features)
void testBorrowerIsBroker()
void testBatchBypassCounterparty(FeatureBitset features)
void testSelfLoan(FeatureBitset features)
void testIssuerIsBorrower(FeatureBitset features)
void run() override
Runs the suite.
void runAmendmentIndependent()
void testLifecycle(FeatureBitset features)
void testFullLifecycleVaultPnLNearZeroRate()
Shared base for the Loan*_test family under src/test/app/lending/.
void testCaseWrapper(jtx::Env &env, jtx::MPTTester &mptt, std::array< TAsset, NAsset > const &assets, BrokerInfo const &broker, Number const &loanAmount, int interestExponent)
Wrapper to run a series of lifecycle tests for a given asset and loan amount.
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const ¶ms=BrokerParameters::defaults())
Keylet nextLoanKeylet(jtx::Env const &env, BrokerInfo const &broker)
void makeLoanPayments(jtx::Env &env, BrokerInfo const &broker, LoanParameters const &loanParams, Keylet const &loanKeylet, VerifyLoanStatus const &verifyLoanStatus, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, PaymentParameters const &paymentParams=PaymentParameters::defaults())
std::optional< std::tuple< BrokerInfo, Keylet, jtx::Account > > createLoan(jtx::Env &env, AssetType assetType, BrokerParameters const &brokerParams, LoanParameters const &loanParams, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower)
std::string const iouCurrency_
Immutable cryptographic account descriptor.
SecretKey const & sk() const
Return the secret key.
std::string const & human() const
Returns the human readable public key.
PublicKey const & pk() const
Return the public key.
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.
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
SLE::const_pointer le(Account const &account) const
Return an account root.
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
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.
beast::unit_test::Suite & test
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.
Adds an inner Batch transaction to a JTx and autofills it.
T emplace_back(T... args)
@ Object
object value (collection of name/value pairs).
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
json::Value outer(jtx::Account const &account, uint32_t seq, STAmount const &fee, std::uint32_t flags)
Build an outer Batch transaction JSON object.
XRPAmount calcBatchFee(jtx::Env const &env, uint32_t const &numSigners, uint32_t const &txns=0)
Calculate the expected outer Batch transaction fee.
json::Value set(AccountID const &account, UInt256 const &vaultId, uint32_t flags)
json::Value coverWithdraw(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value del(AccountID const &account, UInt256 const &brokerID, uint32_t flags)
json::Value pay(AccountID const &account, UInt256 const &loanID, STAmount const &amount, std::uint32_t flags)
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.
XRPAmount txFee(Env const &env, std::uint16_t n)
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
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)
constexpr XRPAmount
Convert XRP to drops (integral types).
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.
std::string strHex(FwdIt begin, FwdIt end)
TenthBips< std::uint32_t > TenthBips32
TenthBips< std::uint16_t > TenthBips16
std::string to_string(BaseUInt< Bits, Tag > const &a)
constexpr Number abs(Number x) noexcept
@ TxSign
inner transaction to sign
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
Keylet vaultKeylet() const
Helper class to compare the expected state of a loan and loan broker against the data in the ledger.
STAmount const & value() const
Asset const & raw() const