Skip to content

Conversation

@ximinez
Copy link
Collaborator

@ximinez ximinez commented Dec 1, 2025

High Level Overview of Change

This PR is effectively a clone of #5270 to allow for reviewers to have a "pristine" view of the change set that was merged in commit 6c67f1f.

The content of commits 6c67f1f (in develop) and c953073 (the current state of this branch as of this update) are identical. (Note that I have rewritten the end of the branch since this PR was opened to bring it back into sync. Also note that the branch name kinda sucks.)

The only changes that should be made to this branch are fixes and other changes that are identified as part of a review of this PR.

There is a second PR, #6102, that is intended for any new code changes, bug fixes, and PRs that aren't identified as part of this PR.

Context of Change

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Refactor (non-breaking change that only restructures code)
  • Tests (you added tests for code that already exists, or your new feature included in this PR)

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

ximinez and others added 30 commits September 26, 2025 16:05
- Clarify the purpose of the assert in calculateOwnerReserveFee
- Amendment checking correctness.
- Sync some changes between LoanBrokerCoverWithdraw and CoverDeposit.
- Check for nulls.
- Add documentation.
- Exclude impossible logging from code coverage, too.
…to ximinez/lending-XLS-66

* XRPLF/ximinez/lending-refactoring-4:
  switch `fixIncludeKeyletFields` to `Supported::yes` (5819)
* Write more comments explaining what's going on.
* Rename some variables.
* Do a final safety check for valid values in `LoanPay`.
* XRPLF/develop:
  refactor: Add support for extra transaction signatures (5594)
  refactor: Restructure Transactor::preflight to reduce boilerplate (5592)
- Fixes a bug introduced by PR #5594, commit 550f90a, which
  introduced the concept of a "sigObject", in which the signature is
  checked without necessarily being the transaction.
- Fortunately, no code uses the "sigObject" as anything other than the
  transactor yet, so the bug is harmless for now.
- This fix removes the "PreclaimContext" from the parameter list, and
  adds only the individual parts needed by the function, none of which
  are the transaction.
…inez/lending-XLS-66

* mywork/ximinez/lending-tx-fix:
  fix: Transaction sig checking functions do not get a full context
  ci: Upload artifacts during build and test in a separate job (5817)
- All current items are done
- Mostly comments
- Restructured PaymentParts (formerly PeriodicPaymentParts) to bring
  along fees, and removed the computed / combined PeriodicPayment from
  places that should be using PaymentParts instead.
* XRPLF/develop:
  fix: Transaction sig checking functions do not get a full context (5829)
- Restructures `STTx` signature checking code to be able to handle
  a `sigObject`, which may be the full transaction, or may be an object
  field containing a separate signature. Either way, the `sigObject` can
  be a single- or multi-sign signature.
