Compare commits

..

32 Commits

Author SHA1 Message Date
Ed Hennis
55e9fe4e57 Merge branch 'ripple/staging-313' into ximinez/loanpay-assertion 2026-04-10 12:12:31 -04:00
Bart
0915028c93 fix: Change Tuning::bookOffers minimum limit to 1 (#6812)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-04-10 12:00:09 -04:00
Bart
345fc08a07 refactor: Apply various minor improvements and corrections 2026-04-10 10:52:10 -04:00
Valentin Balaschenko
d4d01fc75c fix: Remove fatal assertion on Linux thread name truncation (#6690) 2026-04-10 10:52:10 -04:00
Valentin Balaschenko
22c3f1a9ae chore: Shorten job names to stay within Linux 15-char thread limit (#6669) 2026-04-10 10:52:10 -04:00
Valentin Balaschenko
15613bcb3a refactor: Enforce 15-char limit and simplify labels for thread naming (#6212)
This change continues the thread naming work from #5691 and #5758, which enables more useful lock contention profiling by ensuring threads/jobs have short, stable, human-readable names (rather than being truncated/failing due to OS limits). This changes diagnostic naming only (thread names and job/load-event labels), not behavior.

Specific modifications are:
* Shortens all thread/job names used with `beast::setCurrentThreadName`, so the effective Linux thread name stays within the 15-character limit.
* Removes per-ledger sequence numbers from job/thread names to avoid long labels. This improves aggregation in lock contention profiling for short-lived job executions.
2026-04-10 10:52:10 -04:00
Mayukha Vadari
e74aa71edf fix: Assorted Permissioned Domain fixes (#6587) 2026-04-10 10:52:10 -04:00
Mayukha Vadari
380d4f9ab9 fix: Assorted Vault fixes (#6607) 2026-04-10 10:52:09 -04:00
Mayukha Vadari
fdbba18c6d fix: Assorted Oracle fixes (#6570) 2026-04-10 10:52:09 -04:00
Mayukha Vadari
a025835745 fix: Make assorted NFT fixes (#6566)
This change:
* Removes a set of unnecessary brackets in the initialization of an `std::uint32_t`.
* Fixes a couple of incorrect flags (same value, just wrong variables - so no amendment needed).
2026-04-10 10:52:09 -04:00
Copilot
81119d1f7d fix: Peer crawler port field type inconsistency (#6318)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
Co-authored-by: Mayukha Vadari <mvadari@gmail.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-04-10 10:52:09 -04:00
Peter Chen
19e55bd00a fix: Gateway balance with MPT (#6143)
When `gateway_balances` gets called on an account that is involved in the `EscrowCreate` transaction (with MPT being escrowed), the method returns internal error. This change fixes this case by excluding the MPT type when totaling escrow amount.
2026-04-10 10:52:08 -04:00
tequ
023320d6d9 refactor: Update PermissionedDomainDelete to use keylet for sle access (#6063) 2026-04-10 10:52:08 -04:00
Ed Hennis
dec12299f8 Fix tautological assertion (#6393) 2026-04-10 10:52:08 -04:00
Copilot
c6989da973 fix: Deletes expired NFToken offers from ledger (#5707)
This change introduces the `fixExpiredNFTokenOfferRemoval` amendment that allows expired offers to pass through `preclaim()` and be deleted in `doApply()`, following the same pattern used for expired credentials.
2026-04-10 10:52:08 -04:00
Pratik Mankawde
da32970ebd Limit reply size on TMGetObjectByHash queries (#6110)
`PeerImp` processes `TMGetObjectByHash` queries with an unbounded per-request loop, which performs a `NodeStore` fetch and then appends retrieved data to the reply for each queried object without a local count cap or reply-byte budget. However, the `Nodestore` fetches are expensive when high in numbers, which might slow down the process overall. Hence this code change adds an upper cap on the response size.
2026-04-10 10:52:07 -04:00
Zhanibek Bakin
be960f9401 fix: Truncate thread name to 15 chars on Linux (#5758)
This change:
* Truncates thread names if more than 15 chars with `snprintf`.
* Adds warnings for truncated thread names if `-DTRUNCATED_THREAD_NAME_LOGS=ON`.
* Add a static assert for string literals to stop compiling if > 15 chars.
* Shortens `Resource::Manager` to `Resource::Mngr` to fix the static assert failure.
* Updates `CurrentThreadName_test` unit test specifically for Linux to verify truncation.
2026-04-10 10:52:07 -04:00
Olek
ab3df74e70 Fix: nullptr resolving without db config (#6029)
If the config disables SQL db usage, such as a validator:

```
[ledger_tx_tables]
use_tx_tables = 0
```

then the pointer to DB engine is null, but it was still resolved during startup. Although it didn't crash in Release mode, possibly due to the compiler optimizing it away, it did crash in Debug mode. This change explicitly checks for the validity of the pointer and generates a runtime error if not set.
2026-04-10 10:52:07 -04:00
Mayukha Vadari
7dc13a20f2 fix: Set correct index for limit in book_offers CLI (#6043)
This change fixes an indexing typo in the `book_offers` CLI processing, and does not affect the HTTPS/WS RPC processing.
2026-04-10 10:52:07 -04:00
Olek
0f7d9f23a6 Fix: Perform array size check (#6030)
The `ledger_entry` and `deposit_preauth` requests require an array of credentials. However, the array size is not checked before is gets processing. This fix adds checks and return errors in case array size is too big.
2026-04-10 10:52:07 -04:00
Copilot
fc5ddc5629 fix: account_tx limit parameter validation for malformed values (#5891)
This change fixes the `account_tx` RPC method to properly validate malformed limit parameter values. Previously, invalid values like `0`, `1.2`, `"10"`, `true`, `false`, `-1`, `[]`, `{}`, etc. were either accepted without errors or caused internal errors. Now all malformed values correctly return the `invalidParams` error.

Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com>
2026-04-10 10:52:06 -04:00
Ed Hennis
0f6914ceb1 Merge branch 'ripple/staging-313' into ximinez/loanpay-assertion 2026-04-09 18:53:51 -04:00
Ed Hennis
68b978fd2a chore: Make pre-commit line ending conversions work on Windows (#6832) 2026-04-09 18:49:13 -04:00
Ed Hennis
1bc6b96135 Merge branch 'ripple/staging-313' into ximinez/loanpay-assertion 2026-04-09 11:40:12 -04:00
Bart
fde03c8d77 ci: Use latest version of publish-docs workflow 2026-04-09 10:26:35 -04:00
Ayaz Salikhov
a6b526cc76 ci: Use latest version of publish-docs workflow 2026-04-09 15:18:20 +01:00
Ed Hennis
92bdf1f05f Remove duplicate declaration introduced by merge error 2026-04-06 16:58:32 -04:00
Ed Hennis
c085c5e5a9 Review feedback from @tapanito
- Return a default value in LoanPay balanceScale if the exponent list is
  empty.
- Amendment gate the "roundedAmount" change in overpayment.
- Improve comments and logging.
documentation
2026-04-06 16:37:57 -04:00
Ed Hennis
b82c8c51c7 Address an outdated TODO comment 2026-04-06 16:37:57 -04:00
Ed Hennis
0b986bd847 Verify and log LoanPay fund conservation in all builds
- A warning will be logged if there's a mismatch.
2026-04-06 16:37:57 -04:00
Ed Hennis
3b4e4afc54 Review feedback from @tapanito: readability, duplicate checks 2026-04-06 16:37:57 -04:00
Ed Hennis
76c21d3eb6 Fix touchy "funds are conserved" assertion in LoanPay
- Add "Yield Theft via Rounding Manipulation" test, which used to
  reliably triggered it. The test now verifies that no "yield theft"
  occurs.
2026-04-06 16:37:57 -04:00
14 changed files with 486 additions and 1246 deletions

View File

@@ -4,6 +4,17 @@ name: Build and publish documentation
on:
push:
branches:
- "develop"
paths:
- ".github/workflows/publish-docs.yml"
- "*.md"
- "**/*.md"
- "docs/**"
- "include/**"
- "src/libxrpl/**"
- "src/xrpld/**"
pull_request:
paths:
- ".github/workflows/publish-docs.yml"
- "*.md"
@@ -22,21 +33,26 @@ defaults:
shell: bash
env:
BUILD_DIR: .build
NPROC_SUBTRACT: 2
BUILD_DIR: build
# ubuntu-latest has only 2 CPUs for private repositories
# https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for--private-repositories
NPROC_SUBTRACT: ${{ github.event.repository.visibility == 'public' && '2' || '1' }}
jobs:
publish:
build:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/ci/tools-rippled-documentation:sha-a8c7be1
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@90f11ee655d1687824fb8793db770477d52afbab
with:
enable_ccache: false
- name: Get number of processors
uses: XRPLF/actions/.github/actions/get-nproc@046b1620f6bfd6cd0985dc82c3df02786801fe0a
uses: XRPLF/actions/get-nproc@cf0433aa74563aead044a1e395610c96d65a37cf
id: nproc
with:
subtract: ${{ env.NPROC_SUBTRACT }}
@@ -64,9 +80,23 @@ jobs:
cmake -Donly_docs=ON ..
cmake --build . --target docs --parallel ${BUILD_NPROC}
- name: Publish documentation
if: ${{ github.ref_type == 'branch' && github.ref_name == github.event.repository.default_branch }}
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
- name: Create documentation artifact
if: ${{ github.event.repository.visibility == 'public' && github.event_name == 'push' }}
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ${{ env.BUILD_DIR }}/docs/html
path: ${{ env.BUILD_DIR }}/docs/html
deploy:
if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0

View File

@@ -15,7 +15,6 @@ repos:
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: mixed-line-ending
- id: check-merge-conflict
args: [--assume-in-merge]
@@ -30,6 +29,7 @@ repos:
rev: 5ba47274f9b181bce26a5150a725577f3c336011 # frozen: v3.6.2
hooks:
- id: prettier
args: [--end-of-line=auto]
exclude: |
(?x)^(

View File

@@ -62,8 +62,8 @@ private:
public:
using value_type = STAmount;
static int const cMinOffset = -96;
static int const cMaxOffset = 80;
static constexpr int cMinOffset = -96;
static constexpr int cMaxOffset = 80;
// Maximum native value supported by the code
constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull;

View File

@@ -1559,9 +1559,7 @@ authorizeMPToken(
{
auto const mptokenKey = keylet::mptoken(mptIssuanceID, account);
auto const sleMpt = view.peek(mptokenKey);
if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0 ||
(view.rules().enabled(fixSecurity3_1_3) &&
(*sleMpt)[~sfLockedAmount].value_or(0) != 0))
if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0)
return tecINTERNAL; // LCOV_EXCL_LINE
if (!view.dirRemove(
@@ -1870,9 +1868,7 @@ removeEmptyHolding(
// balance, it can not just be deleted, because that will throw the issuance
// accounting out of balance, so fail. Since this should be impossible
// anyway, I'm not going to put any effort into it.
if (mptoken->at(sfMPTAmount) != 0 ||
(view.rules().enabled(fixSecurity3_1_3) &&
(*mptoken)[~sfLockedAmount].value_or(0) != 0))
if (mptoken->at(sfMPTAmount) != 0)
return tecHAS_OBLIGATIONS;
return authorizeMPToken(

View File

@@ -1939,41 +1939,39 @@ class Invariants_test : public beast::unit_test::suite
{
// Initialize with a placeholder value because there's no default
// ctor
auto const setupAsset = [&](Account const& alice,
Account const& issuer,
Env& env) -> PrettyAsset {
switch (assetType)
{
case Asset::IOU: {
PrettyAsset const iouAsset = issuer["IOU"];
env(trust(alice, iouAsset(1000)));
env(pay(issuer, alice, iouAsset(1000)));
env.close();
return iouAsset;
}
case Asset::MPT: {
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create(
{.flags = tfMPTCanClawback | tfMPTCanTransfer |
tfMPTCanLock});
PrettyAsset const mptAsset = mptt.issuanceID();
mptt.authorize({.account = alice});
env(pay(issuer, alice, mptAsset(1000)));
env.close();
return mptAsset;
}
case Asset::XRP:
default:
return PrettyAsset{xrpIssue(), 1'000'000};
}
};
Keylet loanBrokerKeylet = keylet::amendments();
Preclose const createLoanBroker = [&, this](
Account const& alice,
Account const& issuer,
Env& env) {
auto const asset = setupAsset(alice, issuer, env);
Preclose createLoanBroker = [&, this](
Account const& alice,
Account const& issuer,
Env& env) {
PrettyAsset const asset = [&]() {
switch (assetType)
{
case Asset::IOU: {
PrettyAsset const iouAsset = issuer["IOU"];
env(trust(alice, iouAsset(1000)));
env(pay(issuer, alice, iouAsset(1000)));
env.close();
return iouAsset;
}
case Asset::MPT: {
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create(
{.flags = tfMPTCanClawback | tfMPTCanTransfer |
tfMPTCanLock});
PrettyAsset const mptAsset = mptt.issuanceID();
mptt.authorize({.account = alice});
env(pay(issuer, alice, mptAsset(1000)));
env.close();
return mptAsset;
}
case Asset::XRP:
default:
return PrettyAsset{xrpIssue(), 1'000'000};
}
}();
loanBrokerKeylet = this->createLoanBroker(alice, env, asset);
return BEAST_EXPECT(env.le(loanBrokerKeylet));
};
@@ -2161,61 +2159,6 @@ class Invariants_test : public beast::unit_test::suite
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
// Test: cover available less than pseudo-account asset balance
{
Keylet brokerKeylet = keylet::amendments();
Preclose const createBrokerWithCover =
[&, this](
Account const& alice, Account const& issuer, Env& env) {
auto const asset = setupAsset(alice, issuer, env);
brokerKeylet =
this->createLoanBroker(alice, env, asset);
if (!BEAST_EXPECT(env.le(brokerKeylet)))
return false;
env(loanBroker::coverDeposit(
alice, brokerKeylet.key, asset(10)));
env.close();
return BEAST_EXPECT(env.le(brokerKeylet));
};
doInvariantCheck(
{{"Loan Broker cover available is less than pseudo-account "
"asset balance"}},
[&](Account const&, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(brokerKeylet);
if (!BEAST_EXPECT(sle))
return false;
// Pseudo-account holds 10 units, set cover to 5
sle->at(sfCoverAvailable) = Number(5);
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createBrokerWithCover);
}
// Test: cover available greater than pseudo-account asset balance
// (requires fixSecurity3_1_3)
doInvariantCheck(
{{"Loan Broker cover available is greater than pseudo-account "
"asset balance"}},
[&](Account const&, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(loanBrokerKeylet);
if (!BEAST_EXPECT(sle))
return false;
// Pseudo-account has no cover deposited; set cover
// higher than any incidental balance
sle->at(sfCoverAvailable) = Number(1'000'000);
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
}
}

View File

@@ -314,17 +314,11 @@ protected:
env.balance(vaultPseudo, broker.asset).number());
if (ownerCount == 0)
{
// Allow some slop for rounding IOUs
// TODO: This needs to be an exact match once all the
// other rounding issues are worked out.
// The Vault must be perfectly balanced if there
// are no loans outstanding
auto const total = vaultSle->at(sfAssetsTotal);
auto const available = vaultSle->at(sfAssetsAvailable);
env.test.BEAST_EXPECT(
total == available ||
(!broker.asset.integral() && available != 0 &&
((total - available) / available <
Number(1, -6))));
env.test.BEAST_EXPECT(total == available);
env.test.BEAST_EXPECT(
vaultSle->at(sfLossUnrealized) == 0);
}
@@ -2273,23 +2267,7 @@ protected:
fee(XRPAmount{
baseFee *
(Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}),
ter(tecNO_PERMISSION));
{
env.disableFeature(fixSecurity3_1_3);
env(pay(borrower,
loanKeylet.key,
STAmount{
broker.asset,
state.periodicPayment * Number{15, -1}},
tfLoanOverpayment),
fee(XRPAmount{
baseFee *
(Number{15, -1} / loanPaymentsPerFeeIncrement +
1)}),
ter(temINVALID_FLAG));
env.enableFeature(fixSecurity3_1_3);
}
ter(temINVALID_FLAG));
}
// Try to send a payment marked as multiple mutually exclusive
// payment types. Do not include `txFlags`, so we don't duplicate
@@ -7660,6 +7638,133 @@ 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 = 5'000'000,
.debtMax = Number{100'000'000},
.coverDeposit = 500'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);
}();
// 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);
// 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++;
}
previousAssetsTotal = currentAssetsTotal;
}
BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount));
}
public:
void
run() override
@@ -7668,6 +7773,11 @@ public:
testLoanPayLateFullPaymentBypassesPenalties();
testLoanCoverMinimumRoundingExploit();
#endif
for (auto const flags : {0u, tfLoanOverpayment})
{
testYieldTheftRounding(flags);
}
testInvalidLoanSet();
testCoverDepositWithdrawNonTransferableMPT();

File diff suppressed because it is too large Load Diff

View File

@@ -1927,10 +1927,20 @@ loanMakePayment(
"no value change");
// -------------------------------------------------------------
// 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) && paymentRemainingProxy > 0 &&
totalPaid < amount && numPayments < loanMaximumPaymentsPerTransaction)
totalPaid < roundedAmount &&
numPayments < loanMaximumPaymentsPerTransaction)
{
TenthBips32 const overpaymentInterestRate{
loan->at(sfOverpaymentInterestRate)};
@@ -1940,7 +1950,7 @@ loanMakePayment(
// 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);
std::min(roundedAmount - totalPaid, *totalValueOutstandingProxy);
detail::ExtendedPaymentComponents const overpaymentComponents =
detail::computeOverpaymentComponents(

View File

@@ -2532,33 +2532,18 @@ ValidLoanBroker::finalize(
return false;
}
auto const& vaultAsset = vault->at(sfAsset);
auto const pseudoBalance = accountHolds(
view,
after->at(sfAccount),
vaultAsset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j);
if (after->at(sfCoverAvailable) < pseudoBalance)
if (after->at(sfCoverAvailable) < accountHolds(
view,
after->at(sfAccount),
vaultAsset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j))
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available "
"is less than pseudo-account asset balance";
return false;
}
if (view.rules().enabled(fixSecurity3_1_3))
{
// Don't check the balance when LoanBroker is deleted,
// sfCoverAvailable is not zeroed
if (tx.getTxnType() != ttLOAN_BROKER_DELETE &&
after->at(sfCoverAvailable) > pseudoBalance)
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker cover "
"available is greater "
"than pseudo-account asset balance";
return false;
}
}
}
return true;
}

View File

@@ -418,30 +418,21 @@ LoanManage::doApply()
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultAsset = vaultSle->at(sfAsset);
auto const result = [&]() -> TER {
// Valid flag combinations are checked in preflight. No flags is valid -
// just a noop.
if (tx.isFlag(tfLoanDefault))
return defaultLoan(
view, loanSle, brokerSle, vaultSle, vaultAsset, j_);
if (tx.isFlag(tfLoanImpair))
return impairLoan(view, loanSle, vaultSle, vaultAsset, j_);
if (tx.isFlag(tfLoanUnimpair))
return unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_);
// Noop, as described above.
return tesSUCCESS;
}();
// Valid flag combinations are checked in preflight. No flags is valid -
// just a noop.
if (tx.isFlag(tfLoanDefault))
return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_);
if (tx.isFlag(tfLoanImpair))
return impairLoan(view, loanSle, vaultSle, vaultAsset, j_);
if (tx.isFlag(tfLoanUnimpair))
return unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_);
// Noop, as described above.
// Pre-amendment, associateAsset was only called on the noop (no flags)
// path. Post-amendment, we call associateAsset on all successful paths.
if (view.rules().enabled(fixSecurity3_1_3) && isTesSuccess(result))
{
associateAsset(*loanSle, vaultAsset);
associateAsset(*brokerSle, vaultAsset);
associateAsset(*vaultSle, vaultAsset);
}
associateAsset(*loanSle, vaultAsset);
associateAsset(*brokerSle, vaultAsset);
associateAsset(*vaultSle, vaultAsset);
return result;
return tesSUCCESS;
}
//------------------------------------------------------------------------------

View File

@@ -8,6 +8,7 @@
#include <xrpl/protocol/STTakesAsset.h>
#include <xrpl/protocol/TxFlags.h>
#include <algorithm>
#include <bit>
namespace ripple {
@@ -149,9 +150,7 @@ LoanPay::preclaim(PreclaimContext const& ctx)
{
JLOG(ctx.j.warn())
<< "Requested overpayment on a loan that doesn't allow it";
return ctx.view.rules().enabled(fixSecurity3_1_3)
? TER{tecNO_PERMISSION}
: temINVALID_FLAG;
return temINVALID_FLAG;
}
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
@@ -432,9 +431,10 @@ 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,17 +458,6 @@ 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
@@ -495,12 +484,74 @@ LoanPay::doApply()
associateAsset(*vaultSle, asset);
// Duplicate some checks after rounding
Number const assetsAvailableAfter = *assetsAvailableProxy;
Number const assetsTotalAfter = *assetsTotalProxy;
XRPL_ASSERT_PARTS(
*assetsAvailableProxy <= *assetsTotalProxy,
assetsAvailableAfter <= assetsTotalAfter,
"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_.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(
view,
account_,
@@ -529,7 +580,6 @@ LoanPay::doApply()
ahIGNORE_AUTH,
j_,
SpendableHandling::shFULL_BALANCE);
#endif
if (totalPaidToVaultRounded != beast::zero)
{
@@ -570,19 +620,22 @@ LoanPay::doApply()
return ter;
#if !NDEBUG
Number const assetsAvailableAfter = *assetsAvailableProxy;
Number const pseudoAccountBalanceAfter = accountHolds(
view,
vaultPseudoAccount,
asset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
XRPL_ASSERT_PARTS(
assetsAvailableAfter == pseudoAccountBalanceAfter,
"ripple::LoanPay::doApply",
"vault pseudo balance agrees after");
{
Number const pseudoAccountBalanceAfter = accountHolds(
view,
vaultPseudoAccount,
asset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
XRPL_ASSERT_PARTS(
assetsAvailableAfter == pseudoAccountBalanceAfter,
"xrpl::LoanPay::doApply",
"vault pseudo balance agrees after");
}
#endif
// Check that funds are conserved
auto const accountBalanceAfter = accountHolds(
view,
account_,
@@ -611,16 +664,132 @@ LoanPay::doApply()
ahIGNORE_AUTH,
j_,
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::minmax_element(exponents.begin(), exponents.end());
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);
}();
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(
accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore ==
accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter,
"ripple::LoanPay::doApply",
totalBalanceBeforeRounded == totalBalanceAfterRounded ||
totalBalanceChangeRounded == beast::zero,
"xrpl::LoanPay::doApply",
"funds are conserved (with rounding)");
XRPL_ASSERT_PARTS(
accountBalanceAfter >= beast::zero,
"ripple::LoanPay::doApply",
"positive account balance");
XRPL_ASSERT_PARTS(
accountBalanceAfter < accountBalanceBefore ||
account_ == asset.getIssuer(),
@@ -643,7 +812,6 @@ LoanPay::doApply()
brokerBalanceAfter > brokerBalanceBefore,
"ripple::LoanPay::doApply",
"vault and/or broker balance increased");
#endif
return tesSUCCESS;
}

View File

@@ -30,7 +30,6 @@
#include <xrpl/protocol/TER.h>
#include <optional>
#include <utility>
namespace ripple {
@@ -258,12 +257,7 @@ VaultClawback::assetsToClawback(
auto const mptIssuanceID = *vault->at(sfShareMPTID);
MPTIssue const share{mptIssuanceID};
// Pre-fixSecurity3_1_3: zero-amount clawback returned early without
// clamping to assetsAvailable, allowing more assets to be recovered
// than available when there was an outstanding loan. Retained for
// ledger replay compatibility.
if (!ctx_.view().rules().enabled(fixSecurity3_1_3) &&
clawbackAmount == beast::zero)
if (clawbackAmount == beast::zero)
{
auto const sharesDestroyed = accountHolds(
view(),
@@ -281,40 +275,23 @@ VaultClawback::assetsToClawback(
}
STAmount sharesDestroyed;
STAmount assetsRecovered;
STAmount assetsRecovered = clawbackAmount;
try
{
if (clawbackAmount == beast::zero)
{
sharesDestroyed = accountHolds(
view(),
holder,
share,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
auto const maybeAssets = sharesToAssetsWithdraw(
vault, sleShareIssuance, sharesDestroyed);
if (!maybeAssets)
return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
assetsRecovered = *maybeAssets;
}
else
{
auto const maybeShares =
assetsToSharesWithdraw(vault, sleShareIssuance, clawbackAmount);
auto const maybeShares = assetsToSharesWithdraw(
vault, sleShareIssuance, assetsRecovered);
if (!maybeShares)
return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
sharesDestroyed = *maybeShares;
auto const maybeAssets = sharesToAssetsWithdraw(
vault, sleShareIssuance, sharesDestroyed);
if (!maybeAssets)
return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
assetsRecovered = *maybeAssets;
}
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
if (!maybeAssets)
return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
assetsRecovered = *maybeAssets;
// Clamp to maximum.
if (assetsRecovered > *assetsAvailable)
{

View File

@@ -61,10 +61,10 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
if (!vault)
return tecNO_ENTRY;
auto const amount = ctx.tx[sfAmount];
auto const assets = ctx.tx[sfAmount];
auto const vaultAsset = vault->at(sfAsset);
auto const vaultShare = vault->at(sfShareMPTID);
if (amount.asset() != vaultAsset && amount.asset() != vaultShare)
if (assets.asset() != vaultAsset && assets.asset() != vaultShare)
return tecWRONG_ASSET;
auto const& vaultAccount = vault->at(sfAccount);
@@ -87,56 +87,8 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
// LCOV_EXCL_STOP
}
if (ctx.view.rules().enabled(fixSecurity3_1_3) &&
amount.asset() == vaultShare)
{
// Post-fixSecurity3_1_3: if the user specified shares, convert
// to the equivalent asset amount before checking withdrawal
// limits. Pre-amendment the limit check was skipped for
// share-denominated withdrawals.
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(vaultShare));
if (!sleIssuance)
{
// LCOV_EXCL_START
JLOG(ctx.j.error())
<< "VaultWithdraw: missing issuance of vault shares.";
return tefINTERNAL;
// LCOV_EXCL_STOP
}
try
{
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleIssuance, amount);
if (!maybeAssets)
return tefINTERNAL; // LCOV_EXCL_LINE
if (auto const ret = canWithdraw(
ctx.view,
account,
dstAcct,
*maybeAssets,
ctx.tx.isFieldPresent(sfDestinationTag)))
return ret;
}
catch (std::overflow_error const&)
{
// It's easy to hit this exception from Number with large enough
// Scale so we avoid spamming the log and only use debug here.
JLOG(ctx.j.debug()) //
<< "VaultWithdraw: overflow error with"
<< " scale=" << (int)vault->at(sfScale) //
<< ", assetsTotal=" << vault->at(sfAssetsTotal)
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
<< ", amount=" << amount.value();
return tecPATH_DRY;
}
}
else
{
if (auto const ret = canWithdraw(ctx.view, ctx.tx))
return ret;
}
if (auto const ret = canWithdraw(ctx.view, ctx.tx))
return ret;
// If sending to Account (i.e. not a transfer), we will also create (only
// if authorized) a trust line or MPToken as needed, in doApply().

View File

@@ -49,7 +49,7 @@ static LimitRange constexpr accountOffers = {10, 200, 400};
static LimitRange constexpr accountTx = {10, 200, 400};
/** Limits for the book_offers command. */
static LimitRange constexpr bookOffers = {0, 60, 100};
static LimitRange constexpr bookOffers = {1, 60, 100};
/** Limits for the no_ripple_check command. */
static LimitRange constexpr noRippleCheck = {10, 300, 400};