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/fee.h>
7#include <test/jtx/jtx_json.h>
8#include <test/jtx/pay.h>
9#include <test/jtx/ter.h>
10#include <test/jtx/trust.h>
12#include <xrpl/basics/Number.h>
13#include <xrpl/beast/unit_test/suite.h>
14#include <xrpl/beast/utility/Zero.h>
15#include <xrpl/json/json_value.h>
16#include <xrpl/protocol/Feature.h>
17#include <xrpl/protocol/Indexes.h>
18#include <xrpl/protocol/Issue.h>
19#include <xrpl/protocol/Protocol.h>
20#include <xrpl/protocol/SField.h>
21#include <xrpl/protocol/STAmount.h>
22#include <xrpl/protocol/SeqProxy.h>
23#include <xrpl/protocol/TER.h>
24#include <xrpl/protocol/TxFlags.h>
25#include <xrpl/protocol/Units.h>
26#include <xrpl/tx/transactors/lending/LoanSet.h>
40 testLoanPayLateFullPaymentBypassesPenalties(
FeatureBitset features)
42 testcase(
"LoanPay full payment skips late penalties");
45 using namespace std::chrono_literals;
47 Env env(*
this, features);
51 Account const borrower{
"borrower"};
53 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
57 env(
trust(lender, asset(100'000'000)));
58 env(
trust(borrower, asset(100'000'000)));
59 env(
pay(issuer, lender, asset(50'000'000)));
60 env(
pay(issuer, borrower, asset(5'000'000)));
65 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
68 if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value())
71 auto const loanSequence = brokerPreLoan->at(sfLoanSequence);
74 Number const principal = asset(1'000).value();
75 Number const serviceFee = asset(2).value();
76 Number const lateFee = asset(5).value();
77 Number const closeFee = asset(4).value();
80 Sig(sfCounterpartySignature, lender),
81 kLoanServiceFee(serviceFee),
82 kLatePaymentFee(lateFee),
83 kClosePaymentFee(closeFee),
88 kPaymentInterval(600),
94 if (!BEAST_EXPECT(state1.paymentRemaining > 1))
99 auto const overdueClose = tp{d{state1.nextPaymentDate + state1.paymentInterval}};
100 env.
close(overdueClose);
103 auto const loanSle = env.
le(loanKeylet);
104 if (!BEAST_EXPECT(brokerSle && loanSle))
109 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
110 TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)};
111 TenthBips32 const lateInterestRateValue{loanSle->at(sfLateInterestRate)};
112 TenthBips32 const closeInterestRateValue{loanSle->at(sfCloseInterestRate)};
114 Number const closePaymentFeeRounded =
116 Number const latePaymentFeeRounded =
120 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
121 Number const totalInterestOutstanding = roundedLoanState.interestDue;
123 auto const periodicRate =
loanPeriodicRate(interestRateValue, state.paymentInterval);
126 state.periodicPayment,
128 state.paymentRemaining,
131 auto const parentCloseTime = env.
current()->parentCloseTime();
132 auto const startDateSeconds =
133 static_cast<std::uint32_t>(state.startDate.time_since_epoch().count());
136 rawLoanState.principalOutstanding,
139 state.paymentInterval,
140 state.previousPaymentDate,
142 closeInterestRateValue);
144 Number const roundedFullInterestAmount =
147 broker.
asset, roundedFullInterestAmount, managementFeeRate, state.loanScale);
148 Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee;
150 Number const trackedValueDelta =
151 state.principalOutstanding + totalInterestOutstanding + state.managementFeeOutstanding;
152 Number const untrackedManagementFee =
153 closePaymentFeeRounded + roundedFullManagementFee - state.managementFeeOutstanding;
154 Number const untrackedInterest = roundedFullInterest - totalInterestOutstanding;
156 Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee;
157 BEAST_EXPECT(baseFullDue ==
roundToAsset(broker.
asset, baseFullDue, state.loanScale));
159 auto const overdueSeconds =
160 parentCloseTime.time_since_epoch().count() - state.nextPaymentDate;
161 if (!BEAST_EXPECT(overdueSeconds > 0))
165 Number const lateInterestRaw = state.principalOutstanding * overdueRate;
166 Number const lateInterestRounded =
169 broker.
asset, lateInterestRounded, managementFeeRate, state.loanScale);
171 lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded;
172 BEAST_EXPECT(penaltyDue >
Number{});
177 env(
pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment));
180 if (
auto const meta = env.
meta(); BEAST_EXPECT(meta))
181 BEAST_EXPECT(meta->at(sfTransactionResult) ==
tesSUCCESS);
184 Number const actualPaid = balanceBefore - balanceAfter;
185 BEAST_EXPECT(actualPaid == baseFullDue);
187 Number const expectedWithPenalty = baseFullDue + penaltyDue;
188 BEAST_EXPECT(expectedWithPenalty > actualPaid);
189 BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue);
196 testcase(
"testOverpaymentManagementFee");
199 using namespace loan;
201 Env env{*
this, features};
203 Account const lender{
"lender"}, borrower{
"borrower"};
205 env.
fund(
XRP(10'000'000), lender, borrower);
215 .vaultDeposit = asset(100'000).value(),
219 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
221 auto const brokerSle = env.
le(result.brokerKeylet());
222 if (!BEAST_EXPECT(brokerSle))
227 borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment),
228 Sig(sfCounterpartySignature, lender),
235 auto const expectedOverpaymentManagementFee =
Number{33333, 0};
236 auto const loanBrokerBalanceBefore = env.
balance(lender);
238 auto const loanPayFee =
Fee(env.
current()->fees().base * 2);
239 env(
pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee);
243 env.
balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee,
244 "overpayment management fee mismatch; expected:" +
245 to_string(expectedOverpaymentManagementFee) +
252 bool const feeCapped = features[fixCleanup3_1_3];
255 testcase <<
"DoS LoanPay: fee calculation " << (feeCapped ?
"capped" :
"uncapped");
258 using namespace std::chrono_literals;
260 Env env(*
this, features);
262 Account const issuer{
"issuer"};
263 Account const lender{
"lender"};
264 Account const borrower{
"borrower"};
266 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
269 BEAST_EXPECT(feeCapped == env.
current()->rules().enabled(fixCleanup3_1_3));
272 env(
trust(lender, iouAsset(100'000'000)));
273 env(
trust(borrower, iouAsset(100'000'000)));
274 env(
pay(issuer, lender, iouAsset(10'000'000)));
275 env(
pay(issuer, borrower, iouAsset(1'000)));
280 using namespace loan;
282 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
283 Number const principalRequest{3959'37, -2};
284 auto const baseFee = env.
current()->fees().base;
286 auto const createJson = env.
json(
298 kPaymentInterval(60),
299 kPaymentTotal(3239184));
306 env(createJson,
Sig(sfCounterpartySignature, lender));
309 auto const roundedPayment = [&]() {
311 BEAST_EXPECT(stateBefore.paymentRemaining == 3239184);
312 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
316 stateBefore.periodicPayment,
317 stateBefore.loanScale,
321 auto test = [&](
int const payFactor,
325 BEAST_EXPECT(stateBefore.paymentRemaining <= 3239184);
326 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
328 Number const amount = roundedPayment * payFactor;
330 XRPAmount const payFee{baseFee * feeFactor};
331 env(loanPayTx,
Ter(expectedTer),
Fee(payFee));
334 ?
std::min(kLoanMaximumPaymentsPerTransaction, payFactor)
339 stateAfter.paymentRemaining == stateBefore.paymentRemaining - expectedChange);
343 kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement;
349 test(1819878, 363976);
351 test(1819878, kMaxFeeIncrements, failWithoutFix);
354 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements);
356 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements + 10);
360 test(kLoanPaymentsPerFeeIncrement * 2, 2);
365 kLoanMaximumPaymentsPerTransaction - 10,
366 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) - 1,
369 kLoanMaximumPaymentsPerTransaction - 10,
370 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement));
373 kLoanMaximumPaymentsPerTransaction - 10,
374 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) + 3);
376 for (
int under = 1; under < kLoanPaymentsPerFeeIncrement; ++under)
378 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements - 1,
telINSUF_FEE_P);
379 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements);
383 kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement,
384 kMaxFeeIncrements - 1);
386 test(kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, kMaxFeeIncrements);
405 testcase(
"LoanSet near-zero interest rate covers principal");
408 using namespace loan;
410 Number const principalRequested{1000};
415 bool created =
false;
424 Env env(*
this, features);
426 Account const issuer{
"issuer"};
427 Account const lender{
"vaultOwner"};
428 Account const borrower{
"borrower"};
436 {.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate =
TenthBips16{0}});
438 auto const brokerSle = env.
le(broker.brokerKeylet());
439 BEAST_EXPECT(brokerSle);
440 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
441 auto const loanKeylet =
444 env(
set(borrower, broker.brokerID, principalRequested),
445 Sig(sfCounterpartySignature, lender),
448 kPaymentInterval(400),
455 if (
auto const loanSle = env.
le(loanKeylet))
458 r.loanScale = loanSle->at(sfLoanScale);
459 r.principal = loanSle->at(sfPrincipalOutstanding);
460 r.totalValue = loanSle->at(sfTotalValueOutstanding);
461 r.managementFee = loanSle->at(sfManagementFeeOutstanding);
462 r.periodicPayment = loanSle->at(sfPeriodicPayment);
475 BEAST_EXPECT(!legacy.created);
480 BEAST_EXPECT(
fixed.created);
481 BEAST_EXPECT(
fixed.loanScale == -10);
482 BEAST_EXPECT(
fixed.principal == principalRequested);
483 BEAST_EXPECT((
fixed.totalValue ==
Number{10000000001903, -10}));
489 BEAST_EXPECT((
fixed.periodicPayment ==
Number{5000000000951293762, -16}));
490 BEAST_EXPECT((
fixed.periodicPayment * 2 ==
Number{1000000000190258752, -15}));
491 BEAST_EXPECT(
fixed.periodicPayment * 2 > principalRequested);
498 testcase <<
"Prevent nextPaymentDueDate overflow";
501 using namespace std::chrono_literals;
503 Env env{*
this, features};
505 Account const issuer{
"issuer"};
506 Account const lender{
"lender"};
507 Account const borrower{
"borrower"};
515 using namespace loan;
517 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
519 using timeType =
decltype(sfNextPaymentDueDate)::type::value_type;
522 static_assert(kMaxTime == 4'294'967'295);
524 auto const baseJson = [&]() {
525 auto createJson = env.
json(
533 kLoanOriginationFee(218),
541 auto const baseFee = env.
current()->fees().base;
543 auto parentCloseTime = [&]() {
544 return env.
current()->parentCloseTime().time_since_epoch().count();
546 auto maxLoanTime = [&]() {
547 auto const startDate = parentCloseTime();
549 BEAST_EXPECT(startDate >= 50);
551 return kMaxTime - startDate;
556 auto const interval = maxLoanTime() + 1;
557 auto const total = 1;
558 auto createJson = env.
json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
566 auto const interval = 60;
567 auto const total = maxLoanTime() + 1;
568 auto createJson = env.
json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
576 auto const interval = maxLoanTime() + 1;
577 auto const total = 1;
578 auto const grace = interval;
579 auto createJson = env.
json(
580 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
588 auto const interval = 1'000'000'000;
589 auto const total = 10;
590 auto createJson = env.
json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
598 auto const interval = 60;
599 auto const total = 1'000'000'000;
600 auto createJson = env.
json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
608 auto const total = 60;
609 auto const interval = (maxLoanTime() - total) / total;
610 auto const grace = interval;
611 auto createJson = env.
json(
612 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
621 auto const grace = 100;
622 auto const interval = maxLoanTime() - grace;
623 auto const total = 1;
624 auto createJson = env.
json(
625 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
631 auto const meta = env.
meta();
632 if (BEAST_EXPECT(meta))
634 BEAST_EXPECT(meta->at(sfTransactionResult) ==
tecKILLED);
638 auto const loanSle = env.
le(
keylet);
640 BEAST_EXPECT(!loanSle);
646 auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10;
647 auto const grace = 5'000;
648 auto const interval = kMaxTime - closeStartDate - grace;
649 auto const total = 1;
650 auto createJson = env.
json(
651 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
657 auto const meta = env.
meta();
658 if (BEAST_EXPECT(meta))
660 BEAST_EXPECT(meta->at(sfTransactionResult) ==
tesSUCCESS);
665 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
666 BEAST_EXPECT(afterState.previousPaymentDate == 0);
667 BEAST_EXPECT(afterState.paymentRemaining == 1);
672 env(
pay(issuer, borrower, iouAsset(
Number{1'055'524'81, -2})));
675 auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10;
676 auto const grace = 5'000;
677 auto const maxLoanTime = kMaxTime - closeStartDate - grace;
678 auto const total = [&]() {
679 if (maxLoanTime % 5 == 0)
681 if (maxLoanTime % 3 == 0)
683 if (maxLoanTime % 2 == 0)
687 if (!BEAST_EXPECT(total != 0))
691 if (!BEAST_EXPECT(brokerState))
694 auto const loanSequence = brokerState->at(sfLoanSequence);
697 auto const interval = maxLoanTime / total;
698 auto createJson = env.
json(
699 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
706 BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval);
707 BEAST_EXPECT(beforeState.previousPaymentDate == 0);
708 BEAST_EXPECT(beforeState.paymentRemaining == total);
709 BEAST_EXPECT(beforeState.periodicPayment > 0);
714 Number const payment = beforeState.periodicPayment * (total - 1);
715 XRPAmount const payFee{baseFee * ((total - 1) / kLoanPaymentsPerFeeIncrement + 1)};
718 auto loanPayTx = env.
json(
pay(borrower,
keylet.key, paymentAmount),
Fee(payFee));
725 BEAST_EXPECT(afterState.paymentRemaining == 1);
726 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
727 BEAST_EXPECT(afterState.previousPaymentDate == kMaxTime - grace - interval);
742 testLoanPayLateFullPaymentBypassesPenalties(features);
755 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2},
all_))
TestcaseT testcase
Memberspace for declaring test cases.
Value removeMember(char const *key)
Remove and return the named member.
static constexpr std::uint32_t kDefaultGracePeriod
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 run() override
Runs the suite.
void runAmendmentIndependent()
void testDosLoanPay(FeatureBitset features)
void testLoanSetNearZeroInterestRateSucceeds()
void runAmendmentSensitive(FeatureBitset features)
void testLoanNextPaymentDueDateOverflow(FeatureBitset features)
void testOverpaymentManagementFee(FeatureBitset features)
jtx::PrettyAsset createFundedIouAsset(jtx::Env &env, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, Number const &lenderPay=100 '000 '000, Number const &borrowerPay=1 '000 '000)
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const ¶ms=BrokerParameters::defaults())
LoanState getCurrentState(jtx::Env const &env, BrokerInfo const &broker, Keylet const &loanKeylet)
Get the state without checking anything.
Keylet nextLoanKeylet(jtx::Env const &env, BrokerInfo const &broker)
static jtx::PrettyAsset createFundedRippleIouAsset(jtx::Env &env, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, Number const &lenderPay=1 '000 '000, Number const &borrowerPay=1 '000 '000)
std::string const iouCurrency_
Immutable cryptographic account descriptor.
A transaction testing environment.
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
TER ter() const
Return the TER for the last JTx.
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)
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.
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.
@ Object
object value (collection of name/value pairs).
Keylet computation functions.
Keylet loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
json::Value set(AccountID const &account, uint256 const &loanBrokerID, Number principalRequested, std::uint32_t flags)
auto const kPaymentInterval
auto const kOverpaymentInterestRate
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.
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.
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
TenthBips< std::uint32_t > TenthBips32
static FunctionType fixed(Keylet const &keylet)
TenthBips< std::uint16_t > TenthBips16
boost::outcome_v2::result< T, std::error_code > Result
std::string to_string(BaseUInt< Bits, Tag > const &a)
STAmount roundToScale(STAmount const &value, std::int32_t scale, Number::RoundingMode rounding=Number::getround())
Round an arbitrary precision Amount to the precision of an STAmount that has a given exponent.
LoanState computeTheoreticalLoanState(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate)
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
bool isTesSuccess(TER x) noexcept
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
TERSubset< CanCvtToTER > TER
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
Number computeFullPaymentInterest(Number const &theoreticalPrincipalOutstanding, Number const &periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, std::uint32_t prevPaymentDate, std::uint32_t startDate, TenthBips32 closeInterestRate)
Asset const & raw() const