This commit is contained in:
JCW
2026-08-21 08:27:29 +01:00
parent fa60adb9a6
commit 2f2e306fcd
8 changed files with 2154 additions and 108 deletions

View File

@@ -365,6 +365,11 @@ public:
class ValidPseudoAccounts
{
std::vector<std::string> errors_;
// Live pseudo-account entries touched by the transaction. Populated in
// visitEntry and consumed by finalize to cross-check owner-field
// resolution (e.g. VaultID -> vault whose sfAccount matches the
// pseudo-account).
std::vector<SLE::const_pointer> pseudoAccounts_;
public:
void

View File

@@ -1,14 +1,19 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <map>
#include <unordered_map>
#include <utility>
#include <vector>
namespace xrpl {
@@ -34,24 +39,55 @@ class ValidLoanBroker
};
// Collect all the LoanBrokers found directly or indirectly through
// pseudo-accounts. Key is the brokerID / index. It will be used to find the
// LoanBroker object if brokerBefore and brokerAfter are nullptr
// LoanBroker object if brokerBefore and brokerAfter are nullptr. Populated
// for live (created or modified) brokers only; deletions go to
// deletedBrokers_ below so the live-broker check loop does not have to
// exempt the delete case.
std::map<uint256, BrokerInfo> brokers_;
// Pre-deletion snapshots of brokers erased by this transaction. Deletion
// preconditions (e.g. first-loss capital was returned to the owner) are
// stated positively against this collection, rather than by exempting the
// delete case from the live-broker checks.
std::vector<SLE::const_pointer> deletedBrokers_;
// Collect all the modified trust lines. Their high and low accounts will be
// loaded to look for LoanBroker pseudo-accounts.
std::vector<SLE::const_pointer> lines_;
// Collect all the modified MPTokens. Their accounts will be loaded to look
// for LoanBroker pseudo-accounts.
std::vector<SLE::const_pointer> mpts_;
// Every touched (created or modified) balance-bearing entry, keyed by its
// ledger key. Used by the broker-deletion checks to compute the change in
// the owner's vault-asset balance and verify it matches the returned
// first-loss capital. Deletion-side entries are not stored: their
// after-state balance is zero by construction.
std::unordered_map<uint256, std::pair<SLE::const_pointer, SLE::const_pointer>>
touchedBalances_;
static bool
goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j);
// Return the balance of @p id in @p asset held by @p sle. Handles XRP
// (ACCOUNT_ROOT), IOU (RIPPLE_STATE, sign-flipped per which side is @p id)
// and MPT (MPTOKEN). Returns 0 when @p sle is null, so callers can compute
// a delta uniformly across create / modify / delete transitions.
[[nodiscard]] static Number
balanceOf(SLE::const_ref sle, AccountID const& id, Asset const& asset);
public:
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
// The TER parameter is named because some checks (deltas, deletion
// post-conditions, participant-flow identities) are class-2 and must gate
// on isTesSuccess(result) to avoid firing against the fee-claim-only state
// the framework re-runs in InvariantScope::ProtocolOnly.
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
finalize(
STTx const&,
TER const result,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
} // namespace xrpl

View File

