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/noop.h>
8#include <test/jtx/txflags.h>
9#include <test/jtx/vault.h>
11#include <xrpl/basics/Number.h>
12#include <xrpl/basics/chrono.h>
13#include <xrpl/beast/unit_test/suite.h>
14#include <xrpl/beast/utility/Zero.h>
15#include <xrpl/json/to_string.h>
16#include <xrpl/ledger/helpers/LendingHelpers.h>
17#include <xrpl/protocol/Feature.h>
18#include <xrpl/protocol/Indexes.h>
19#include <xrpl/protocol/Issue.h>
20#include <xrpl/protocol/Protocol.h>
21#include <xrpl/protocol/SField.h>
22#include <xrpl/protocol/STAmount.h>
23#include <xrpl/protocol/SeqProxy.h>
24#include <xrpl/protocol/TxFlags.h>
25#include <xrpl/protocol/jss.h>
53 testcase(
"PoC: Unsigned-underflow full-pay accrual after early periodic");
57 using namespace std::chrono_literals;
59 Env env{*
this, features};
61 Account const lender{
"poc_lender4"};
62 Account const borrower{
"poc_borrower4"};
63 env.
fund(
XRP(3'000'000), lender, borrower);
72 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
73 Number const principalRequest = asset(1000).value();
74 auto const originationFee = asset(0).value();
75 auto const serviceFee = asset(1).value();
76 auto const serviceFeePA = asset(1);
77 auto const lateFee = asset(0).value();
78 auto const closeFee = asset(0).value();
83 auto const total = 3u;
84 auto const interval = 600u;
85 auto const grace = 60u;
87 auto createJtx = env.
jt(
88 set(borrower, broker.brokerID, principalRequest, 0),
89 Sig(sfCounterpartySignature, lender),
90 kLoanOriginationFee(originationFee),
91 kLoanServiceFee(serviceFee),
92 kLatePaymentFee(lateFee),
93 kClosePaymentFee(closeFee),
95 kInterestRate(interest),
96 kLateInterestRate(lateInterest),
97 kCloseInterestRate(closeInterest),
98 kOverpaymentInterestRate(overpaymentInterest),
100 kPaymentInterval(interval),
105 BEAST_EXPECT(brokerSle);
106 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
120 state.principalOutstanding,
121 state.managementFeeOutstanding,
122 state.periodicPayment,
124 state.paymentRemaining,
126 STAmount const regularDue{asset, components.trackedValueDelta + serviceFeePA.number()};
128 env(
pay(borrower, loanKeylet.key, regularDue));
134 auto const loanSle = env.
le(loanKeylet);
135 BEAST_EXPECT(loanSle);
137 BEAST_EXPECT(brokerSle2);
139 auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) :
Number{};
140 auto const closeInterestRate =
142 auto const managementFeeRate =
151 after.periodicPayment,
153 after.paymentRemaining),
155 env.
current()->parentCloseTime(),
156 after.paymentInterval,
157 after.previousPaymentDate,
162 auto const roundedInterest =
164 Number const roundedFullMgmtFee =
166 Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee;
171 auto const startSecs =
173 auto const lastPaymentDate =
std::max(
after.previousPaymentDate, startSecs);
174 auto const signedDelta =
176 auto const unsignedDelta =
static_cast<std::uint32_t>(nowSecs - lastPaymentDate);
177 log <<
"PoC window: prev=" <<
after.previousPaymentDate <<
" start=" << startSecs
178 <<
" now=" << nowSecs <<
" signedDelta=" << signedDelta
179 <<
" unsignedDelta=" << unsignedDelta <<
std::endl;
183 auto const prevClamped =
std::min(
after.previousPaymentDate, nowSecs);
187 after.periodicPayment,
189 after.paymentRemaining),
191 env.
current()->parentCloseTime(),
192 after.paymentInterval,
196 auto const roundedInterestClamped =
199 asset.
raw(), roundedInterestClamped, managementFeeRate,
after.loanScale);
200 Number const roundedFullInterestClamped =
201 roundedInterestClamped - roundedFullMgmtFeeClamped;
204 after.principalOutstanding + roundedFullInterestClamped + roundedFullMgmtFeeClamped +
208 auto const vaultId2 = brokerSle2 ? brokerSle2->at(sfVaultID) :
uint256{};
210 auto const vaultBefore = env.
le(vaultKey2);
211 BEAST_EXPECT(vaultBefore);
212 Number const assetsTotalBefore = vaultBefore ? vaultBefore->at(sfAssetsTotal) :
Number{};
216 after.principalOutstanding + roundedFullInterest + roundedFullMgmtFee +
219 log <<
"PoC payoff: principalOutstanding=" <<
after.principalOutstanding
220 <<
" roundedFullInterest=" << roundedFullInterest
221 <<
" roundedFullMgmtFee=" << roundedFullMgmtFee <<
" closeFee=" << closePaymentFee
223 log <<
"PoC reference (clamped): roundedFullInterestClamped=" << roundedFullInterestClamped
224 <<
" roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped
227 env(
pay(borrower, loanKeylet.key, fullDue),
Txflags(tfLoanFullPayment));
232 BEAST_EXPECT(unsignedDelta >
after.paymentInterval);
235 auto const vaultAfter = env.
le(vaultKey2);
236 BEAST_EXPECT(vaultAfter);
239 auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal);
240 log <<
"PoC NAV: assetsTotalBefore=" << assetsTotalBefore
241 <<
" assetsTotalAfter=" << assetsTotalAfter
242 <<
" delta=" << (assetsTotalAfter - assetsTotalBefore) <<
std::endl;
246 BEAST_EXPECT(fullDue == fullDueClamped);
247 if (fullDue != fullDueClamped)
248 log <<
"PoC delta: overcharge (fullDue > clamped)" <<
std::endl;
252 auto const finalLoan = env.
le(loanKeylet);
253 BEAST_EXPECT(finalLoan);
256 BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0);
257 BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0);
268 Account const issuer(
"issuer");
269 Account const lender(
"lender");
270 Account const borrower(
"borrower");
273 .vaultDeposit = 100000,
281 .principalRequest =
Number{200'000, -6},
282 .lateFee =
Number{200, -6},
289 Env env{*
this, features};
292 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
294 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
297 auto broker = std::get<BrokerInfo>(*loanResult);
298 auto loanKeylet = std::get<Keylet>(*loanResult);
304 if (
auto loan = env.
le(loanKeylet); BEAST_EXPECT(
loan))
306 env.
close(tp{d{
loan->at(sfNextPaymentDueDate) +
loan->at(sfGracePeriod) + 1}});
317 auto const jr = env.
rpc(
"submit", borrower.
name(), submitParam);
319 BEAST_EXPECT(jr.isMember(jss::result));
336 testcase(
"RIPD-3459 - LoanBroker incorrect debt total");
340 Account const issuer(
"issuer");
341 Account const lender(
"lender");
342 Account const borrower(
"borrower");
345 .vaultDeposit = 200'000,
353 .principalRequest =
Number{100'000, -4},
359 Env env{*
this, features};
362 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
364 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
367 auto broker = std::get<BrokerInfo>(*loanResult);
368 auto loanKeylet = std::get<Keylet>(*loanResult);
369 auto pseudoAcct = std::get<Account>(*loanResult);
371 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
373 if (
auto const brokerSle = env.
le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
375 if (
auto const loanSle = env.
le(loanKeylet); BEAST_EXPECT(loanSle))
377 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding));
392 if (
auto const brokerSle = env.
le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
394 if (
auto const loanSle = env.
le(loanKeylet); BEAST_EXPECT(loanSle))
396 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding));
397 BEAST_EXPECT(brokerSle->at(sfDebtTotal) ==
beast::kZero);
405 testcase(
"Crash with tfLoanOverpayment");
407 using namespace loan;
408 Account const lender{
"lender"};
409 Account const issuer{
"issuer"};
410 Account const borrower{
"borrower"};
411 Account const depositor{
"depositor"};
415 Vault const vault(env);
417 env.
fund(
XRP(10'000), lender, issuer, borrower, depositor);
420 auto [tx, vaultKeyLet] = vault.create({.owner = lender, .asset =
xrpIssue()});
424 env(vault.deposit({.depositor = depositor, .id = vaultKeyLet.key, .amount = XRP(1'000)}),
428 auto const brokerKeyLet =
436 env(
set(borrower, brokerKeyLet.key, debtMaximumRequest),
437 Sig(sfCounterpartySignature, lender),
440 kPaymentInterval(150),
460 testcase(
"RIPD-3902 - 1 IOU loan payments");
464 Account const issuer(
"issuer");
465 Account const lender(
"lender");
466 Account const borrower(
"borrower");
477 .principalRequest =
Number{1, 0},
485 Env env{*
this, features};
488 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
490 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
493 auto broker = std::get<BrokerInfo>(*loanResult);
494 auto loanKeylet = std::get<Keylet>(*loanResult);
495 auto pseudoAcct = std::get<Account>(*loanResult);
497 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
533 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2},
all_))
LogOs< char > log
Logging output stream.
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.
json::Value getJson(JsonOptions=JsonOptions::Values::None) const override
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
void testRIPD3902(FeatureBitset features)
void runAmendmentSensitive(FeatureBitset features)
void testRIPD3459(FeatureBitset features)
void runAmendmentIndependent()
void run() override
Runs the suite.
void testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(FeatureBitset features)
void testRIPD3831(FeatureBitset features)
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.
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)
static void topUpBorrower(jtx::Env &env, BrokerInfo const &broker, jtx::Account const &issuer, jtx::Account const &borrower, LoanState const &state, std::optional< Number > const &servFee)
Immutable cryptographic account descriptor.
std::string const & name() const
Return the name.
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)
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.
beast::unit_test::Suite & test
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Set the regular signature on a JTx.
Number loanPrincipalFromPeriodicPayment(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentsRemaining)
PaymentComponents computePaymentComponents(Rules const &rules, Asset const &asset, std::int32_t scale, Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentRemaining, TenthBips16 managementFeeRate)
Keylet loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
json::Value set(AccountID const &account, uint256 const &vaultId, 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.
json::Value noop(Account const &account)
The null transaction.
XRPAmount txFee(Env const &env, std::uint16_t n)
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
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
TenthBips< std::uint16_t > TenthBips16
std::string to_string(BaseUInt< Bits, Tag > const &a)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
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)
TenthBips16 managementFeeRate
std::optional< Number > serviceFee
Helper class to compare the expected state of a loan and loan broker against the data in the ledger.
Asset const & raw() const