mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Compare commits
4 Commits
develop
...
tapanito/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
370be1d267 | ||
|
|
180796dc0c | ||
|
|
2b91e84cb4 | ||
|
|
00af7ba315 |
@@ -644,9 +644,6 @@ doOverpayment(
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal change agrees");
|
||||
|
||||
// I'm not 100% sure the following asserts are correct. If in doubt, and
|
||||
// everything else works, remove any that cause trouble.
|
||||
|
||||
JLOG(j.debug()) << "valueChange: " << loanPaymentParts.valueChange
|
||||
<< ", totalValue before: " << *totalValueOutstandingProxy
|
||||
<< ", totalValue after: " << newRoundedLoanState.valueOutstanding
|
||||
@@ -658,11 +655,28 @@ doOverpayment(
|
||||
<< overpaymentComponents.trackedPrincipalDelta -
|
||||
(totalValueOutstandingProxy - newRoundedLoanState.valueOutstanding);
|
||||
|
||||
// The valueChange returned by tryOverpayment satisfies
|
||||
// valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
|
||||
// Using the loan-state identity v = p + i + m and the adjacent
|
||||
// `principal change agrees` assertion (dp = oldP - newP), this
|
||||
// rearranges into three independently-computable terms:
|
||||
//
|
||||
// 1. TVO change beyond what principal repayment alone explains:
|
||||
// newTVO - (oldTVO - dp)
|
||||
// 2. Management fee released by re-amortization (positive when
|
||||
// mfee decreased; zero when managementFeeRate == 0):
|
||||
// oldMfee - newMfee
|
||||
// 3. The overpayment's penalty interest part (= untrackedInterest
|
||||
// for the overpayment path; see computeOverpaymentComponents):
|
||||
// trackedInterestPart()
|
||||
[[maybe_unused]] Number const tvoChange = newRoundedLoanState.valueOutstanding -
|
||||
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
|
||||
[[maybe_unused]] Number const mfeeReleased =
|
||||
managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
|
||||
[[maybe_unused]] Number const interestPart = overpaymentComponents.trackedInterestPart();
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
loanPaymentParts.valueChange ==
|
||||
newRoundedLoanState.valueOutstanding -
|
||||
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta) +
|
||||
overpaymentComponents.trackedInterestPart(),
|
||||
loanPaymentParts.valueChange == tvoChange + mfeeReleased + interestPart,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"interest paid agrees");
|
||||
|
||||
@@ -1867,50 +1881,57 @@ loanMakePayment(
|
||||
// It shouldn't be possible for the overpayment to be greater than
|
||||
// totalValueOutstanding, because that would have been processed as
|
||||
// another normal payment. But cap it just in case.
|
||||
Number const overpayment = std::min(amount - totalPaid, *totalValueOutstandingProxy);
|
||||
Number const overpaymentRaw = std::min(amount - totalPaid, *totalValueOutstandingProxy);
|
||||
Number const overpayment = view.rules().enabled(fixCleanup3_2_0)
|
||||
? roundToAsset(asset, overpaymentRaw, loanScale, Number::downward)
|
||||
: overpaymentRaw;
|
||||
|
||||
detail::ExtendedPaymentComponents const overpaymentComponents =
|
||||
detail::computeOverpaymentComponents(
|
||||
asset,
|
||||
loanScale,
|
||||
overpayment,
|
||||
overpaymentInterestRate,
|
||||
overpaymentFeeRate,
|
||||
managementFeeRate);
|
||||
|
||||
// Don't process an overpayment if the whole amount (or more!)
|
||||
// gets eaten by fees and interest.
|
||||
if (overpaymentComponents.trackedPrincipalDelta > 0)
|
||||
// Due to rounding, overpayment could be zero
|
||||
if (overpayment > 0)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.untrackedInterest >= beast::zero,
|
||||
"xrpl::loanMakePayment",
|
||||
"overpayment penalty did not reduce value of loan");
|
||||
// Can't just use `periodicPayment` here, because it might
|
||||
// change
|
||||
auto periodicPaymentProxy = loan->at(sfPeriodicPayment);
|
||||
if (auto const overResult = detail::doOverpayment(
|
||||
detail::ExtendedPaymentComponents const overpaymentComponents =
|
||||
detail::computeOverpaymentComponents(
|
||||
asset,
|
||||
loanScale,
|
||||
overpaymentComponents,
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
periodicPaymentProxy,
|
||||
periodicRate,
|
||||
paymentRemainingProxy,
|
||||
managementFeeRate,
|
||||
j))
|
||||
overpayment,
|
||||
overpaymentInterestRate,
|
||||
overpaymentFeeRate,
|
||||
managementFeeRate);
|
||||
|
||||
// Don't process an overpayment if the whole amount (or more!)
|
||||
// gets eaten by fees and interest.
|
||||
if (overpaymentComponents.trackedPrincipalDelta > 0)
|
||||
{
|
||||
totalParts += *overResult;
|
||||
}
|
||||
else if (overResult.error())
|
||||
{
|
||||
// error() will be the TER returned if a payment is not
|
||||
// made. It will only evaluate to true if it's unsuccessful.
|
||||
// Otherwise, tesSUCCESS means nothing was done, so
|
||||
// continue.
|
||||
return Unexpected(overResult.error());
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.untrackedInterest >= beast::zero,
|
||||
"xrpl::loanMakePayment",
|
||||
"overpayment penalty did not reduce value of loan");
|
||||
// Can't just use `periodicPayment` here, because it might
|
||||
// change
|
||||
auto periodicPaymentProxy = loan->at(sfPeriodicPayment);
|
||||
if (auto const overResult = detail::doOverpayment(
|
||||
asset,
|
||||
loanScale,
|
||||
overpaymentComponents,
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
periodicPaymentProxy,
|
||||
periodicRate,
|
||||
paymentRemainingProxy,
|
||||
managementFeeRate,
|
||||
j))
|
||||
{
|
||||
totalParts += *overResult;
|
||||
}
|
||||
else if (overResult.error())
|
||||
{
|
||||
// error() will be the TER returned if a payment is not
|
||||
// made. It will only evaluate to true if it's unsuccessful.
|
||||
// Otherwise, tesSUCCESS means nothing was done, so
|
||||
// continue.
|
||||
return Unexpected(overResult.error());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7198,6 +7198,89 @@ protected:
|
||||
attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
|
||||
}
|
||||
|
||||
// An overpayment whose residual amount has more precision than loanScale
|
||||
// fires the isRounded(asset, overpayment, loanScale) assertion in
|
||||
// computeOverpaymentComponents (and a downstream "interest paid agrees"
|
||||
// assertion in doOverpayment). fixCleanup3_2_0 rounds the residual down
|
||||
// to loanScale before passing it in. The pre-amendment path can't be
|
||||
// tested here because the assertion fires in Debug builds and aborts
|
||||
// the test process — see the PR description for context.
|
||||
void
|
||||
testBugOverpayUnroundedAmount()
|
||||
{
|
||||
testcase("bug: computeOverpaymentComponents isRounded assertion");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, all);
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const vaultOwner{"vaultOwner"};
|
||||
Account const depositor{"depositor"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
env.fund(XRP(1'000'000), issuer, vaultOwner, depositor, borrower);
|
||||
env.close();
|
||||
env(fset(issuer, asfDefaultRipple));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const iouAsset = issuer["USD"];
|
||||
STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}};
|
||||
env(trust(vaultOwner, iouLimit));
|
||||
env(trust(depositor, iouLimit));
|
||||
env(trust(borrower, iouLimit));
|
||||
env.close();
|
||||
env(pay(issuer, vaultOwner, iouAsset(1'000'000)));
|
||||
env(pay(issuer, depositor, iouAsset(1'000'000)));
|
||||
env(pay(issuer, borrower, iouAsset(1'000'000)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [vaultTx, vaultKeylet] = vault.create({.owner = vaultOwner, .asset = iouAsset});
|
||||
vaultTx[sfScale] = 1;
|
||||
env(vaultTx);
|
||||
env.close();
|
||||
env(vault.deposit(
|
||||
{.depositor = depositor, .id = vaultKeylet.key, .amount = iouAsset(100'000)}));
|
||||
env.close();
|
||||
|
||||
auto const brokerKeylet = keylet::loanbroker(vaultOwner.id(), env.seq(vaultOwner));
|
||||
{
|
||||
using namespace loanBroker;
|
||||
env(set(vaultOwner, vaultKeylet.key),
|
||||
managementFeeRate(TenthBips16{1000}),
|
||||
debtMaximum(Number{5000}),
|
||||
fee(env.current()->fees().base * 2));
|
||||
}
|
||||
env.close();
|
||||
|
||||
auto const brokerStateBefore = env.le(brokerKeylet);
|
||||
auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
|
||||
auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence);
|
||||
|
||||
using namespace loan;
|
||||
auto createJson = env.json(
|
||||
set(borrower, brokerKeylet.key, Number{1000}, tfLoanOverpayment),
|
||||
fee(env.current()->fees().base * 2),
|
||||
json(sfCounterpartySignature, Json::objectValue));
|
||||
|
||||
createJson["InterestRate"] = 10000;
|
||||
createJson["PaymentTotal"] = 12;
|
||||
createJson["PaymentInterval"] = 60;
|
||||
createJson["GracePeriod"] = 60;
|
||||
createJson["OverpaymentFee"] = 1000;
|
||||
createJson["OverpaymentInterestRate"] = 1000;
|
||||
createJson = env.json(createJson, sig(sfCounterpartySignature, vaultOwner));
|
||||
env(createJson, ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// periodic * 1.5 at 15-sig-digit precision: 125.000154585042. This
|
||||
// has too many digits to round cleanly to loanScale=-10, so the
|
||||
// overpayment residual fails the isRounded check.
|
||||
STAmount const payAmount{iouAsset.raw(), Number{125'000'154'585'042LL, -12}};
|
||||
env(pay(borrower, loanKeylet.key, payAmount), txflags(tfLoanOverpayment), ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -7206,6 +7289,8 @@ public:
|
||||
testLoanPayLateFullPaymentBypassesPenalties();
|
||||
testLoanCoverMinimumRoundingExploit();
|
||||
#endif
|
||||
testBugOverpayUnroundedAmount();
|
||||
|
||||
testWithdrawReflectsUnrealizedLoss();
|
||||
testInvalidLoanSet();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user