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/TxFormats.h>
34#include <xrpl/protocol/jss.h>
35#include <xrpl/tx/transactors/system/Batch.h>
58 Env env(*
this, features);
65 Account const borrower{
"borrower"};
79 env(
trust(lender, iouAsset(10'000'000)));
80 env(
trust(borrower, iouAsset(10'000'000)));
81 env(
trust(evan, iouAsset(10'000'000)));
82 env(
pay(issuer, evan, iouAsset(1'000'000)));
83 env(
pay(issuer, lender, iouAsset(10'000'000)));
85 env(
pay(issuer, borrower, iouAsset(10'000)));
89 mptt.
create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
95 env(
pay(issuer, lender, mptAsset(10'000'000)));
96 env(
pay(issuer, evan, mptAsset(1'000'000)));
98 env(
pay(issuer, borrower, mptAsset(10'000)));
101 std::array const assets{iouAsset, xrpAsset, mptAsset};
106 for (
auto const& asset : assets)
113 for (
auto const& broker : brokers)
115 for (
int amountExponent = 3; amountExponent >= 3; --amountExponent)
117 Number const loanAmount{1, amountExponent};
118 for (
int interestExponent = 0; interestExponent >= 0; --interestExponent)
120 testCaseWrapper(env, mptt, assets, broker, loanAmount, interestExponent);
125 BEAST_EXPECT(brokerSle))
127 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
128 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
130 auto const coverAvailable = brokerSle->at(sfCoverAvailable);
132 lender, broker.brokerID,
STAmount(broker.asset, coverAvailable)));
136 BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0);
150 using namespace std::chrono_literals;
153 Env env(*
this, features);
155 Account const issuer{
"issuer"};
158 Account const lender{
"lender"};
171 using namespace loan;
173 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
174 Number const principalRequest{1, 3};
178 auto createJson = env.
json(
187 if (
auto const jt = env.
jt(createJson); BEAST_EXPECT(jt.stx))
193 BEAST_EXPECT(jr.isMember(jss::result));
194 auto const jResult = jr[jss::result];
195 BEAST_EXPECT(jResult[jss::error] ==
"invalidTransaction");
197 jResult[jss::error_exception] ==
198 "fails local checks: Transaction has bad signature.");
203 counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
204 counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
205 if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
206 counterpartyJson[sfSigners] = createJson[sfSigners];
209 createJson = env.
json(createJson,
Json(sfCounterpartySignature, counterpartyJson));
214 auto const startDate = env.
current()->header().parentCloseTime;
218 auto const res = env.
rpc(
"account_objects", lender.
human());
219 auto const objects = res[jss::result][jss::account_objects];
222 BEAST_EXPECT(objects.size() == 4);
223 for (
auto const&
object : objects)
225 ++types[
object[sfLedgerEntryType].asString()];
227 BEAST_EXPECT(types.
size() == 4);
228 for (
std::string const type : {
"MPToken",
"Vault",
"LoanBroker",
"Loan"})
230 BEAST_EXPECT(types[type] == 1);
233 auto const loanID = [&]() {
235 params[jss::account] = lender.
human();
236 params[jss::type] =
"Loan";
237 auto const res = env.
rpc(
"json",
"account_objects",
to_string(params));
238 auto const objects = res[jss::result][jss::account_objects];
240 BEAST_EXPECT(objects.size() == 1);
242 auto const loan = objects[0u];
243 BEAST_EXPECT(
loan[sfBorrower] == lender.
human());
246 BEAST_EXPECT(!
loan.isMember(sfCloseInterestRate));
247 BEAST_EXPECT(!
loan.isMember(sfClosePaymentFee));
248 BEAST_EXPECT(
loan[sfFlags] == 0);
249 BEAST_EXPECT(
loan[sfGracePeriod] == 60);
250 BEAST_EXPECT(!
loan.isMember(sfInterestRate));
251 BEAST_EXPECT(!
loan.isMember(sfLateInterestRate));
252 BEAST_EXPECT(!
loan.isMember(sfLatePaymentFee));
254 BEAST_EXPECT(!
loan.isMember(sfLoanOriginationFee));
255 BEAST_EXPECT(
loan[sfLoanSequence] == 1);
256 BEAST_EXPECT(!
loan.isMember(sfLoanServiceFee));
257 BEAST_EXPECT(
loan[sfNextPaymentDueDate] ==
loan[sfStartDate].asUInt() + 60);
258 BEAST_EXPECT(!
loan.isMember(sfOverpaymentFee));
259 BEAST_EXPECT(!
loan.isMember(sfOverpaymentInterestRate));
260 BEAST_EXPECT(
loan[sfPaymentInterval] == 60);
261 BEAST_EXPECT(
loan[sfPeriodicPayment] ==
"1000000000");
262 BEAST_EXPECT(
loan[sfPaymentRemaining] == 1);
263 BEAST_EXPECT(!
loan.isMember(sfPreviousPaymentDueDate));
264 BEAST_EXPECT(
loan[sfPrincipalOutstanding] ==
"1000000000");
265 BEAST_EXPECT(
loan[sfTotalValueOutstanding] ==
"1000000000");
266 BEAST_EXPECT(!
loan.isMember(sfLoanScale));
267 BEAST_EXPECT(
loan[sfStartDate].asUInt() == startDate.time_since_epoch().count());
269 return loan[
"index"].asString();
273 env.
close(startDate);
276 env(
pay(lender, loanKeylet.key, broker.
asset(1000)));
285 using namespace loan;
286 Account const issuer(
"issuer");
287 Account const borrower = issuer;
288 Account const lender(
"lender");
291 env.
fund(
XRP(1'000), issuer, lender);
293 static constexpr std::int64_t kIssuerBalance = 10'000'000;
295 {.env = env, .issuer = issuer, .holders = {lender}, .pay = kIssuerBalance});
301 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
303 env(
set(borrower, broker.brokerID, 200),
Sig(sfCounterpartySignature, lender), loanSetFee);
308 BEAST_EXPECT(env.
balance(issuer, asset) == asset(-kIssuerBalance + 200));
311 env(
pay(borrower, loanKeylet.key, asset(200)));
314 BEAST_EXPECT(env.
balance(issuer, asset) == asset(-kIssuerBalance));
320 testcase(
"Test Borrower is Broker");
322 using namespace loan;
323 Account const broker{
"broker"};
324 Account const issuer{
"issuer"};
325 Account const borrower{
"borrower"};
326 Account const depositor{
"depositor"};
328 auto testLoanAsset = [&](
auto&& getMaxDebt,
auto const& borrower) {
330 Vault const vault(env);
332 if (borrower == broker)
334 env.
fund(
XRP(10'000), broker, issuer, depositor);
338 env.
fund(
XRP(10'000), broker, borrower, issuer, depositor);
342 auto const xrpFee =
XRP(100);
345 STAmount const debtMaximumRequest = getMaxDebt(env);
347 auto const& asset = debtMaximumRequest.
asset();
348 auto const initialVault = asset(debtMaximumRequest * 100);
350 auto [tx, vaultKeylet] = vault.create({.owner = broker, .asset = asset});
355 {.depositor = depositor, .id = vaultKeylet.key, .amount = initialVault}),
359 auto const brokerKeylet =
365 auto const serviceFee = 101;
367 env(
set(broker, brokerKeylet.key, debtMaximumRequest),
368 kCounterparty(borrower),
369 Sig(sfCounterpartySignature, borrower),
370 kLoanServiceFee(serviceFee),
376 auto const loanKeylet =
379 auto const brokerBalanceBefore = env.
balance(broker, asset);
381 if (
auto const loanSle = env.
le(loanKeylet); env.
test.BEAST_EXPECT(loanSle))
383 auto const payment = loanSle->at(sfPeriodicPayment);
384 auto const totalPayment = payment + serviceFee;
385 env(
loan::pay(borrower, loanKeylet.key, asset(totalPayment)),
txFee);
387 if (
auto const vaultSle = env.
le(vaultKeylet); BEAST_EXPECT(vaultSle))
389 auto const expected = [&]() {
392 if (borrower != broker)
393 return brokerBalanceBefore.number() + serviceFee;
399 return brokerBalanceBefore.number() - payment - xrpFee.number();
400 return brokerBalanceBefore.number() - payment;
402 BEAST_EXPECT(env.
balance(broker, asset).
value() == asset(expected).value());
408 for (
auto const& borrowerAcct : {broker, borrower})
414 auto const iou = issuer[
"USD"];
415 env(
trust(broker, iou(1'000'000'000)));
416 env(
trust(depositor, iou(1'000'000'000)));
417 env(
pay(issuer, broker, iou(100'000'000)));
418 env(
pay(issuer, depositor, iou(100'000'000)));
428 .holders = {broker, depositor},
429 .pay = 100'000'000});
439 testcase(
"RIPD-4096 - Issuer as borrower");
443 Account const issuer(
"issuer");
444 Account const lender(
"lender");
447 .vaultDeposit = 100'000,
453 .account = lender, .counter = issuer, .principalRequest =
Number{10000}};
457 Env env{*
this, features};
460 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer);
462 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
465 auto broker = std::get<BrokerInfo>(*loanResult);
466 auto loanKeylet = std::get<Keylet>(*loanResult);
467 auto pseudoAcct = std::get<Account>(*loanResult);
469 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
487 testcase <<
"Batch Bypass Counterparty";
493 using namespace std::chrono_literals;
494 Env env(*
this, features);
496 Account const lender{
"lender"};
497 Account const borrower{
"borrower"};
507 using namespace loan;
509 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
510 Number const principalRequest{1, 3};
512 auto forgedLoanSet =
set(borrower, broker.
brokerID, principalRequest, 0);
520 parse(randomData).addWithoutSigningFields(ss);
522 sigObject[jss::TxnSignature] =
strHex(
Slice{sig.data(), sig.size()});
529 auto const seq = env.
seq(borrower);
532 env(
batch::outer(borrower, seq, batchFee, tfAllOrNothing),
541 params[jss::account] = borrower.
human();
542 params[jss::type] =
"Loan";
543 auto const res = env.
rpc(
"json",
"account_objects",
to_string(params));
544 auto const objects = res[jss::result][jss::account_objects];
545 BEAST_EXPECT(objects.size() == 0);
561 testcase(
"integration: full loan lifecycle, vault interest at near-zero rate");
565 using namespace std::chrono_literals;
568 Account const issuer{
"issuer"};
569 Account const lender{
"lender"};
570 Account const borrower{
"borrower"};
572 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
574 env(
fset(issuer, asfDefaultRipple));
579 env(
trust(lender, trustLimit));
580 env(
trust(borrower, trustLimit));
582 env(
pay(issuer, lender, iouAsset(5'000'000'000LL)));
583 env(
pay(issuer, borrower, iouAsset(5'000'000'000LL)));
586 auto usdBalance = [&](
Account const& a) {
589 STAmount const borrowerStartBal = usdBalance(borrower);
592 .vaultDeposit =
Number{2, 9},
601 if (!BEAST_EXPECT(vaultBefore))
603 Number const vaultAvailableBefore = vaultBefore->at(sfAssetsAvailable);
606 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
607 Number const principalRequest{1, 9};
608 auto createJson = env.
json(
612 createJson[
"InterestRate"] = 1;
613 createJson[
"PaymentTotal"] = 3;
614 createJson[
"PaymentInterval"] = 600;
617 createJson = env.
json(createJson,
Sig(sfCounterpartySignature, lender));
621 auto const loanSle = env.
le(loanKeylet);
622 if (!BEAST_EXPECT(loanSle))
624 Number const expectedTotalInterest =
625 loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding);
627 env(
pay(borrower, loanKeylet.key, iouAsset(1'500'000'000LL)),
Ter(
tesSUCCESS));
631 if (!BEAST_EXPECT(vaultAfter))
633 Number const vaultAvailableAfter = vaultAfter->at(sfAssetsAvailable);
634 Number const vaultGain = vaultAvailableAfter - vaultAvailableBefore;
636 STAmount const borrowerEndBal = usdBalance(borrower);
637 STAmount const borrowerNetOut = borrowerStartBal - borrowerEndBal;
641 BEAST_EXPECT(vaultGain == expectedTotalInterest);
642 BEAST_EXPECT(
Number(borrowerNetOut) == expectedTotalInterest);
651 Number const decimalReference{38051750382930729LL, -17};
652 Number const tolerance{1, -6};
653 Number const error =
abs(vaultGain - decimalReference);
656 "vault gain " +
to_string(vaultGain) +
" differs from Decimal reference " +
685 {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
static constexpr auto kDisabledTxTypes
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()
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.
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.
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 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
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