mirror of
https://github.com/XRPLF/rippled.git
synced 2026-01-26 17:45:31 +00:00
Compare commits
7 Commits
ximinez/lo
...
ximinez/as
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c51758d12c | ||
|
|
69dda6b34c | ||
|
|
2132398b7d | ||
|
|
e3644f265c | ||
|
|
8c573cd0bb | ||
|
|
c894cd2b5f | ||
|
|
ecfe43ece7 |
@@ -23,6 +23,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
|
||||
@@ -62,8 +62,8 @@ private:
|
||||
public:
|
||||
using value_type = STAmount;
|
||||
|
||||
static constexpr int cMinOffset = -96;
|
||||
static constexpr int cMaxOffset = 80;
|
||||
static int const cMinOffset = -96;
|
||||
static int const cMaxOffset = 80;
|
||||
|
||||
// Maximum native value supported by the code
|
||||
constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull;
|
||||
|
||||
@@ -416,6 +416,9 @@ public:
|
||||
void
|
||||
delField(int index);
|
||||
|
||||
SOEStyle
|
||||
getStyle(SField const& field) const;
|
||||
|
||||
bool
|
||||
hasMatchingEntry(STBase const&);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace BuildInfo {
|
||||
// and follow the format described at http://semver.org/
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
char const* const versionString = "3.1.0-rc1"
|
||||
char const* const versionString = "3.1.0-rc2"
|
||||
// clang-format on
|
||||
|
||||
#if defined(DEBUG) || defined(SANITIZER)
|
||||
|
||||
@@ -142,6 +142,8 @@ areComparable(STAmount const& v1, STAmount const& v2)
|
||||
return false;
|
||||
}
|
||||
|
||||
static_assert(INITIAL_XRP.drops() == STAmount::cMaxNativeN);
|
||||
|
||||
STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name)
|
||||
{
|
||||
std::uint64_t value = sit.get64();
|
||||
|
||||
@@ -599,6 +599,12 @@ STObject::delField(int index)
|
||||
v_.erase(v_.begin() + index);
|
||||
}
|
||||
|
||||
SOEStyle
|
||||
STObject::getStyle(SField const& field) const
|
||||
{
|
||||
return mType ? mType->style(field) : soeINVALID;
|
||||
}
|
||||
|
||||
unsigned char
|
||||
STObject::getFieldU8(SField const& field) const
|
||||
{
|
||||
@@ -924,6 +930,11 @@ STObject::add(Serializer& s, WhichFields whichFields) const
|
||||
(sType != STI_OBJECT) ||
|
||||
(field->getFName().fieldType == STI_OBJECT),
|
||||
"ripple::STObject::add : valid field type");
|
||||
XRPL_ASSERT_PARTS(
|
||||
getStyle(field->getFName()) != soeDEFAULT || !field->isDefault(),
|
||||
"ripple::STObject::add",
|
||||
"non-default value");
|
||||
|
||||
field->addFieldID(s);
|
||||
field->add(s);
|
||||
if (sType == STI_ARRAY || sType == STI_OBJECT)
|
||||
|
||||
@@ -18,10 +18,28 @@ associateAsset(SLE& sle, Asset const& asset)
|
||||
// If the field is not set or present, skip it.
|
||||
if (type == STI_NOTPRESENT)
|
||||
continue;
|
||||
|
||||
// If the type doesn't downcast, then the flag shouldn't be on the
|
||||
// SField
|
||||
auto& ta = entry.downcast<STTakesAsset>();
|
||||
auto const style = sle.getStyle(ta.getFName());
|
||||
XRPL_ASSERT_PARTS(
|
||||
style != soeINVALID,
|
||||
"ripple::associateAsset",
|
||||
"valid template element style");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
style != soeDEFAULT || !ta.isDefault(),
|
||||
"ripple::associateAsset",
|
||||
"non-default value");
|
||||
ta.associateAsset(asset);
|
||||
|
||||
// associateAsset in derived classes may change the underlying
|
||||
// value, but it won't know anything about how the value relates to
|
||||
// the SLE. If the template element is soeDEFAULT, and the value
|
||||
// changed to the default value, remove the field.
|
||||
if (style == soeDEFAULT && ta.isDefault())
|
||||
sle.makeFieldAbsent(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7644,154 +7644,6 @@ protected:
|
||||
BEAST_EXPECT(afterSecondCoverAvailable == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testYieldTheftRounding(std::uint32_t flags)
|
||||
{
|
||||
testcase("Yield Theft via Rounding Manipulation");
|
||||
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 = 1'000'000,
|
||||
.debtMax = Number{100'000'000},
|
||||
});
|
||||
auto const [currentSeq, vaultId, vaultKeylet] = [&]() {
|
||||
auto const brokerSle =
|
||||
env.le(keylet::loanbroker(brokerInfo.brokerID));
|
||||
auto const currentSeq = brokerSle->at(sfLoanSequence);
|
||||
auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID));
|
||||
auto const vaultId = brokerSle->at(sfVaultID);
|
||||
return std::make_tuple(currentSeq, vaultId, vaultKeylet);
|
||||
}();
|
||||
Vault vault{env};
|
||||
env(vault.deposit(
|
||||
{.depositor = lender, .id = vaultId, .amount = iou(5'000'000)}));
|
||||
env.close();
|
||||
env(loanBroker::coverDeposit(
|
||||
lender, brokerInfo.brokerID, iou(500'000)));
|
||||
env.close();
|
||||
|
||||
// 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 const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq);
|
||||
auto const [periodicPayment, loanScale] = [&]() {
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
// Construct Payment
|
||||
return std::make_tuple(
|
||||
STAmount{iou, loanSle->at(sfPeriodicPayment)},
|
||||
loanSle->at(sfLoanScale));
|
||||
}();
|
||||
auto const roundedPayment =
|
||||
roundToScale(periodicPayment, loanScale, 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 initialVaultAssets = env.le(vaultKeylet)->at(sfAssetsTotal);
|
||||
|
||||
log << "Periodic Payment: " << periodicPayment.value() << std::endl;
|
||||
log << "Attack Payment: " << attackPayment.value() << std::endl;
|
||||
log << "Initial Vault Assets: " << initialVaultAssets << std::endl;
|
||||
|
||||
// 5. Execution Loop
|
||||
int yieldTheftCount = 0;
|
||||
auto previousAssetsTotal = initialVaultAssets;
|
||||
|
||||
auto borrowerBalance = [&]() { return env.balance(borrower, iou); };
|
||||
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
auto const balanceBefore = borrowerBalance();
|
||||
env(pay(borrower, loanKeylet.key, attackPayment, flags));
|
||||
env.close();
|
||||
auto const borrowerDelta = borrowerBalance() - balanceBefore;
|
||||
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
if (!BEAST_EXPECT(loanSle))
|
||||
break;
|
||||
auto const updatedPayment =
|
||||
STAmount{iou, loanSle->at(sfPeriodicPayment)};
|
||||
BEAST_EXPECT(
|
||||
(roundToScale(updatedPayment, loanScale, 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++;
|
||||
// delta should be zero
|
||||
log << "[ALERT] Iteration " << i
|
||||
<< ": YIELD THEFT CONFIRMED. Vault Delta: " << delta
|
||||
<< std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
log << "[INFO] Iteration " << i << ": Normal Yield: " << delta
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
previousAssetsTotal = currentAssetsTotal;
|
||||
}
|
||||
|
||||
BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount));
|
||||
log << "[RESULT] Yield Theft Events: " << yieldTheftCount
|
||||
<< " / 50 payments." << std::endl;
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -7800,11 +7652,6 @@ public:
|
||||
testLoanPayLateFullPaymentBypassesPenalties();
|
||||
testLoanCoverMinimumRoundingExploit();
|
||||
#endif
|
||||
for (auto const flags : {0u, tfLoanOverpayment})
|
||||
{
|
||||
testYieldTheftRounding(flags);
|
||||
}
|
||||
|
||||
testInvalidLoanSet();
|
||||
|
||||
testCoverDepositWithdrawNonTransferableMPT();
|
||||
|
||||
@@ -5801,6 +5801,263 @@ class Vault_test : public beast::unit_test::suite
|
||||
testCase(MPT, "MPT", owner, depositor, issuer);
|
||||
}
|
||||
|
||||
void
|
||||
testAssetsMaximum()
|
||||
{
|
||||
testcase("Assets Maximum");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
|
||||
Vault vault{env};
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
|
||||
auto const maxInt64 =
|
||||
std::to_string(std::numeric_limits<std::int64_t>::max());
|
||||
BEAST_EXPECT(maxInt64 == "9223372036854775807");
|
||||
|
||||
// Naming things is hard
|
||||
auto const maxInt64Plus1 = std::to_string(
|
||||
static_cast<std::uint64_t>(
|
||||
std::numeric_limits<std::int64_t>::max()) +
|
||||
1);
|
||||
BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808");
|
||||
|
||||
auto const initialXRP = to_string(INITIAL_XRP);
|
||||
BEAST_EXPECT(initialXRP == "100000000000000000");
|
||||
|
||||
auto const initialXRPPlus1 = to_string(INITIAL_XRP + 1);
|
||||
BEAST_EXPECT(initialXRPPlus1 == "100000000000000001");
|
||||
|
||||
{
|
||||
testcase("Assets Maximum: XRP");
|
||||
|
||||
PrettyAsset const xrpAsset = xrpIssue();
|
||||
|
||||
auto [tx, keylet] =
|
||||
vault.create({.owner = owner, .asset = xrpAsset});
|
||||
tx[sfData] = "4D65746144617461";
|
||||
|
||||
tx[sfAssetsMaximum] = maxInt64;
|
||||
env(tx, ter(tefEXCEPTION), THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = initialXRPPlus1;
|
||||
env(tx, ter(tefEXCEPTION), THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = initialXRP;
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = maxInt64Plus1;
|
||||
env(tx, ter(tefEXCEPTION), THISLINE);
|
||||
env.close();
|
||||
|
||||
// This value will be rounded
|
||||
auto const insertAt = maxInt64Plus1.size() - 3;
|
||||
auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." +
|
||||
maxInt64Plus1.substr(insertAt); // (max int64+1) / 1000
|
||||
BEAST_EXPECT(decimalTest == "9223372036854775.808");
|
||||
tx[sfAssetsMaximum] = decimalTest;
|
||||
auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
auto const vaultSle = env.le(newKeylet);
|
||||
if (!BEAST_EXPECT(vaultSle))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 9223372036854776);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Assets Maximum: MPT");
|
||||
|
||||
PrettyAsset const mptAsset = [&]() {
|
||||
MPTTester mptt{env, issuer, mptInitNoFund};
|
||||
mptt.create(
|
||||
{.flags =
|
||||
tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
|
||||
env.close();
|
||||
PrettyAsset const mptAsset = mptt["MPT"];
|
||||
mptt.authorize({.account = owner});
|
||||
env.close();
|
||||
return mptAsset;
|
||||
}();
|
||||
|
||||
env(pay(issuer, owner, mptAsset(100'000)), THISLINE);
|
||||
env.close();
|
||||
|
||||
auto [tx, keylet] =
|
||||
vault.create({.owner = owner, .asset = mptAsset});
|
||||
tx[sfData] = "4D65746144617461";
|
||||
|
||||
tx[sfAssetsMaximum] = maxInt64;
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = initialXRPPlus1;
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = initialXRP;
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = maxInt64Plus1;
|
||||
env(tx, ter(tefEXCEPTION), THISLINE);
|
||||
env.close();
|
||||
|
||||
// This value will be rounded
|
||||
auto const insertAt = maxInt64Plus1.size() - 1;
|
||||
auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." +
|
||||
maxInt64Plus1.substr(insertAt); // (max int64+1) / 10
|
||||
BEAST_EXPECT(decimalTest == "922337203685477580.8");
|
||||
tx[sfAssetsMaximum] = decimalTest;
|
||||
auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
auto const vaultSle = env.le(newKeylet);
|
||||
if (!BEAST_EXPECT(vaultSle))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 922337203685477581);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Assets Maximum: IOU");
|
||||
|
||||
// Almost anything goes with IOUs
|
||||
PrettyAsset iouAsset = issuer["IOU"];
|
||||
env.trust(iouAsset(1000), owner);
|
||||
env(pay(issuer, owner, iouAsset(200)));
|
||||
env.close();
|
||||
|
||||
auto [tx, keylet] =
|
||||
vault.create({.owner = owner, .asset = iouAsset});
|
||||
tx[sfData] = "4D65746144617461";
|
||||
|
||||
tx[sfAssetsMaximum] = maxInt64;
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = initialXRPPlus1;
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = initialXRP;
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = maxInt64Plus1;
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = "1000000000000000e80";
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = "1000000000000000e-96";
|
||||
env.close();
|
||||
|
||||
// These values will be rounded to 15 significant digits
|
||||
{
|
||||
auto const insertAt = maxInt64Plus1.size() - 1;
|
||||
auto const decimalTest = maxInt64Plus1.substr(0, insertAt) +
|
||||
"." + maxInt64Plus1.substr(insertAt); // (max int64+1) / 10
|
||||
BEAST_EXPECT(decimalTest == "922337203685477580.8");
|
||||
tx[sfAssetsMaximum] = decimalTest;
|
||||
auto const newKeylet =
|
||||
keylet::vault(owner.id(), env.seq(owner));
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
auto const vaultSle = env.le(newKeylet);
|
||||
if (!BEAST_EXPECT(vaultSle))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(
|
||||
(vaultSle->at(sfAssetsMaximum) ==
|
||||
Number{9223372036854776, 2, Number::normalized{}}));
|
||||
}
|
||||
{
|
||||
tx[sfAssetsMaximum] =
|
||||
"9223372036854775807e40"; // max int64 * 10^40
|
||||
auto const newKeylet =
|
||||
keylet::vault(owner.id(), env.seq(owner));
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
auto const vaultSle = env.le(newKeylet);
|
||||
if (!BEAST_EXPECT(vaultSle))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(
|
||||
(vaultSle->at(sfAssetsMaximum) ==
|
||||
Number{9223372036854776, 43, Number::normalized{}}));
|
||||
}
|
||||
{
|
||||
tx[sfAssetsMaximum] =
|
||||
"9223372036854775807e-40"; // max int64 * 10^-40
|
||||
auto const newKeylet =
|
||||
keylet::vault(owner.id(), env.seq(owner));
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
auto const vaultSle = env.le(newKeylet);
|
||||
if (!BEAST_EXPECT(vaultSle))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(
|
||||
(vaultSle->at(sfAssetsMaximum) ==
|
||||
Number{9223372036854776, -37, Number::normalized{}}));
|
||||
}
|
||||
{
|
||||
tx[sfAssetsMaximum] =
|
||||
"9223372036854775807e-100"; // max int64 * 10^-100
|
||||
auto const newKeylet =
|
||||
keylet::vault(owner.id(), env.seq(owner));
|
||||
env(tx, THISLINE);
|
||||
env.close();
|
||||
|
||||
// Field 'AssetsMaximum' may not be explicitly set to default.
|
||||
auto const vaultSle = env.le(newKeylet);
|
||||
if (!BEAST_EXPECT(vaultSle))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == numZero);
|
||||
}
|
||||
|
||||
// What _can't_ IOUs do?
|
||||
// 1. Exceed maximum exponent / offset
|
||||
tx[sfAssetsMaximum] = "1000000000000000e81";
|
||||
env(tx, ter(tefEXCEPTION), THISLINE);
|
||||
env.close();
|
||||
|
||||
// 2. Mantissa larger than uint64 max
|
||||
try
|
||||
{
|
||||
tx[sfAssetsMaximum] =
|
||||
"18446744073709551617e5"; // uint64 max + 1
|
||||
env(tx, THISLINE);
|
||||
BEAST_EXPECT(false);
|
||||
}
|
||||
catch (parse_error const& e)
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
BEAST_EXPECT(
|
||||
e.what() ==
|
||||
"invalidParamsField 'tx_json.AssetsMaximum' has invalid "
|
||||
"data."s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -5821,6 +6078,7 @@ public:
|
||||
testDelegate();
|
||||
testVaultClawbackBurnShares();
|
||||
testVaultClawbackAssets();
|
||||
testAssetsMaximum();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1928,12 +1928,9 @@ loanMakePayment(
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// overpayment handling
|
||||
auto const roundedAmount =
|
||||
roundToAsset(asset, amount, loanScale, Number::towards_zero);
|
||||
if (paymentType == LoanPaymentType::overpayment &&
|
||||
loan->isFlag(lsfLoanOverpayment) && paymentRemainingProxy > 0 &&
|
||||
totalPaid < roundedAmount &&
|
||||
numPayments < loanMaximumPaymentsPerTransaction)
|
||||
totalPaid < amount && numPayments < loanMaximumPaymentsPerTransaction)
|
||||
{
|
||||
TenthBips32 const overpaymentInterestRate{
|
||||
loan->at(sfOverpaymentInterestRate)};
|
||||
@@ -1943,7 +1940,7 @@ loanMakePayment(
|
||||
// totalValueOutstanding, because that would have been processed as
|
||||
// another normal payment. But cap it just in case.
|
||||
Number const overpayment =
|
||||
std::min(roundedAmount - totalPaid, *totalValueOutstandingProxy);
|
||||
std::min(amount - totalPaid, *totalValueOutstandingProxy);
|
||||
|
||||
detail::ExtendedPaymentComponents const overpaymentComponents =
|
||||
detail::computeOverpaymentComponents(
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
|
||||
namespace ripple {
|
||||
@@ -431,10 +430,9 @@ LoanPay::doApply()
|
||||
// Vault object state changes
|
||||
view.update(vaultSle);
|
||||
|
||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||
Number const assetsTotalBefore = *assetsTotalProxy;
|
||||
#if !NDEBUG
|
||||
{
|
||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||
Number const pseudoAccountBalanceBefore = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
@@ -458,6 +456,17 @@ LoanPay::doApply()
|
||||
"rippled::LoanPay::doApply",
|
||||
"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
|
||||
<< ", total paid to vault rounded: "
|
||||
<< totalPaidToVaultRounded
|
||||
@@ -484,70 +493,10 @@ LoanPay::doApply()
|
||||
associateAsset(*vaultSle, asset);
|
||||
|
||||
// Duplicate some checks after rounding
|
||||
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
||||
Number const assetsTotalAfter = *assetsTotalProxy;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter <= *assetsTotalProxy,
|
||||
*assetsAvailableProxy <= *assetsTotalProxy,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"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_.warn()) << "LoanPay: Vault assets changed unexpectedly after "
|
||||
"rounding: Before: "
|
||||
<< assetsTotalBefore << ", After: " << assetsTotalAfter
|
||||
<< ", ValueChange: " << paymentParts->valueChange;
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (assetsAvailableAfter > *assetsTotalProxy)
|
||||
{
|
||||
// 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: " << *assetsTotalProxy;
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
#if !NDEBUG
|
||||
auto const accountBalanceBefore = accountHolds(
|
||||
@@ -619,6 +568,7 @@ LoanPay::doApply()
|
||||
return ter;
|
||||
|
||||
#if !NDEBUG
|
||||
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
||||
Number const pseudoAccountBalanceAfter = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
@@ -659,98 +609,12 @@ LoanPay::doApply()
|
||||
ahIGNORE_AUTH,
|
||||
j_,
|
||||
SpendableHandling::shFULL_BALANCE);
|
||||
auto const balanceScale = [&]() {
|
||||
// This is so ugly.
|
||||
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());
|
||||
}
|
||||
auto [min, max] =
|
||||
std::minmax_element(exponents.begin(), exponents.end());
|
||||
// IOU rounding can be interesting. Give a margin of error that reflects
|
||||
// the orders of magnitude between the extremes.
|
||||
if (!asset.integral() && *max < STAmount::cMaxOffset * 3 / 4)
|
||||
*max += *max - *min;
|
||||
return std::min(*max, STAmount::cMaxOffset);
|
||||
}();
|
||||
|
||||
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 = roundToScale(
|
||||
accountBalanceChange + vaultBalanceChange + brokerBalanceChange,
|
||||
balanceScale);
|
||||
|
||||
JLOG(j_.trace()) << "Changes: account " << to_string(accountBalanceChange)
|
||||
<< ", vault " << to_string(vaultBalanceChange)
|
||||
<< ", broker " << to_string(brokerBalanceChange)
|
||||
<< ", total " << to_string(totalBalanceChange);
|
||||
|
||||
// Rounding for IOUs can be weird, so check a few different ways to show
|
||||
// that funds are conserved.
|
||||
XRPL_ASSERT_PARTS(
|
||||
totalBalanceBefore == totalBalanceAfter ||
|
||||
totalBalanceBeforeRounded == totalBalanceAfterRounded ||
|
||||
totalBalanceChange == beast::zero,
|
||||
accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore ==
|
||||
accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter,
|
||||
"ripple::LoanPay::doApply",
|
||||
"funds are conserved (with rounding)");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
accountBalanceAfter >= beast::zero,
|
||||
"ripple::LoanPay::doApply",
|
||||
|
||||
Reference in New Issue
Block a user