- This is distinct from 550f90a (#5594), which changed the check in
  Transactor, which validates whether a given account is allowed to sign
  for the given transaction. This cryptographically checks the signature
  validity.
** Vault Invariants for Loan txs need to be fleshed out. **

- Give the appropriate Loan transactions vault modification privileges.
- Give the appropriate Loan transactions the ability to authorize
  (create in this case) MPTokens.
- Check that LoanManage does not leave Vault in an inconsistent state
  (AssetsAvailable > AssetsTotal). For IOU vaults, if the difference is
  dust, "round up".
* XRPLF/develop:
  refactor: Update Conan dependencies: OpenSSL (5873)
  Add vault invariants (5518)
  test: Add more tests for Simulate RPC metadata (5827)
  chore: Fix release build error (5864)
  refactor: Update CI strategy matrix to use new RHEL 9 and RHEL 10 images (5856)
  chore: exclude all `UNREACHABLE` blocks from codecov (5846)
  Set version to 3.0.0-b1 (5859)
@ximinez ximinez requested a review from a team as a code owner December 1, 2025 18:08
@codecov
Copy link

codecov bot commented Dec 1, 2025

Codecov Report

❌ Patch coverage is 90.60250% with 248 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (ximinez/develop-nolending@b772f62). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/xrpld/app/misc/detail/LendingHelpers.cpp 85.6% 79 Missing ⚠️
src/xrpld/app/tx/detail/InvariantCheck.cpp 89.0% 27 Missing ⚠️
src/xrpld/app/tx/detail/LoanManage.cpp 84.9% 27 Missing ⚠️
src/xrpld/app/tx/detail/LoanSet.cpp 91.4% 27 Missing ⚠️
src/xrpld/app/tx/detail/LoanBrokerDelete.cpp 72.5% 25 Missing ⚠️
src/xrpld/rpc/handlers/LedgerEntry.cpp 0.0% 20 Missing ⚠️
src/libxrpl/ledger/View.cpp 94.5% 16 Missing ⚠️
src/xrpld/app/tx/detail/LoanPay.cpp 95.5% 11 Missing ⚠️
...rc/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp 97.8% 3 Missing ⚠️
src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp 94.8% 3 Missing ⚠️
... and 6 more
Additional details and impacted files

Impacted file tree graph

@@                     Coverage Diff                     @@
##             ximinez/develop-nolending   #6093   +/-   ##
===========================================================
  Coverage                             ?   79.1%           
===========================================================
  Files                                ?     839           
  Lines                                ?   71378           
  Branches                             ?    8332           
===========================================================
  Hits                                 ?   56433           
  Misses                               ?   14945           
  Partials                             ?       0           
Files with missing lines Coverage Δ
include/xrpl/basics/Number.h 100.0% <ø> (ø)
include/xrpl/json/json_value.h 98.5% <ø> (ø)
include/xrpl/ledger/ApplyView.h 100.0% <ø> (ø)
include/xrpl/ledger/View.h 100.0% <100.0%> (ø)
include/xrpl/protocol/Asset.h 96.3% <100.0%> (ø)
include/xrpl/protocol/Indexes.h 100.0% <100.0%> (ø)
include/xrpl/protocol/Protocol.h 100.0% <100.0%> (ø)
include/xrpl/protocol/SField.h 100.0% <ø> (ø)
include/xrpl/protocol/STAmount.h 95.7% <100.0%> (ø)
include/xrpl/protocol/STObject.h 93.1% <ø> (ø)
... and 50 more

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines +1620 to +1631
// Use STAmount's internal rounding instead of roundToAsset, because
// we're going to use this result to determine the scale for all the
// other rounding.

// Equation (30) from XLS-66 spec, Section A-2 Equation Glossary
STAmount amount{asset, periodicPayment * paymentsRemaining};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what kind of difference would it make if amount is Number type? What does using STAmount give us - wouldn't STAmount still always use Number to do the normalization since getSTNumberSwitchover() is always true?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what kind of difference would it make if amount is Number type? What does using STAmount give us - wouldn't STAmount still always use Number to do the normalization since getSTNumberSwitchover() is always true?

Rounding. STAmount uses Number, yes, but it uses it in two very different ways depending on whether the asset type represents integers (XRP, MPT) or floating point (IOU). (See STAmount::canonicalize().)

For example, you have an XRP loan with a periodic payment amount of 12.3456 drops and 7 payments. The total value of the loan is 12.3456 * 7 = 86.4192 drops. But drops are integers - you can't have a partial drop. So in this example, amount is rounded to have a value of 86, which is a valid number of drops, and that is used as the initial total value.

Most of the lending-related code uses roundToAsset to do rounding, but that's not appropriate here, because that function needs a loanScale, and we do not yet know what loan scale to use. In fact, the loanScale is based on amount (given the additional restriction of minimumScale). Also, the first step of roundToAsset is to just construct an STAmount exactly like this, so why bother with the overhead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, i see. we'd always need to set the offset to 0 for integer types like MPT or XRP through STAmount.

For example, you have an XRP loan with a periodic payment amount of 12.3456 drops and 7 payments. The total value of the loan is 12.3456 * 7 = 86.4192 drops.

But according to the spec, wouldn't this need to be rounded up to 87 drops? The spec says it always have to be rounded up:

For loans denominated in discrete asset types (XRP drops and MPTs), all monetary values must be rounded > to whole numbers. This rounding requirement means that:

  1. TotalValueOutstanding is always rounded up to the nearest precision unit of an asset. This ensures the borrower pays at least the full theoretical value, preventing the loan from becoming underfunded due to rounding losses.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, i see. we'd always need to set the offset to 0 for integer types like MPT or XRP through STAmount.

This is also correct. STAmount handles integer values differently from Number, and only STAmount is guaranteed to have an exponent of 0 for them.

But according to the spec, wouldn't this need to be rounded up to 87 drops? The spec says it always have to be rounded up:

Good catch! That's a bug just a couple of lines up from here where I set the rounding mode.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! That's a bug just a couple of lines up from here where I set the rounding mode.

Fixed in af43572

- Exclude LogicError lines in ApplyView.cpp (specifically directory
  operations) from code coverage.
- Replace the ability to set the next page on a new directory page with
  an assert, because nothing uses it right now.
- Early return with success for batch inner transactions in preflight2.
…iminez/lending-XLS-66-2

* XRPLF/ximinez/develop-nolending:
  Revert "Implement Lending Protocol (unsupported) (5270)"
  Implement Lending Protocol (unsupported) (5270)
  docs: Update CONTRIBUTING.md for XLS submission guidelines (6065)
  Placeholder
@ximinez ximinez force-pushed the ximinez/lending-XLS-66-2 branch from 335b9be to c953073 Compare December 3, 2025 17:37
@ximinez ximinez changed the title Lending protocol Part 2 - XLS-66 Lending protocol Part 2 for re-review - XLS-66 Dec 3, 2025
// Calculate how the loan's value changed due to the overpayment.
// This should be negative (value decreased) or zero. A principal
// overpayment should never increase the loan's value.
auto const valueChange =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An overpayment decreases the future principal of a loan, thus decreasing the future interest, and the future management fee due on that interest.

The value change here captures the change in value for all components (principal, interest and fee), and is later used to update the Vault assests total.

However, vault assets total does not capture management fee (i.e. it's excluded).

@ximinez , shouldn't the value change here, also exclude the change in management fee? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An overpayment decreases the future principal of a loan, thus decreasing the future interest, and the future management fee due on that interest.

The value change here captures the change in value for all components (principal, interest and fee), and is later used to update the Vault assests total.

However, vault assets total does not capture management fee (i.e. it's excluded).

@ximinez , shouldn't the value change here, also exclude the change in management fee? 🤔

Yes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An overpayment decreases the future principal of a loan, thus decreasing the future interest, and the future management fee due on that interest.

The value change here captures the change in value for all components (principal, interest and fee), and is later used to update the Vault assests total.

However, vault assets total does not capture management fee (i.e. it's excluded).

@ximinez , shouldn't the value change here, also exclude the change in management fee? 🤔

Updated in 947ad00.

- In overpayment results, the management fee was being calculated twice:
  once as part of the value change, and as part of the fees paid.
  Exclude it from the value change.
Copy link
Collaborator

@shawnxie999 shawnxie999 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please feel free to ignore nits and defer any comments for later in this PR - may want to be selective on the ones to address given the time constraints

}

inline int
getVaultScale(SLE::const_ref vaultSle)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this function name is confusing, since there's a Scale field in the vault object. perhaps getAssetsTotalExponent, or something similar?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this function name is confusing, since there's a Scale field in the vault object. perhaps getAssetsTotalExponent, or something similar?

Two things:

  1. I'm going to go with getAssetsTotalScale. Everything else related to this is called "scale", so I'd like to keep it consistent.
  2. This function has a bug in it! It's grabbing the exponent of the Number. Every other scale computation is based on the STAmount exponent. I need to build an STAmount and grab it's exponent.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function has a bug in it! It's grabbing the exponent of the Number. Every other scale computation is based on the STAmount exponent. I need to build an STAmount and grab it's exponent.

I may be lacking some historical context, but is AssetsTotal guaranteed to fit inside a STAmount without loss of precision due to rounding/truncation?

We should probably also check that no rounding/truncation happens during the conversion for safety

Copy link
Collaborator Author

@ximinez ximinez Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things:

  1. I'm going to go with getAssetsTotalScale. Everything else related to this is called "scale", so I'd like to keep it consistent.

  2. This function has a bug in it! It's grabbing the exponent of the Number. Every other scale computation is based on the STAmount exponent. I need to build an STAmount and grab it's exponent.

Fixed in af43572

Comment on lines +1620 to +1631
// Use STAmount's internal rounding instead of roundToAsset, because
// we're going to use this result to determine the scale for all the
// other rounding.

// Equation (30) from XLS-66 spec, Section A-2 Equation Glossary
STAmount amount{asset, periodicPayment * paymentsRemaining};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, i see. we'd always need to set the offset to 0 for integer types like MPT or XRP through STAmount.

For example, you have an XRP loan with a periodic payment amount of 12.3456 drops and 7 payments. The total value of the loan is 12.3456 * 7 = 86.4192 drops.

But according to the spec, wouldn't this need to be rounded up to 87 drops? The spec says it always have to be rounded up:

For loans denominated in discrete asset types (XRP drops and MPTs), all monetary values must be rounded > to whole numbers. This rounding requirement means that:

  1. TotalValueOutstanding is always rounded up to the nearest precision unit of an asset. This ensures the borrower pays at least the full theoretical value, preventing the loan from becoming underfunded due to rounding losses.

Comment on lines +1104 to +1116
[[nodiscard]] TER inline canTransfer(
ReadView const& view,
Asset const& asset,
AccountID const& from,
AccountID const& to)
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) -> TER {
return canTransfer(view, issue, from, to);
},
asset.value());
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be a good place to check for depositPreauth/credentials as well

Copy link
Collaborator Author

@ximinez ximinez Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be a good place to check for depositPreauth/credentials as well

Probably. I'll have to follow up on this, if there isn't already a pending ticket being worked.


// Update the Vault object(set "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
vaultLossUnrealizedProxy += lossUnrealized;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to call adjustImpreciseNumber here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to call adjustImpreciseNumber here?

It probably wouldn't hurt.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to call adjustImpreciseNumber here?

It probably wouldn't hurt.

Fixed in af43572

}

TER
LoanPay::preclaim(PreclaimContext const& ctx)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to check canTranfer and depositpreauth/credentials here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to check canTranfer and depositpreauth/credentials here?

Probably. I'll have to follow up on this, if there isn't already a pending ticket being worked.


// The vault may be at a different scale than the loan. Reduce rounding
// errors during the payment by rounding some of the values to that scale.
auto const vaultScale = assetsTotalProxy.value().exponent();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also be calling getVaultScale and/or convert to STAmount as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also be calling getVaultScale and/or convert to STAmount as well?

Good spotting! I caught and fixed this same thing as part of renaming getVaultScale. I haven't pushed yet, because I broke tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also be calling getVaultScale and/or convert to STAmount as well?

Good spotting! I caught and fixed this same thing as part of renaming getVaultScale. I haven't pushed yet, because I broke tests.

Fixed in af43572

- Round the initial total value computation upward, unless there is
  0-interest.
- Rename getVaultScale to getAssetsTotalScale, and convert one incorrect
  computation to use it.
- Use adjustImpreciseNumber for LossUnrealized.
- Add some logging to computeLoanProperties.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants