migrate lending helpers

This commit is contained in:
Mayukha Vadari
2026-07-09 22:31:58 -04:00
parent 628272a11e
commit fb9b10fc03
16 changed files with 320 additions and 166 deletions

View File

@@ -33,7 +33,7 @@ namespace xrpl {
[[nodiscard]] TER
canApplyToBrokerCover(
ReadView const& view,
SLE::const_ref sleBroker,
LoanBrokerEntry<ReadView> const& sleBroker,
Asset const& vaultAsset,
STAmount const& amount,
beast::Journal j,
@@ -1632,7 +1632,7 @@ constructLoanState(
}
LoanState
constructRoundedLoanState(SLE::const_ref loan)
constructRoundedLoanState(LoanEntry<ReadView> const& loan)
{
return constructLoanState(
loan->at(sfTotalValueOutstanding),
@@ -1784,8 +1784,8 @@ std::expected<LoanPaymentParts, TER>
loanMakePayment(
Asset const& asset,
ApplyView& view,
SLE::ref loan,
SLE::const_ref brokerSle,
LoanEntry<ApplyView>& loan,
LoanBrokerEntry<ReadView> const& brokerSle,
STAmount const& amount,
LoanPaymentType const paymentType,
beast::Journal j)
@@ -1836,7 +1836,7 @@ loanMakePayment(
XRPL_ASSERT(*totalValueOutstandingProxy > 0, "xrpl::loanMakePayment : valid total value");
view.update(loan);
loan.update();
// -------------------------------------------------------------
// A late payment not flagged as late overrides all other options.

View File

@@ -17,6 +17,7 @@
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/nftPageMask.h>
#include <boost/endian/conversion.hpp>
@@ -32,6 +33,23 @@
namespace xrpl {
// This list should include all of the keylet functions that take a single
// AccountID parameter. Declared in Indexes.h; defined here so the header need
// not include jss.h.
std::array<KeyletDesc<AccountID const&>, 6> const kDirectAccountKeylets{
{{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false},
{.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true},
{.function = &keylet::signerList, .expectedLEName = jss::SignerList, .includeInTests = true},
// It's normally impossible to create an item at nftpage_min, but
// test it anyway, since the invariant checks for it.
{.function = &keylet::nftokenPageMin,
.expectedLEName = jss::NFTokenPage,
.includeInTests = true},
{.function = &keylet::nftokenPageMax,
.expectedLEName = jss::NFTokenPage,
.includeInTests = true},
{.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}};
/** Type-specific prefix for calculating ledger indices.
The identifier for a given object within the ledger is calculated based

View File

@@ -161,7 +161,7 @@ determineClawAmount(
SLE const& sleBroker,
Asset const& vaultAsset,
std::optional<STAmount> const& amount,
SLE::const_ref vaultSle,
VaultEntry<ReadView> const& vaultSle,
Rules const& rules)
{
auto const maxClawAmount = [&]() {
@@ -293,8 +293,8 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
}
}
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount, vault, ctx.view.rules());
auto const findClawAmount = determineClawAmount(
*sleBroker, vaultAsset, amount, VaultEntry<ReadView>{vault, ctx.view}, ctx.view.rules());
if (!findClawAmount)
{
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
@@ -303,7 +303,12 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
STAmount const& clawAmount = *findClawAmount;
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
ctx.view,
LoanBrokerEntry<ReadView>{sleBroker, ctx.view},
vaultAsset,
clawAmount,
ctx.j,
"LoanBrokerCoverClawback"))
return ret;
// Explicitly check the balance of the trust line / MPT to make sure the
@@ -356,8 +361,8 @@ LoanBrokerCoverClawback::doApply()
auto const vaultAsset = vault->at(sfAsset);
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount, vault, view().rules());
auto const findClawAmount = determineClawAmount(
*sleBroker, vaultAsset, amount, VaultEntry<ReadView>{vault, view()}, view().rules());
if (!findClawAmount)
return tecINTERNAL; // LCOV_EXCL_LINE
STAmount const& clawAmount = *findClawAmount;

View File

@@ -96,7 +96,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
// Helper handles both IOU and MPT correctly without explicit branching.
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, amount, ctx.j, "LoanBrokerCoverWithdraw"))
ctx.view,
LoanBrokerEntry<ReadView>{sleBroker, ctx.view},
vaultAsset,
amount,
ctx.j,
"LoanBrokerCoverWithdraw"))
return ret;
// The broker's pseudo-account is the source of funds.
@@ -152,7 +157,9 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
if (fix320Enabled)
{
return minimumBrokerCover(
currentDebtTotal, TenthBips32{sleBroker->at(sfCoverRateMinimum)}, vault);
currentDebtTotal,
TenthBips32{sleBroker->at(sfCoverRateMinimum)},
VaultEntry<ReadView>{vault, ctx.view});
}
// Always round the minimum required up.

View File

@@ -78,7 +78,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
{
// Any remaining debt should have been wiped out by the last Loan
// Delete. This check is purely defensive.
auto const scale = getAssetsTotalScale(vault);
auto const scale = getAssetsTotalScale(VaultEntry<ReadView>{vault, ctx.view});
auto const rounded =
roundToAsset(asset, debtTotal, scale, Number::RoundingMode::TowardsZero);

View File

@@ -121,7 +121,7 @@ LoanDelete::doApply()
roundToAsset(
vaultSle->at(sfAsset),
debtTotalProxy,
getAssetsTotalScale(vaultSle),
getAssetsTotalScale(VaultEntry<ReadView>{vaultSle, view}),
Number::RoundingMode::TowardsZero) == beast::kZero,
"xrpl::LoanDelete::doApply",
"last loan, remaining debt rounds to zero");

View File

@@ -128,7 +128,7 @@ LoanManage::preclaim(PreclaimContext const& ctx)
}
static Number
owedToVault(SLE::ref loanSle)
owedToVault(LoanEntry<ApplyView> const& loanSle)
{
// Spec section 3.2.3.2, defines the default amount as
//
@@ -147,9 +147,9 @@ owedToVault(SLE::ref loanSle)
TER
LoanManage::defaultLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref brokerSle,
SLE::ref vaultSle,
LoanEntry<ApplyView>& loanSle,
LoanBrokerEntry<ApplyView>& brokerSle,
VaultEntry<ApplyView>& vaultSle,
Asset const& vaultAsset,
beast::Journal j)
{
@@ -251,7 +251,7 @@ LoanManage::defaultLoan(
adjustImpreciseNumber(
vaultLossUnrealizedProxy, -totalDefaultAmount, vaultAsset, vaultScale);
}
view.update(vaultSle);
vaultSle.update();
}
// Update the LoanBroker object:
@@ -269,7 +269,7 @@ LoanManage::defaultLoan(
// LCOV_EXCL_STOP
}
coverAvailableProxy -= defaultCovered;
view.update(brokerSle);
brokerSle.update();
}
// Update the Loan object:
@@ -282,7 +282,7 @@ LoanManage::defaultLoan(
// Zero out the next due date. Since it's default, it'll be removed from
// the object.
loanSle->at(sfNextPaymentDueDate) = 0;
view.update(loanSle);
loanSle.update();
// Return funds from the LoanBroker pseudo-account to the
// Vault pseudo-account:
@@ -298,8 +298,8 @@ LoanManage::defaultLoan(
TER
LoanManage::impairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
LoanEntry<ApplyView>& loanSle,
VaultEntry<ApplyView>& vaultSle,
Asset const& vaultAsset,
beast::Journal j)
{
@@ -321,7 +321,7 @@ LoanManage::impairLoan(
"corrupt the vault.";
return tecLIMIT_EXCEEDED;
}
view.update(vaultSle);
vaultSle.update();
// Update the Loan object
loanSle->setFlag(lsfLoanImpaired);
@@ -332,7 +332,7 @@ LoanManage::impairLoan(
// move the next payment due date to now
loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
}
view.update(loanSle);
loanSle.update();
return tesSUCCESS;
}
@@ -340,8 +340,8 @@ LoanManage::impairLoan(
[[nodiscard]] TER
LoanManage::unimpairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
LoanEntry<ApplyView>& loanSle,
VaultEntry<ApplyView>& vaultSle,
Asset const& vaultAsset,
beast::Journal j)
{
@@ -363,7 +363,7 @@ LoanManage::unimpairLoan(
// Reverse the "paper loss"
adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
view.update(vaultSle);
vaultSle.update();
// Update the Loan object
loanSle->clearFlag(lsfLoanImpaired);
@@ -381,7 +381,7 @@ LoanManage::unimpairLoan(
loanSle->at(sfNextPaymentDueDate) =
view.parentCloseTime().time_since_epoch().count() + paymentInterval;
}
view.update(loanSle);
loanSle.update();
return tesSUCCESS;
}
@@ -393,16 +393,16 @@ LoanManage::doApply()
auto& view = ctx_.view();
auto const loanID = tx[sfLoanID];
auto const loanSle = view.peek(keylet::loan(loanID));
LoanEntry<ApplyView> loanSle{keylet::loan(loanID), view};
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
LoanBrokerEntry<ApplyView> brokerSle{keylet::loanBroker(brokerID), view};
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID)));
VaultEntry<ApplyView> vaultSle{keylet::vault(brokerSle->at(sfVaultID)), view};
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultAsset = vaultSle->at(sfAsset);

View File

@@ -287,19 +287,19 @@ LoanPay::doApply()
auto const amount = tx[sfAmount];
auto const loanID = tx[sfLoanID];
auto const loanSle = view.peek(keylet::loan(loanID));
LoanEntry<ApplyView> loanSle{keylet::loan(loanID), view};
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
std::int32_t const loanScale = loanSle->at(sfLoanScale);
auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
LoanBrokerEntry<ApplyView> brokerSle{keylet::loanBroker(brokerID), view};
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerOwner = brokerSle->at(sfOwner);
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
auto const vaultID = brokerSle->at(sfVaultID);
auto const vaultSle = view.peek(keylet::vault(vaultID));
VaultEntry<ApplyView> vaultSle{keylet::vault(vaultID), view};
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudoAccount = vaultSle->at(sfAccount);
@@ -392,7 +392,7 @@ LoanPay::doApply()
// If the payment computation completed without error, the loanSle object
// has been modified.
view.update(loanSle);
loanSle.update();
XRPL_ASSERT_PARTS(
// It is possible to pay 0 principal
@@ -427,7 +427,7 @@ LoanPay::doApply()
//------------------------------------------------------
// LoanBroker object state changes
view.update(brokerSle);
brokerSle.update();
auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable);
auto assetsTotalProxy = vaultSle->at(sfAssetsTotal);
@@ -468,7 +468,7 @@ LoanPay::doApply()
//------------------------------------------------------
// Vault object state changes
view.update(vaultSle);
vaultSle.update();
Number const assetsAvailableBefore = *assetsAvailableProxy;
Number const assetsTotalBefore = *assetsTotalProxy;

View File

@@ -381,7 +381,7 @@ LoanSet::doApply()
if (!brokerOwnerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
VaultEntry<ApplyView> vaultSle{keylet::vault(brokerSle->at(sfVaultID)), view};
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudo = vaultSle->at(sfAccount);
@@ -627,7 +627,7 @@ LoanSet::doApply()
*vaultAvailableProxy <= *vaultTotalProxy,
"xrpl::LoanSet::doApply",
"assets available must not be greater than assets outstanding");
view.update(vaultSle);
vaultSle.update();
// Update the balances in the loan broker
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale);

View File

@@ -29,6 +29,7 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/helpers/SLEWrappers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -139,9 +140,9 @@ class LoanBroker_test : public beast::unit_test::Suite
VaultInfo const& vault,
VaultInfo const& badVault,
std::function<jtx::JTx(jtx::JTx const&)> modifyJTx,
std::function<void(SLE::const_ref)> checkBroker,
std::function<void(SLE::const_ref)> changeBroker,
std::function<void(SLE::const_ref)> checkChangedBroker)
std::function<void(LoanBrokerEntry<ReadView> const&)> checkBroker,
std::function<void(LoanBrokerEntry<ReadView> const&)> changeBroker,
std::function<void(LoanBrokerEntry<ReadView> const&)> checkChangedBroker)
{
{
auto const& asset = vault.asset.raw();
@@ -219,7 +220,7 @@ class LoanBroker_test : public beast::unit_test::Suite
BEAST_EXPECT(broker->at(sfDebtTotal) == 0);
BEAST_EXPECT(broker->at(sfCoverAvailable) == 0);
if (checkBroker)
checkBroker(broker);
checkBroker(LoanBrokerEntry<ReadView>{broker, *env.current()});
// if (auto const vaultSLE = env.le(keylet::vault(vault.vaultID)))
//{
@@ -469,14 +470,14 @@ class LoanBroker_test : public beast::unit_test::Suite
// Make modifications to the broker
if (changeBroker)
changeBroker(broker);
changeBroker(LoanBrokerEntry<ReadView>{broker, *env.current()});
env.close();
// Check the results of modifications
broker = env.le(keylet);
if (BEAST_EXPECT(broker) && checkChangedBroker)
checkChangedBroker(broker);
checkChangedBroker(LoanBrokerEntry<ReadView>{broker, *env.current()});
// Verify that fields get removed when set to default values
// Debt maximum: explicit 0
@@ -722,7 +723,7 @@ class LoanBroker_test : public beast::unit_test::Suite
badVault,
// No modifications
{},
[&](SLE::const_ref broker) {
[&](LoanBrokerEntry<ReadView> const& broker) {
// Extra checks
BEAST_EXPECT(!broker->isFieldPresent(sfManagementFeeRate));
BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateMinimum));
@@ -735,7 +736,7 @@ class LoanBroker_test : public beast::unit_test::Suite
BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount + 4);
},
[&](SLE::const_ref broker) {
[&](LoanBrokerEntry<ReadView> const& broker) {
// Modifications
// Update the fields
@@ -795,7 +796,7 @@ class LoanBroker_test : public beast::unit_test::Suite
kData(testData),
kDebtMaximum(debtMax));
},
[&](SLE::const_ref broker) {
[&](LoanBrokerEntry<ReadView> const& broker) {
// Check the updated fields
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
Number const expected = STAmount{vault.asset, Number(175, -1)};
@@ -826,7 +827,7 @@ class LoanBroker_test : public beast::unit_test::Suite
kCoverRateMinimum(TenthBips32(100)),
kCoverRateLiquidation(TenthBips32(200)));
},
[&](SLE::const_ref broker) {
[&](LoanBrokerEntry<ReadView> const& broker) {
// Extra checks
BEAST_EXPECT(broker->at(sfManagementFeeRate) == 123);
BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 100);
@@ -834,14 +835,14 @@ class LoanBroker_test : public beast::unit_test::Suite
BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(9));
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
},
[&](SLE::const_ref broker) {
[&](LoanBrokerEntry<ReadView> const& broker) {
// Reset Data & Debt maximum to default values
env(set(alice, vault.vaultID),
kLoanBrokerId(broker->key()),
kData(""),
kDebtMaximum(Number(0)));
},
[&](SLE::const_ref broker) {
[&](LoanBrokerEntry<ReadView> const& broker) {
// Check the updated fields
BEAST_EXPECT(!broker->isFieldPresent(sfData));
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));

View File

@@ -211,7 +211,7 @@ protected:
using namespace jtx;
auto const vaultSle = env.le(keylet::vault(vaultID));
return getAssetsTotalScale(vaultSle);
return getAssetsTotalScale(VaultEntry<ReadView>{vaultSle, *env.current()});
}
};
@@ -440,7 +440,8 @@ protected:
env.test.BEAST_EXPECT(loan->at(sfPeriodicPayment) == periodicPayment);
env.test.BEAST_EXPECT(loan->at(sfFlags) == flags);
auto const ls = constructRoundedLoanState(loan);
auto const ls =
constructRoundedLoanState(LoanEntry<ReadView>{loan, *env.current()});
auto const interestRate = TenthBips32{loan->at(sfInterestRate)};
auto const paymentInterval = loan->at(sfPaymentInterval);
@@ -1108,7 +1109,8 @@ protected:
// No reason for this not to exist
return;
}
auto const current = constructRoundedLoanState(loanSle);
auto const current =
constructRoundedLoanState(LoanEntry<ReadView>{loanSle, *env.current()});
auto const errors = nextTrueState - current;
log << currencyLabel << " Loan balances: "
<< "\n\tAmount taken: " << paymentComponents.trackedValueDelta
@@ -6033,7 +6035,8 @@ protected:
auto const loanSle = env.le(loanKeylet);
if (!BEAST_EXPECT(loanSle))
return;
auto const state = constructRoundedLoanState(loanSle);
auto const state =
constructRoundedLoanState(LoanEntry<ReadView>{loanSle, *env.current()});
log << "Loan state:" << std::endl;
log << " ValueOutstanding: " << state.valueOutstanding << std::endl;
@@ -8257,7 +8260,8 @@ protected:
return std::nullopt;
if (!BEAST_EXPECT(tinyLoanSle->at(sfLoanScale) == -12) ||
!BEAST_EXPECT(bigLoanSle->at(sfLoanScale) == -11) ||
!BEAST_EXPECT(getAssetsTotalScale(vaultSle) == -11))
!BEAST_EXPECT(
getAssetsTotalScale(VaultEntry<ReadView>{vaultSle, *env.current()}) == -11))
return std::nullopt;
// Use issuer clawback to reduce cover to the minimum the
@@ -8384,7 +8388,8 @@ protected:
auto const coverAvail = brokerSle->at(sfCoverAvailable);
auto const debtTotal = brokerSle->at(sfDebtTotal);
auto const vaultScale = getAssetsTotalScale(vaultSle);
auto const vaultScale =
getAssetsTotalScale(VaultEntry<ReadView>{vaultSle, *env.current()});
auto const debtScale = scale(debtTotal, asset);
// Sanity: debt scale differs from vault scale for this setup.
@@ -8398,7 +8403,9 @@ protected:
debtScale);
}();
auto const newMin = minimumBrokerCover(
debtTotal, TenthBips32{c.brokerParams.coverRateMin}, vaultSle);
debtTotal,
TenthBips32{c.brokerParams.coverRateMin},
VaultEntry<ReadView>{vaultSle, *env.current()});
// The new (vaultScale) minimum must be strictly larger than
// the old (debtScale) minimum — that is the gap the amendment
@@ -8482,7 +8489,8 @@ protected:
auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
if (!BEAST_EXPECT(vaultSle))
return;
auto const vaultScale = getAssetsTotalScale(vaultSle);
auto const vaultScale =
getAssetsTotalScale(VaultEntry<ReadView>{vaultSle, *env.current()});
BEAST_EXPECT(vaultScale == -11);
// Now try to create a tiny additional loan. Principal is