mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Compare commits
33 Commits
develop
...
ximinez/lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e064f57a9 | ||
|
|
fee6f2c85f | ||
|
|
b37f1a0837 | ||
|
|
44e438b2f2 | ||
|
|
8efafbe993 | ||
|
|
2c6ad964dc | ||
|
|
9d8cede450 | ||
|
|
2a019fde43 | ||
|
|
934e431af5 | ||
|
|
168b4dff5f | ||
|
|
1a97a6b24f | ||
|
|
ec1faeff0f | ||
|
|
94327d04ba | ||
|
|
afe8614d94 | ||
|
|
3b1b5ef522 | ||
|
|
c22b38dd03 | ||
|
|
60984c19a3 | ||
|
|
068d8458bb | ||
|
|
4304b3acb4 | ||
|
|
29af96bfa1 | ||
|
|
7944d03795 | ||
|
|
b4b47e80ba | ||
|
|
c8e1d02dee | ||
|
|
f68b3326df | ||
|
|
da1cb2d7ce | ||
|
|
0797a5d4db | ||
|
|
d33ee7ed46 | ||
|
|
53183c71e8 | ||
|
|
05eaef5233 | ||
|
|
e44fc46ad0 | ||
|
|
3f8f5a081f | ||
|
|
14236fb767 | ||
|
|
c0b6712064 |
@@ -1857,8 +1857,18 @@ loanMakePayment(
|
|||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// overpayment handling
|
// overpayment handling
|
||||||
|
//
|
||||||
|
// If the "fixSecurity3_1_3" amendment is enabled, truncate "amount",
|
||||||
|
// at the loan scale. If the raw value is used, the overpayment
|
||||||
|
// amount could be meaningless dust. Trying to process such a small
|
||||||
|
// amount will, at best, waste time when all the result values round
|
||||||
|
// to zero. At worst, it can cause logical errors with tiny amounts
|
||||||
|
// of interest that don't add up correctly.
|
||||||
|
auto const roundedAmount = view.rules().enabled(fixSecurity3_1_3)
|
||||||
|
? roundToAsset(asset, amount, loanScale, Number::towards_zero)
|
||||||
|
: amount;
|
||||||
if (paymentType == LoanPaymentType::overpayment && loan->isFlag(lsfLoanOverpayment) &&
|
if (paymentType == LoanPaymentType::overpayment && loan->isFlag(lsfLoanOverpayment) &&
|
||||||
paymentRemainingProxy > 0 && totalPaid < amount &&
|
paymentRemainingProxy > 0 && totalPaid < roundedAmount &&
|
||||||
numPayments < loanMaximumPaymentsPerTransaction)
|
numPayments < loanMaximumPaymentsPerTransaction)
|
||||||
{
|
{
|
||||||
TenthBips32 const overpaymentInterestRate{loan->at(sfOverpaymentInterestRate)};
|
TenthBips32 const overpaymentInterestRate{loan->at(sfOverpaymentInterestRate)};
|
||||||
@@ -1867,7 +1877,7 @@ loanMakePayment(
|
|||||||
// It shouldn't be possible for the overpayment to be greater than
|
// It shouldn't be possible for the overpayment to be greater than
|
||||||
// totalValueOutstanding, because that would have been processed as
|
// totalValueOutstanding, because that would have been processed as
|
||||||
// another normal payment. But cap it just in case.
|
// another normal payment. But cap it just in case.
|
||||||
Number const overpayment = std::min(amount - totalPaid, *totalValueOutstandingProxy);
|
Number const overpayment = std::min(roundedAmount - totalPaid, *totalValueOutstandingProxy);
|
||||||
|
|
||||||
detail::ExtendedPaymentComponents const overpaymentComponents =
|
detail::ExtendedPaymentComponents const overpaymentComponents =
|
||||||
detail::computeOverpaymentComponents(
|
detail::computeOverpaymentComponents(
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#include <bit>
|
#include <bit>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace xrpl {
|
namespace xrpl {
|
||||||
|
|
||||||
@@ -143,6 +144,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
|
|||||||
Number const numPaymentEstimate = static_cast<std::int64_t>(amount / regularPayment);
|
Number const numPaymentEstimate = static_cast<std::int64_t>(amount / regularPayment);
|
||||||
|
|
||||||
// Charge one base fee per paymentsPerFeeIncrement payments, rounding up.
|
// Charge one base fee per paymentsPerFeeIncrement payments, rounding up.
|
||||||
|
// This set round is safe because there's a mode guard just above
|
||||||
Number::setround(Number::upward);
|
Number::setround(Number::upward);
|
||||||
auto const feeIncrements = std::max(
|
auto const feeIncrements = std::max(
|
||||||
std::int64_t(1),
|
std::int64_t(1),
|
||||||
@@ -440,9 +442,10 @@ LoanPay::doApply()
|
|||||||
// Vault object state changes
|
// Vault object state changes
|
||||||
view.update(vaultSle);
|
view.update(vaultSle);
|
||||||
|
|
||||||
|
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||||
|
Number const assetsTotalBefore = *assetsTotalProxy;
|
||||||
#if !NDEBUG
|
#if !NDEBUG
|
||||||
{
|
{
|
||||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
|
||||||
Number const pseudoAccountBalanceBefore = accountHolds(
|
Number const pseudoAccountBalanceBefore = accountHolds(
|
||||||
view,
|
view,
|
||||||
vaultPseudoAccount,
|
vaultPseudoAccount,
|
||||||
@@ -466,16 +469,6 @@ LoanPay::doApply()
|
|||||||
"xrpl::LoanPay::doApply",
|
"xrpl::LoanPay::doApply",
|
||||||
"assets available must not be greater than assets outstanding");
|
"assets available must not be greater than assets outstanding");
|
||||||
|
|
||||||
if (*assetsAvailableProxy > *assetsTotalProxy)
|
|
||||||
{
|
|
||||||
// LCOV_EXCL_START
|
|
||||||
JLOG(j_.fatal()) << "Vault assets available must not be greater "
|
|
||||||
"than assets outstanding. Available: "
|
|
||||||
<< *assetsAvailableProxy << ", Total: " << *assetsTotalProxy;
|
|
||||||
return tecINTERNAL;
|
|
||||||
// LCOV_EXCL_STOP
|
|
||||||
}
|
|
||||||
|
|
||||||
JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
|
JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
|
||||||
<< ", total paid to vault rounded: " << totalPaidToVaultRounded
|
<< ", total paid to vault rounded: " << totalPaidToVaultRounded
|
||||||
<< ", total paid to broker: " << totalPaidToBroker
|
<< ", total paid to broker: " << totalPaidToBroker
|
||||||
@@ -501,12 +494,68 @@ LoanPay::doApply()
|
|||||||
associateAsset(*vaultSle, asset);
|
associateAsset(*vaultSle, asset);
|
||||||
|
|
||||||
// Duplicate some checks after rounding
|
// Duplicate some checks after rounding
|
||||||
|
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
||||||
|
Number const assetsTotalAfter = *assetsTotalProxy;
|
||||||
|
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
*assetsAvailableProxy <= *assetsTotalProxy,
|
assetsAvailableAfter <= assetsTotalAfter,
|
||||||
"xrpl::LoanPay::doApply",
|
"xrpl::LoanPay::doApply",
|
||||||
"assets available must not be greater than assets outstanding");
|
"assets available must not be greater than assets outstanding");
|
||||||
|
if (assetsAvailableAfter == assetsAvailableBefore)
|
||||||
|
{
|
||||||
|
// An unchanged assetsAvailable indicates that the amount paid to the
|
||||||
|
// vault was zero, or rounded to zero. That should be impossible, but I
|
||||||
|
// can't rule it out for extreme edge cases, so fail gracefully if it
|
||||||
|
// happens.
|
||||||
|
//
|
||||||
|
// LCOV_EXCL_START
|
||||||
|
JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
|
||||||
|
<< "Before: " << assetsAvailableBefore //
|
||||||
|
<< ", After: " << assetsAvailableAfter;
|
||||||
|
return tecPRECISION_LOSS;
|
||||||
|
// LCOV_EXCL_STOP
|
||||||
|
}
|
||||||
|
if (paymentParts->valueChange != beast::zero && assetsTotalAfter == assetsTotalBefore)
|
||||||
|
{
|
||||||
|
// Non-zero valueChange with an unchanged assetsTotal indicates that the
|
||||||
|
// actual value change rounded to zero. That should be impossible, but I
|
||||||
|
// can't rule it out for extreme edge cases, so fail gracefully if it
|
||||||
|
// happens.
|
||||||
|
//
|
||||||
|
// LCOV_EXCL_START
|
||||||
|
JLOG(j_.warn())
|
||||||
|
<< "LoanPay: Vault assets expected change, but unchanged after rounding: " //
|
||||||
|
<< "Before: " << assetsTotalBefore //
|
||||||
|
<< ", After: " << assetsTotalAfter //
|
||||||
|
<< ", ValueChange: " << paymentParts->valueChange;
|
||||||
|
return tecPRECISION_LOSS;
|
||||||
|
// LCOV_EXCL_STOP
|
||||||
|
}
|
||||||
|
if (paymentParts->valueChange == beast::zero && assetsTotalAfter != assetsTotalBefore)
|
||||||
|
{
|
||||||
|
// A change in assetsTotal when there was no valueChange indicates that
|
||||||
|
// something really weird happened. That should be flat out impossible.
|
||||||
|
//
|
||||||
|
// LCOV_EXCL_START
|
||||||
|
JLOG(j_.fatal()) << "LoanPay: Vault assets changed unexpectedly after rounding: " //
|
||||||
|
<< "Before: " << assetsTotalBefore //
|
||||||
|
<< ", After: " << assetsTotalAfter //
|
||||||
|
<< ", ValueChange: " << paymentParts->valueChange;
|
||||||
|
return tecINTERNAL;
|
||||||
|
// LCOV_EXCL_STOP
|
||||||
|
}
|
||||||
|
if (assetsAvailableAfter > assetsTotalAfter)
|
||||||
|
{
|
||||||
|
// Assets available are not allowed to be larger than assets total.
|
||||||
|
// LCOV_EXCL_START
|
||||||
|
JLOG(j_.fatal()) << "LoanPay: Vault assets available must not be greater "
|
||||||
|
"than assets outstanding. Available: "
|
||||||
|
<< assetsAvailableAfter << ", Total: " << assetsTotalAfter;
|
||||||
|
return tecINTERNAL;
|
||||||
|
// LCOV_EXCL_STOP
|
||||||
|
}
|
||||||
|
|
||||||
#if !NDEBUG
|
// These three values are used to check that funds are conserved after the transfers
|
||||||
auto const accountBalanceBefore = accountHolds(
|
auto const accountBalanceBefore = accountHolds(
|
||||||
view,
|
view,
|
||||||
account_,
|
account_,
|
||||||
@@ -535,7 +584,6 @@ LoanPay::doApply()
|
|||||||
ahIGNORE_AUTH,
|
ahIGNORE_AUTH,
|
||||||
j_,
|
j_,
|
||||||
SpendableHandling::shFULL_BALANCE);
|
SpendableHandling::shFULL_BALANCE);
|
||||||
#endif
|
|
||||||
|
|
||||||
if (totalPaidToVaultRounded != beast::zero)
|
if (totalPaidToVaultRounded != beast::zero)
|
||||||
{
|
{
|
||||||
@@ -571,19 +619,22 @@ LoanPay::doApply()
|
|||||||
return ter;
|
return ter;
|
||||||
|
|
||||||
#if !NDEBUG
|
#if !NDEBUG
|
||||||
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
{
|
||||||
Number const pseudoAccountBalanceAfter = accountHolds(
|
Number const pseudoAccountBalanceAfter = accountHolds(
|
||||||
view,
|
view,
|
||||||
vaultPseudoAccount,
|
vaultPseudoAccount,
|
||||||
asset,
|
asset,
|
||||||
FreezeHandling::fhIGNORE_FREEZE,
|
FreezeHandling::fhIGNORE_FREEZE,
|
||||||
AuthHandling::ahIGNORE_AUTH,
|
AuthHandling::ahIGNORE_AUTH,
|
||||||
j_);
|
j_);
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
assetsAvailableAfter == pseudoAccountBalanceAfter,
|
assetsAvailableAfter == pseudoAccountBalanceAfter,
|
||||||
"xrpl::LoanPay::doApply",
|
"xrpl::LoanPay::doApply",
|
||||||
"vault pseudo balance agrees after");
|
"vault pseudo balance agrees after");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Check that funds are conserved
|
||||||
auto const accountBalanceAfter = accountHolds(
|
auto const accountBalanceAfter = accountHolds(
|
||||||
view,
|
view,
|
||||||
account_,
|
account_,
|
||||||
@@ -612,14 +663,121 @@ LoanPay::doApply()
|
|||||||
ahIGNORE_AUTH,
|
ahIGNORE_AUTH,
|
||||||
j_,
|
j_,
|
||||||
SpendableHandling::shFULL_BALANCE);
|
SpendableHandling::shFULL_BALANCE);
|
||||||
|
auto const balanceScale = [&]() {
|
||||||
|
// Find a reasonable scale to use for the balance comparisons.
|
||||||
|
//
|
||||||
|
// First find the minimum and maximum exponent of all the non-zero balances, before and
|
||||||
|
// after. If min and max are equal, use that value. If they are not, use "max + 1" to reduce
|
||||||
|
// rounding discrepancies without making the result meaningless. Cap the scale at
|
||||||
|
// STAmount::cMaxOffset, just in case the numbers are all very large.
|
||||||
|
std::vector<int> exponents;
|
||||||
|
|
||||||
|
for (auto const& a : {
|
||||||
|
accountBalanceBefore,
|
||||||
|
vaultBalanceBefore,
|
||||||
|
brokerBalanceBefore,
|
||||||
|
accountBalanceAfter,
|
||||||
|
vaultBalanceAfter,
|
||||||
|
brokerBalanceAfter,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// Exclude zeroes
|
||||||
|
if (a != beast::zero)
|
||||||
|
exponents.push_back(a.exponent());
|
||||||
|
}
|
||||||
|
if (exponents.empty())
|
||||||
|
{
|
||||||
|
UNREACHABLE("xrpl::LoanPay::doApply : all zeroes");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
auto const [minItr, maxItr] = std::ranges::minmax_element(exponents);
|
||||||
|
auto const min = *minItr;
|
||||||
|
auto const max = *maxItr;
|
||||||
|
JLOG(j_.trace()) << "Min scale: " << min << ", max scale: " << max;
|
||||||
|
// IOU rounding can be interesting. We want all the balance checks to agree, but don't want
|
||||||
|
// to round to such an extreme that it becomes meaningless. e.g. Everything rounds to one
|
||||||
|
// digit. So add 1 to the max (reducing the number of digits after the decimal point by 1)
|
||||||
|
// if the scales are not already all the same.
|
||||||
|
return std::min(min == max ? max : max + 1, STAmount::cMaxOffset);
|
||||||
|
}();
|
||||||
|
|
||||||
|
// No object changes are made below this point
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore ==
|
Number::getround() == Number::to_nearest,
|
||||||
accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter,
|
"xrpl::LoanPay::doApply",
|
||||||
|
"Number rounding to_nearest");
|
||||||
|
NumberRoundModeGuard const mg(Number::to_nearest);
|
||||||
|
|
||||||
|
auto const accountBalanceBeforeRounded = roundToScale(accountBalanceBefore, balanceScale);
|
||||||
|
auto const vaultBalanceBeforeRounded = roundToScale(vaultBalanceBefore, balanceScale);
|
||||||
|
auto const brokerBalanceBeforeRounded = roundToScale(brokerBalanceBefore, balanceScale);
|
||||||
|
|
||||||
|
auto const totalBalanceBefore = accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore;
|
||||||
|
auto const totalBalanceBeforeRounded = roundToScale(totalBalanceBefore, balanceScale);
|
||||||
|
|
||||||
|
JLOG(j_.trace()) << "Before: " //
|
||||||
|
<< "account " << Number(accountBalanceBeforeRounded) << " ("
|
||||||
|
<< Number(accountBalanceBefore) << ")"
|
||||||
|
<< ", vault " << Number(vaultBalanceBeforeRounded) << " ("
|
||||||
|
<< Number(vaultBalanceBefore) << ")"
|
||||||
|
<< ", broker " << Number(brokerBalanceBeforeRounded) << " ("
|
||||||
|
<< Number(brokerBalanceBefore) << ")"
|
||||||
|
<< ", total " << Number(totalBalanceBeforeRounded) << " ("
|
||||||
|
<< Number(totalBalanceBefore) << ")";
|
||||||
|
|
||||||
|
auto const accountBalanceAfterRounded = roundToScale(accountBalanceAfter, balanceScale);
|
||||||
|
auto const vaultBalanceAfterRounded = roundToScale(vaultBalanceAfter, balanceScale);
|
||||||
|
auto const brokerBalanceAfterRounded = roundToScale(brokerBalanceAfter, balanceScale);
|
||||||
|
|
||||||
|
auto const totalBalanceAfter = accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter;
|
||||||
|
auto const totalBalanceAfterRounded = roundToScale(totalBalanceAfter, balanceScale);
|
||||||
|
|
||||||
|
JLOG(j_.trace()) << "After: " //
|
||||||
|
<< "account " << Number(accountBalanceAfterRounded) << " ("
|
||||||
|
<< Number(accountBalanceAfter) << ")"
|
||||||
|
<< ", vault " << Number(vaultBalanceAfterRounded) << " ("
|
||||||
|
<< Number(vaultBalanceAfter) << ")"
|
||||||
|
<< ", broker " << Number(brokerBalanceAfterRounded) << " ("
|
||||||
|
<< Number(brokerBalanceAfter) << ")"
|
||||||
|
<< ", total " << Number(totalBalanceAfterRounded) << " ("
|
||||||
|
<< Number(totalBalanceAfter) << ")";
|
||||||
|
|
||||||
|
auto const accountBalanceChange = accountBalanceAfter - accountBalanceBefore;
|
||||||
|
auto const vaultBalanceChange = vaultBalanceAfter - vaultBalanceBefore;
|
||||||
|
auto const brokerBalanceChange = brokerBalanceAfter - brokerBalanceBefore;
|
||||||
|
|
||||||
|
auto const totalBalanceChange = accountBalanceChange + vaultBalanceChange + brokerBalanceChange;
|
||||||
|
auto const totalBalanceChangeRounded = roundToScale(totalBalanceChange, balanceScale);
|
||||||
|
|
||||||
|
JLOG(j_.trace()) << "Changes: " //
|
||||||
|
<< "account " << to_string(accountBalanceChange) //
|
||||||
|
<< ", vault " << to_string(vaultBalanceChange) //
|
||||||
|
<< ", broker " << to_string(brokerBalanceChange) //
|
||||||
|
<< ", total " << to_string(totalBalanceChangeRounded) << " ("
|
||||||
|
<< Number(totalBalanceChange) << ")";
|
||||||
|
|
||||||
|
if (totalBalanceBeforeRounded != totalBalanceAfterRounded)
|
||||||
|
{
|
||||||
|
JLOG(j_.warn()) << "Total rounded balances don't match"
|
||||||
|
<< (totalBalanceChangeRounded == beast::zero ? ", but total changes do"
|
||||||
|
: "");
|
||||||
|
}
|
||||||
|
if (totalBalanceChangeRounded != beast::zero)
|
||||||
|
{
|
||||||
|
JLOG(j_.warn()) << "Total balance changes don't match"
|
||||||
|
<< (totalBalanceBeforeRounded == totalBalanceAfterRounded
|
||||||
|
? ", but total balances do"
|
||||||
|
: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounding for IOUs can be weird, so check a few different ways to show
|
||||||
|
// that funds are conserved.
|
||||||
|
XRPL_ASSERT_PARTS(
|
||||||
|
totalBalanceBeforeRounded == totalBalanceAfterRounded ||
|
||||||
|
totalBalanceChangeRounded == beast::zero,
|
||||||
"xrpl::LoanPay::doApply",
|
"xrpl::LoanPay::doApply",
|
||||||
"funds are conserved (with rounding)");
|
"funds are conserved (with rounding)");
|
||||||
XRPL_ASSERT_PARTS(
|
|
||||||
accountBalanceAfter >= beast::zero, "xrpl::LoanPay::doApply", "positive account balance");
|
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
accountBalanceAfter < accountBalanceBefore || account_ == asset.getIssuer(),
|
accountBalanceAfter < accountBalanceBefore || account_ == asset.getIssuer(),
|
||||||
"xrpl::LoanPay::doApply",
|
"xrpl::LoanPay::doApply",
|
||||||
@@ -640,7 +798,6 @@ LoanPay::doApply()
|
|||||||
vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore,
|
vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore,
|
||||||
"xrpl::LoanPay::doApply",
|
"xrpl::LoanPay::doApply",
|
||||||
"vault and/or broker balance increased");
|
"vault and/or broker balance increased");
|
||||||
#endif
|
|
||||||
|
|
||||||
return tesSUCCESS;
|
return tesSUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -370,16 +370,11 @@ protected:
|
|||||||
env.balance(vaultPseudo, broker.asset).number());
|
env.balance(vaultPseudo, broker.asset).number());
|
||||||
if (ownerCount == 0)
|
if (ownerCount == 0)
|
||||||
{
|
{
|
||||||
// Allow some slop for rounding IOUs
|
// The Vault must be perfectly balanced if there
|
||||||
|
// are no loans outstanding
|
||||||
// TODO: This needs to be an exact match once all the
|
|
||||||
// other rounding issues are worked out.
|
|
||||||
auto const total = vaultSle->at(sfAssetsTotal);
|
auto const total = vaultSle->at(sfAssetsTotal);
|
||||||
auto const available = vaultSle->at(sfAssetsAvailable);
|
auto const available = vaultSle->at(sfAssetsAvailable);
|
||||||
env.test.BEAST_EXPECT(
|
env.test.BEAST_EXPECT(total == available);
|
||||||
total == available ||
|
|
||||||
(!broker.asset.integral() && available != 0 &&
|
|
||||||
((total - available) / available < Number(1, -6))));
|
|
||||||
env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
|
env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7068,6 +7063,140 @@ protected:
|
|||||||
BEAST_EXPECT(afterSecondCoverAvailable == 0);
|
BEAST_EXPECT(afterSecondCoverAvailable == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
testYieldTheftRounding(std::uint32_t flags)
|
||||||
|
{
|
||||||
|
testcase("Rounding manipulation does not permit yield theft");
|
||||||
|
using namespace jtx;
|
||||||
|
using namespace loan;
|
||||||
|
|
||||||
|
// 1. Setup Environment
|
||||||
|
Env env(*this, all);
|
||||||
|
Account const issuer{"issuer"};
|
||||||
|
Account const lender{"lender"};
|
||||||
|
Account const borrower{"borrower"};
|
||||||
|
|
||||||
|
env.fund(XRP(1000), issuer, lender, borrower);
|
||||||
|
env.close();
|
||||||
|
|
||||||
|
// 2. Asset Selection
|
||||||
|
PrettyAsset const iou = issuer["USD"];
|
||||||
|
env(trust(lender, iou(100'000'000)));
|
||||||
|
env(trust(borrower, iou(100'000'000)));
|
||||||
|
env(pay(issuer, lender, iou(100'000'000)));
|
||||||
|
env(pay(issuer, borrower, iou(100'000'000)));
|
||||||
|
env.close();
|
||||||
|
|
||||||
|
// 3. Create Vault and Broker with High Debt Limit (100M)
|
||||||
|
auto const brokerInfo = createVaultAndBroker(
|
||||||
|
env,
|
||||||
|
iou,
|
||||||
|
lender,
|
||||||
|
{
|
||||||
|
.vaultDeposit = 5'000'000,
|
||||||
|
.debtMax = Number{100'000'000},
|
||||||
|
.coverDeposit = 500'000,
|
||||||
|
});
|
||||||
|
auto const [currentSeq, vaultKeylet] = [&]() {
|
||||||
|
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
|
||||||
|
auto const currentSeq = brokerSle->at(sfLoanSequence);
|
||||||
|
auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID));
|
||||||
|
return std::make_tuple(currentSeq, vaultKeylet);
|
||||||
|
}();
|
||||||
|
|
||||||
|
// 4. Loan Parameters (Attack Vector)
|
||||||
|
Number const principal = 1'000'000;
|
||||||
|
TenthBips32 const interestRate = TenthBips32{1}; // 0.001%
|
||||||
|
std::uint32_t const paymentInterval = 86400;
|
||||||
|
std::uint32_t const paymentTotal = 3650;
|
||||||
|
|
||||||
|
auto const loanSetFee = fee(env.current()->fees().base * 2);
|
||||||
|
env(set(borrower, brokerInfo.brokerID, iou(principal).value(), flags),
|
||||||
|
sig(sfCounterpartySignature, lender),
|
||||||
|
loan::interestRate(interestRate),
|
||||||
|
loan::paymentInterval(paymentInterval),
|
||||||
|
loan::paymentTotal(paymentTotal),
|
||||||
|
fee(loanSetFee));
|
||||||
|
env.close();
|
||||||
|
|
||||||
|
// --- RETRIEVE OBJECTS & SETUP ATTACK ---
|
||||||
|
|
||||||
|
auto borrowerBalance = [&]() { return env.balance(borrower, iou); };
|
||||||
|
auto const borrowerScale = static_cast<STAmount const&>(borrowerBalance()).exponent();
|
||||||
|
|
||||||
|
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq);
|
||||||
|
auto const maybePeriodicPayment = [&]() -> std::optional<STAmount> {
|
||||||
|
auto const loanSle = env.le(loanKeylet);
|
||||||
|
if (!BEAST_EXPECT(loanSle))
|
||||||
|
return std::nullopt;
|
||||||
|
// Construct Payment
|
||||||
|
return STAmount{iou, loanSle->at(sfPeriodicPayment)};
|
||||||
|
}();
|
||||||
|
if (!maybePeriodicPayment)
|
||||||
|
return;
|
||||||
|
auto const periodicPayment = *maybePeriodicPayment;
|
||||||
|
auto const roundedPayment = roundToScale(periodicPayment, borrowerScale, Number::upward);
|
||||||
|
|
||||||
|
// ATTACK: Add dust buffer (1e-9) to force 'excess' logic execution
|
||||||
|
STAmount const paymentBuffer{iou, Number(1, -9)};
|
||||||
|
STAmount const attackPayment = periodicPayment + paymentBuffer;
|
||||||
|
|
||||||
|
auto const maybeInitialVaultAssets = [&]() -> std::optional<Number> {
|
||||||
|
auto const vault = env.le(vaultKeylet);
|
||||||
|
if (!BEAST_EXPECT(vault))
|
||||||
|
return std::nullopt;
|
||||||
|
return vault->at(sfAssetsTotal);
|
||||||
|
}();
|
||||||
|
if (!maybeInitialVaultAssets)
|
||||||
|
return;
|
||||||
|
auto const initialVaultAssets = *maybeInitialVaultAssets;
|
||||||
|
|
||||||
|
// 5. Execution Loop
|
||||||
|
int yieldTheftCount = 0;
|
||||||
|
auto previousAssetsTotal = initialVaultAssets;
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
{
|
||||||
|
auto const balanceBefore = borrowerBalance();
|
||||||
|
env(pay(borrower, loanKeylet.key, attackPayment, flags));
|
||||||
|
env.close();
|
||||||
|
auto const borrowerDelta = balanceBefore - borrowerBalance();
|
||||||
|
BEAST_EXPECT(borrowerDelta.signum() == roundedPayment.signum());
|
||||||
|
|
||||||
|
auto const loanSle = env.le(loanKeylet);
|
||||||
|
if (!BEAST_EXPECT(loanSle))
|
||||||
|
break;
|
||||||
|
auto const updatedPayment = STAmount{iou, loanSle->at(sfPeriodicPayment)};
|
||||||
|
BEAST_EXPECT(
|
||||||
|
(roundToScale(updatedPayment, borrowerScale, Number::upward) == roundedPayment));
|
||||||
|
BEAST_EXPECT(
|
||||||
|
(updatedPayment == periodicPayment) ||
|
||||||
|
(flags == tfLoanOverpayment && i >= 2 && updatedPayment < periodicPayment));
|
||||||
|
|
||||||
|
auto const currentVaultSle = env.le(vaultKeylet);
|
||||||
|
if (!BEAST_EXPECT(currentVaultSle))
|
||||||
|
break;
|
||||||
|
|
||||||
|
auto const currentAssetsTotal = currentVaultSle->at(sfAssetsTotal);
|
||||||
|
auto const delta = currentAssetsTotal - previousAssetsTotal;
|
||||||
|
|
||||||
|
BEAST_EXPECT(
|
||||||
|
(delta == beast::zero && borrowerDelta <= roundedPayment) ||
|
||||||
|
(delta > beast::zero && borrowerDelta > roundedPayment));
|
||||||
|
|
||||||
|
// If tx succeeded but Assets Total didn't change, interest was
|
||||||
|
// stolen.
|
||||||
|
if (delta == beast::zero && borrowerDelta > roundedPayment)
|
||||||
|
{
|
||||||
|
yieldTheftCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousAssetsTotal = currentAssetsTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount));
|
||||||
|
}
|
||||||
|
|
||||||
// Tests that vault withdrawals work correctly when the vault has unrealized
|
// Tests that vault withdrawals work correctly when the vault has unrealized
|
||||||
// loss from an impaired loan, ensuring the invariant check properly
|
// loss from an impaired loan, ensuring the invariant check properly
|
||||||
// accounts for the loss.
|
// accounts for the loss.
|
||||||
@@ -7206,6 +7335,11 @@ public:
|
|||||||
testLoanPayLateFullPaymentBypassesPenalties();
|
testLoanPayLateFullPaymentBypassesPenalties();
|
||||||
testLoanCoverMinimumRoundingExploit();
|
testLoanCoverMinimumRoundingExploit();
|
||||||
#endif
|
#endif
|
||||||
|
for (auto const flags : {0u, tfLoanOverpayment})
|
||||||
|
{
|
||||||
|
testYieldTheftRounding(flags);
|
||||||
|
}
|
||||||
|
|
||||||
testWithdrawReflectsUnrealizedLoss();
|
testWithdrawReflectsUnrealizedLoss();
|
||||||
testInvalidLoanSet();
|
testInvalidLoanSet();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user