@@ -8,6 +8,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
@@ -36,22 +37,29 @@ namespace xrpl {
* - vault withdrawal and clawback reduce assets and share issuance, and
* subtracts from: total assets, assets available, shares outstanding
* - vault set must not alter the vault assets or shares balance
* - every lending transaction touches exactly one loan: loan set creates one,
* loan manage and loan pay each modify one
* - loan set moves the requested principal out of the vault: it must create
* exactly one loan, and decreases assets available (and the vault balance)
* by the principal
* - loan manage never removes assets from the vault: assets available may only
* grow (and the vault balance grows with it, by the returned first-loss
* capital on a default), and assets outstanding may only shrink (realized
* loss); a loan manage with none of the sub-operation flags (impair,
* capital on a default, which leaves the loan-broker pseudo-account by the
* same amount), and assets outstanding may only shrink (realized loss); loss
* unrealized moves in the direction of the sub-operation (up on impair, down
* on unimpair and on default, as the paper loss is either reversed or
* realized); a loan manage with none of the sub-operation flags (impair,
* unimpair, default) is a no-op and must not modify the vault
* - loan pay adds the paid principal and interest to the vault: assets
* available (and the vault balance) increase by the same amount, which is at
* most the amount paid; the combined inflow to the vault pseudo-account, the
* loan-broker pseudo-account and the loan-broker owner never exceeds the
* amount paid (no value is manufactured); and assets outstanding move in
* lock-step: their change equals the cash received plus the change in the
* paid loan's claim on the vault, which verifies the payment was split
* correctly between principal and interest
* amount paid (no value is manufactured); the vault's claim on the paid loan
* may only shrink; and assets outstanding move in lock-step: their change
* equals the cash received plus the change in the paid loan's claim on the
* vault (the claim being the exposure the vault recognizes under its
* accounting basis), which verifies the payment was split correctly between
* principal and interest
* - shares outstanding may only change through deposit, withdraw, or clawback
* - no vault transaction can change loss unrealized (it's updated by loan
* transactions)
@@ -82,8 +90,12 @@ class ValidVault
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
std::uint32_t flags = 0;
std::uint8_t withdrawalPolicy = 0;
std::uint8_t scale = 0;
// Recognition model (accrual vs. cash-basis) this vault was created
// with; absent sfLEVersion means the legacy, accrual-basis model.
VaultVersion version = VaultVersion::Legacy;
std::optional<std::uint8_t> vaultKind;
std::optional<std::uint32_t> subscriptionDate;
std::optional<std::uint32_t> redemptionDate;
@@ -96,31 +108,86 @@ class ValidVault
MPTIssue share;
std::uint64_t sharesTotal = 0;
std::uint64_t sharesMaximum = 0;
// Static share MPTokenIssuance fields snapshotted for post-creation
// invariants (Items 3, 4). These are all set at VaultCreate time and
// must not drift on any subsequent transaction.
std::uint16_t transferFee = 0;
std::uint8_t assetScale = 0;
std::uint32_t flags = 0;
Shares static make(SLE const&);
};
// Change in an MPToken holder's balance for a single share issuance. The
// holder is preserved so a future check can point at the offending
// account rather than just reporting an aggregate mismatch.
struct ShareHoldingDelta final
{
AccountID holder;
Number delta = kNumZero;
};
struct Loan final
{
uint256 key = beast::kZero;
uint256 loanBrokerID = beast::kZero;
// Borrower of the loan. Snapshotted so the loan-set funding checks can
// verify the principal, net of the origination fee, was credited to
// this account.
AccountID borrower;
// Origination fee routed to the broker owner when the loan is funded.
// Absent on the ledger entry when zero; the snapshot normalizes to
// Number{0} in that case.
Number originationFee = 0;
Number principalOutstanding = 0;
Number totalValueOutstanding = 0;
Number managementFeeOutstanding = 0;
// Whether lsfLoanImpaired was set on the ledger entry. Required by the
// LossUnrealized magnitude checks in finalizeLoanManage, which switch
// on the pre-transaction impairment state of a defaulted loan.
bool impaired = false;
// Interest booked to the vault at loan creation: the portion of the
// total value owed that is neither principal nor broker management fee.
[[nodiscard]] Number
interestDue() const;
// The vault's claim on the loan: the total value owed less the broker's
// management fee (which belongs to the broker, not the vault).
// The vault's claim on the loan, i.e. its exposure to the loan. This is
// accounting-basis dependent: under accrual it is the total value owed
// less the broker's management fee (which belongs to the broker, not
// the vault); under cash-basis, where interest is only recognised once
// received, it is the outstanding principal alone. Mirrors
// loanVaultExposure in LendingHelpers.cpp.
[[nodiscard]] Number
claim() const;
claim(VaultVersion version) const;
// The vault's exposure to the loan, used by the LossUnrealized
// bookkeeping checks. Numerically identical to claim() under either
// basis (see the note above), but named separately so that
// impair / unimpair / default / pay checks read as "exposure" while
// the LoanPay assets-outstanding identity reads as "claim".
[[nodiscard]] Number
exposure(VaultVersion version) const;
Loan static make(SLE const&);
};
// Snapshot of a LoanBroker ledger entry. Populated by visitEntry whenever a
// broker is touched by the transaction, so the lending-side finalizers can
// compute deltas on DebtTotal, CoverAvailable and OwnerCount without a
// separate read of the after-state.
struct Broker final
{
uint256 key = beast::kZero;
AccountID owner;
uint256 vaultID = beast::kZero;
Number debtTotal = 0;
Number coverAvailable = 0;
std::uint32_t ownerCount = 0;
Broker static make(SLE const&);
};
public:
struct DeltaInfo final
{
@@ -136,10 +203,17 @@ private:
std::vector<Vault> afterVault_;
std::vector<Shares> afterMPTs_;
std::vector<Loan> afterLoan_;
std::vector<Broker> afterBroker_;
std::vector<Vault> beforeVault_;
std::vector<Shares> beforeMPTs_;
std::vector<Loan> beforeLoan_;
std::vector<Broker> beforeBroker_;
std::unordered_map<uint256, DeltaInfo> deltas_;
// Per-issuance holder-side share deltas, populated for every touched
// ltMPTOKEN. The universal share-conservation check in finalize sums
// these for the vault's own share issuance and matches the total against
// the issuance's OutstandingAmount delta.
std::unordered_map<uint192, std::vector<ShareHoldingDelta>> shareHoldings_;
/**
* @brief Compute the minimum STAmount scale for rounding invariant
@@ -149,6 +223,12 @@ private:
* @c assetsTotal scale. Pre-amendment it is the coarsest scale across
* @p vaultDelta and both asset-field deltas.
*
* @pre @c afterVault_ is non-empty. Under the pre-amendment branch also
* @c beforeVault_ is non-empty. Both preconditions are asserted at
* runtime and hold for every current caller; the assert catches a
* future reuse (e.g. from a new transactor via @c checkLoanFunding)
* that reaches this helper without a snapshot.
*
* @param vaultDelta Delta of the vault's asset balance for this transaction.
* @param rules Active ledger rules (used to check the amendment).
* @return The minimum scale to apply when rounding vault-related amounts.
@@ -208,21 +288,85 @@ private:
[[nodiscard]] static bool
isVaultEmpty(Vault const& vault);
/**
* @brief Verify that the transaction touched exactly one loan.
*
* Every lending transaction operates on a single loan: @c ttLOAN_SET creates one, while
* @c ttLOAN_MANAGE and @c ttLOAN_PAY each modify one. The per-transaction checks below index
* the loan snapshots directly, so this must hold before they run.
*
* @param isCreate Whether the loan is expected to be created (rather than modified).
* @param j Journal for logging invariant failures.
* @return @c true when exactly one loan was created, respectively modified.
*/
[[nodiscard]] bool
exactlyOneLoan(bool isCreate, beast::Journal const& j) const;
/**
* @brief Creation-side invariants of a loan-origination transaction.
*
* Enforces the phase gate (closed-ended vaults must be in the Investment phase to originate a
* loan; open-ended vaults fall through) and, under @c fixCleanup3_4_0, the exactly-one-loan
* cardinality expected of a creation. Extracted so the checks are callable from any transactor
* that ends up creating a loan (see the eventual @c LoanAccept split); reads the transaction's
* effect on the vault and loan snapshots directly from @c this.
*
* @param tx The transaction being applied.
* @param view Active ledger view (used for rules).
* @param j Journal for logging invariant failures.
* @return @c true when the creation-side invariants hold.
*/
[[nodiscard]] bool
checkLoanCreation(STTx const& tx, ReadView const& view, beast::Journal const& j) const;
/**
* @brief Funding-side invariants of a loan-origination transaction.
*
* Verifies that the transaction moved the requested principal out of the vault
* pseudo-account, and that the created loan records exactly that principal. Under
* @c featureLendingProtocolV1_1 also verifies the participant-side accounting: the broker's
* @c DebtTotal grows by the new loan's exposure (basis-aware), the borrower and broker owner
* receive their respective portions of the principal, and the vault's @c AssetsTotal /
* @c AssetsAvailable / claim identity holds at origination. Assumes @c checkLoanCreation has
* already run and returned @c true (in particular that the loan cardinality is one), so it may
* index @c afterLoan_[0] directly. Extracted so a future @c LoanAccept transactor can reuse
* the funding checks independently of the creation ones.
*
* @param tx The transaction being applied.
* @param fee Fee charged by this transaction; added back when the fee-payer is one
* of the participants whose vault-asset flow we compare.
* @param view Active ledger view (used for rules).
* @param j Journal for logging invariant failures.
* @return @c true when the funding-side invariants hold.
*/
[[nodiscard]] bool
checkLoanFunding(
STTx const& tx,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) const;
/**
* @brief Invariant check for @c ttLOAN_SET.
*
* For a closed-ended vault, a loan may only be originated while the vault is in the Investment
* phase (strictly past @c SubscriptionDate and before @c RedemptionDate). Open-ended vaults (@c
* NoPhase) are unaffected. The complementary maturity bound (final payment strictly precedes @c
* RedemptionDate) is enforced by @c ValidLoan.
* NoPhase) are exempt from the phase gate only; the funding checks apply to every vault kind.
* The complementary maturity bound (final payment strictly precedes @c RedemptionDate) is
* enforced by @c ValidLoan.
*
* @param tx The transaction being applied.
* @param fee Fee charged by this transaction.
* @param view Active ledger view (used for rules).
* @param j Journal for logging invariant failures.
* @return @c true when all @c ttLOAN_SET invariants hold.
*/
[[nodiscard]] bool
finalizeLoanSet(STTx const& tx, ReadView const& view, beast::Journal const& j) const;
finalizeLoanSet(
STTx const& tx,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) const;
/**
* @brief Enforce the invariants specific to a @c ttLOAN_MANAGE