mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-16 08:38:27 +00:00
Compare commits
2 Commits
ripple/se/
...
ripple/tok
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce85b2799 | ||
|
|
4538d198e1 |
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
@@ -36,3 +36,18 @@ updates:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: uv
|
||||
directory: /cmake/scripts/codegen
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
time: "04:00"
|
||||
timezone: Etc/GMT
|
||||
commit-message:
|
||||
prefix: "chore: [DEPENDABOT] "
|
||||
target-branch: develop
|
||||
groups:
|
||||
codegen-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
@@ -10,4 +10,4 @@ pcpp>=1.30
|
||||
pyparsing>=3.0.0
|
||||
|
||||
# Template engine - used to generate C++ code from templates
|
||||
Mako>=1.2.2
|
||||
Mako>=1.4.2
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --generate-hashes --output-file requirements.txt
|
||||
mako==1.3.12 \
|
||||
--hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \
|
||||
--hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a
|
||||
mako==1.4.3 \
|
||||
--hash=sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f \
|
||||
--hash=sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a
|
||||
# via -r requirements.in
|
||||
markupsafe==3.0.3 \
|
||||
--hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.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/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -17,6 +19,216 @@ namespace xrpl {
|
||||
|
||||
class STTx;
|
||||
|
||||
/**
|
||||
* @brief Return the Vault's current assets total.
|
||||
*
|
||||
* FixedPrecision Vaults return AssetsAvailable plus AssetsDeployed, with no
|
||||
* rounding to the asset's 16-digit precision: the sum is exact at Number's
|
||||
* active 19-digit mantissa width (the large mantissa range, enabled by
|
||||
* featureSingleAssetVault or featureLendingProtocol), rounds Downward regardless of the
|
||||
* caller's ambient mode beyond that, and is not rounded to match the cached
|
||||
* sfAssetsTotal field. Legacy and CashBasis Vaults return the stored
|
||||
* AssetsTotal.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @return The current assets total.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* @brief A change to a FixedPrecision Vault's balances.
|
||||
*/
|
||||
struct VaultBalanceChange
|
||||
{
|
||||
/**
|
||||
* Signed change to AssetsAvailable. Must be the exact amount moved on the
|
||||
* Vault pseudo-account in the same transaction. Absent means no cash
|
||||
* moved; adjustVaultBalances then uses zero of the Vault's own asset.
|
||||
*/
|
||||
std::optional<STAmount> cash = std::nullopt;
|
||||
|
||||
/**
|
||||
* Signed change to AssetsDeployed: open Loan principal, exact.
|
||||
*/
|
||||
Number deployed = 0;
|
||||
|
||||
/**
|
||||
* Signed change to scheduled, unpaid interest (YieldUnrealized), exact.
|
||||
*/
|
||||
Number yield = 0;
|
||||
|
||||
/**
|
||||
* Signed change to impaired principal (LossUnrealized), exact.
|
||||
*/
|
||||
Number loss = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Apply a balance change to a FixedPrecision Vault. The only writer of
|
||||
* AssetsAvailable, AssetsDeployed, AssetsTotal, YieldUnrealized and
|
||||
* LossUnrealized on these Vaults after creation.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param change The balance change to apply.
|
||||
* @param j Journal for logging the fatal (tefBAD_LEDGER) case.
|
||||
*
|
||||
* @return tesSUCCESS; tecLIMIT_EXCEEDED if LossUnrealized would exceed
|
||||
* AssetsDeployed; tefBAD_LEDGER if AssetsAvailable, AssetsDeployed,
|
||||
* LossUnrealized or YieldUnrealized would become negative.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j);
|
||||
|
||||
/**
|
||||
* Return the Vault's current live exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use the exponent of AssetsTotal. FixedPrecision
|
||||
* Vaults use max(base exponent, exponent of AssetsTotal): the grid is never
|
||||
* finer than -Scale and coarsens once AssetsTotal outgrows 16 digits there.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Return the Vault's base exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use their current live exponent. FixedPrecision
|
||||
* Vaults use -Scale, or 0 for integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Round an amount at the Vault's posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Return the Vault's posterior live exponent after applying an unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Shared grid-floor core of getVaultScale / getPosteriorVaultScale /
|
||||
* getPosteriorBrokerCoverScale: baseScale when reference is zero, otherwise
|
||||
* max(baseScale, scale(reference, asset)), rounded Downward regardless of
|
||||
* the caller's ambient rounding mode.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale);
|
||||
|
||||
/**
|
||||
* Shared core of getPosteriorVaultScale and getPosteriorBrokerCoverScale:
|
||||
* the exponent reference + delta would have under version's live-scale rule
|
||||
* (floored at baseScale for FixedPrecision, unbounded for Legacy/CashBasis).
|
||||
*
|
||||
* @param version The Vault's LEVersion.
|
||||
* @param asset The Vault's underlying asset.
|
||||
* @param baseScale The Vault's base exponent; unused outside the
|
||||
* FixedPrecision case.
|
||||
* @param reference The balance the delta is applied to (AssetsAvailable,
|
||||
* AssetsTotal, or a LoanBroker's CoverAvailable).
|
||||
* @param delta The unrounded change to reference.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta);
|
||||
|
||||
/**
|
||||
* Shared core of creditToPosteriorAvailableScale and
|
||||
* creditToPosteriorBrokerCoverScale: floors reference + raw at scale and
|
||||
* returns the difference from reference, so the delta applied is exactly
|
||||
* what moves reference onto the floored sum. See
|
||||
* creditToPosteriorAvailableScale's doc for why the sum, not raw alone,
|
||||
* must be floored. raw is the exact amount; it is not rounded on its own
|
||||
* before being added to reference.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int atScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Round a FixedPrecision cash inflow (LoanPay's credit, LoanManage's
|
||||
* default cover credit) into AssetsAvailable, never AssetsTotal, which is
|
||||
* a derived cache, by flooring the sum rather than the delta:
|
||||
* credit equals floor16(AssetsAvailable + raw) minus AssetsAvailable, never
|
||||
* finer than the Vault's base exponent (-Scale).
|
||||
*
|
||||
* Flooring only the delta at its own posterior grid is not enough: the sum
|
||||
* can still need a 17th digit when AssetsAvailable is off the coarser grid
|
||||
* right after crossing a power of ten. Example at Scale 6: AssetsAvailable
|
||||
* equals 9999999999.999999 (16 digits already) and a credit of 0.000011
|
||||
* floors to 0.00001 on its own (finer) grid, but 9999999999.999999 plus
|
||||
* 0.00001 equals 10000000000.000009, which still needs 17 digits. Flooring
|
||||
* the sum instead, floor16(9999999999.999999 + 0.000011) minus
|
||||
* 9999999999.999999, is exact by construction, since STAmount's own
|
||||
* 16-digit canonical form of the sum is what gets subtracted from.
|
||||
*
|
||||
* raw is taken as an exact Number: rounding it to 16 digits before adding
|
||||
* it to AssetsAvailable could drop a tail that moves the floored sum.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Open-zone capacity ceiling: 9 * 10^(15 + baseScale).
|
||||
*
|
||||
* Defined only for FixedPrecision Vaults, where this is 9 * 10^(15 - P).
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Check whether amount is an admissible optional inflow.
|
||||
*
|
||||
* Legacy and CashBasis Vaults always succeed. FixedPrecision Vaults must
|
||||
* remain at their base scale after applying the rounded amount, and the
|
||||
* posterior capacity (AssetsTotal + YieldUnrealized + rounded amount) must
|
||||
* stay within the Open zone.
|
||||
*
|
||||
* The amount is rounded toward zero at the posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Open-zone capacity after adding roundedAmount: AssetsTotal + YieldUnrealized
|
||||
* + roundedAmount, computed TowardsZero regardless of the caller's ambient
|
||||
* rounding mode. Defined only for FixedPrecision Vaults.
|
||||
*
|
||||
* Shared by checkOptionalVaultInflow and LoanSet's loan-origination capacity
|
||||
* check, which pair this formula with different scale checks (posterior vs.
|
||||
* current), so only the capacity formula itself -- not the whole check -- is
|
||||
* shared here.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param roundedAmount The amount to add, already rounded to the relevant
|
||||
* posterior or base scale by the caller.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount);
|
||||
|
||||
/**
|
||||
* From the perspective of a vault, return the number of shares to give
|
||||
* depositor when they offer a fixed amount of assets. Note, since shares are
|
||||
@@ -46,31 +258,81 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& shares);
|
||||
|
||||
/**
|
||||
* Adjusts a requested asset change (`delta`) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures `sfAssetsTotal`, `sfAssetsAvailable`,
|
||||
* Adjusts a requested asset change (delta) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures sfAssetsTotal, sfAssetsAvailable,
|
||||
* and the actual asset transfer change by the exact same representable amount.
|
||||
*
|
||||
* Rounding strategy:
|
||||
* - Debits (withdrawals): Rounds down `|delta|` on the new scale to prevent
|
||||
* paying out more than requested.
|
||||
* - Credits (deposits): Floors the resulting total asset balance and returns the
|
||||
* difference from the current total. This prevents crediting the vault with
|
||||
* more assets than the user deposited.
|
||||
* - Legacy/CashBasis debits (withdrawals): rounds down the magnitude of delta
|
||||
* on the new scale to prevent paying out more than requested.
|
||||
* - Legacy/CashBasis credits: floors the resulting total asset balance and
|
||||
* returns the difference from the current total.
|
||||
* - FixedPrecision credits only (VaultDeposit): rounds the delta toward zero
|
||||
* at the scale getAssetsTotal (AssetsAvailable + AssetsDeployed, derived,
|
||||
* not the stored AssetsTotal cache) would have after applying delta.
|
||||
* FixedPrecision outflows (VaultWithdraw, VaultClawback) never call this;
|
||||
* they use clampVaultOutflow instead.
|
||||
*
|
||||
* Key rules:
|
||||
* - The returned magnitude never exceeds `|delta|`.
|
||||
* - Returns `tecPRECISION_LOSS` if the change is smaller than 1 ULP of the target scale
|
||||
* - The returned magnitude never exceeds the magnitude of delta.
|
||||
* - Returns tecPRECISION_LOSS if the change is smaller than 1 ULP of the target scale
|
||||
* (prevents share operations when totals cannot change).
|
||||
* - For integer assets (XRP, MPT), rounding is a no-op.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed change to sfAssetsTotal.
|
||||
* @return The rounded, positive magnitude, or `tecPRECISION_LOSS` if the
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Outflow clamp dispatcher for VaultWithdraw and VaultClawback: rounds a
|
||||
* negative delta to the scale appropriate for the Vault's version -- for
|
||||
* FixedPrecision, at AssetsAvailable's own posterior scale (the balance the
|
||||
* cash actually leaves); for Legacy/CashBasis, clampToAssetsTotalScale --
|
||||
* so both call sites share one dispatch point. Same tecPRECISION_LOSS and
|
||||
* integral-asset and magnitude-never-exceeds-delta rules as
|
||||
* clampToAssetsTotalScale.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed outflow (negative).
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Returns the Vault's base exponent for asset at scale, before a Vault
|
||||
* object exists to read it from: 0 for an integral asset, -scale otherwise.
|
||||
* Used by VaultCreate::preclaim, which only has the proposed Scale field,
|
||||
* not yet a Vault SLE; getVaultBaseScale is the post-creation equivalent.
|
||||
*
|
||||
* @param asset The (prospective) Vault's underlying asset.
|
||||
* @param scale The (prospective) Vault's sfScale value, ignored for
|
||||
* integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale);
|
||||
|
||||
/**
|
||||
* Returns true iff value is exactly representable both as an STAmount of
|
||||
* asset and on the base grid at baseScale (no precision lost rounding
|
||||
* TowardsZero at baseScale). Returns false, rather than throwing, for a value
|
||||
* out of range for the asset. Shared representability check for
|
||||
* VaultCreate::preclaim, VaultSet::preclaim and LoanBrokerSet::preclaim's
|
||||
* AssetsMaximum / DebtMaximum / cover-rate field checks.
|
||||
*
|
||||
* @param asset The vault's underlying asset.
|
||||
* @param value The value to check.
|
||||
* @param baseScale The vault's base exponent (see vaultBaseScale /
|
||||
* getVaultBaseScale).
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale);
|
||||
|
||||
/**
|
||||
* Controls whether to truncate shares instead of rounding.
|
||||
*/
|
||||
@@ -98,9 +360,9 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
|
||||
|
||||
/**
|
||||
* Returns true if debiting `amount` from `total` (the current value of a
|
||||
* Returns true if debiting amount from total (the current value of a
|
||||
* vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the
|
||||
* same STAmount value. This happens when `amount` is non-zero but too small
|
||||
* same STAmount value. This happens when amount is non-zero but too small
|
||||
* to change the stored total at STAmount's precision. Shares would still
|
||||
* move, so the ValidVault invariant would fail after apply; callers use
|
||||
* this to reject the transaction upfront instead.
|
||||
@@ -114,6 +376,20 @@ assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
|
||||
|
||||
/**
|
||||
* Returns the reference balance a cash outflow's dust check
|
||||
* (debitIsNonZeroDust) should compare against: FixedPrecision Vaults use
|
||||
* AssetsAvailable, the balance the cash actually leaves; Legacy/CashBasis
|
||||
* Vaults use the given AssetsTotal. Shared by VaultWithdraw and
|
||||
* VaultClawback.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param assetsTotal The vault's current AssetsTotal, as already read by the
|
||||
* caller.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal);
|
||||
|
||||
/**
|
||||
* From the perspective of a vault, return the number of shares to demand from
|
||||
* the depositor when they ask to withdraw a fixed amount of assets. Since
|
||||
@@ -158,7 +434,7 @@ sharesToAssetsWithdraw(
|
||||
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
|
||||
|
||||
/**
|
||||
* Returns true iff `account` holds all of the vault's outstanding shares —
|
||||
* Returns true iff account holds all of the vault's outstanding shares,
|
||||
* i.e. is the sole remaining shareholder. Returns false if the account
|
||||
* holds no shares or fewer than the total outstanding.
|
||||
*
|
||||
@@ -171,11 +447,13 @@ sharesToAssetsWithdraw(
|
||||
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef issuance);
|
||||
|
||||
/**
|
||||
* Resolves a Vault's LEVersion, the single point every accounting touch
|
||||
* point should call to determine which recognition model (instant interest
|
||||
* recognition vs. cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1
|
||||
* activated never have sfLEVersion set, which resolves here to
|
||||
* VaultVersion::Legacy.
|
||||
* Resolves a Vault's LEVersion.
|
||||
*
|
||||
* LEVersion is the single point every accounting and rounding helper
|
||||
* should call to decide which protocol a Vault follows. It is written
|
||||
* at VaultCreate and is not updated afterwards, so a Vault created
|
||||
* under an older amendment keeps that behaviour after later amendments
|
||||
* activate. Absent sfLEVersion resolves to VaultVersion::Legacy.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
*
|
||||
@@ -185,6 +463,33 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Decodes an already-extracted sfLEVersion value with the same range check as
|
||||
* getVaultVersion. Usable from contexts, such as an invariant's ledger-entry
|
||||
* snapshot, that keep the field value but not the owning SLE.
|
||||
*
|
||||
* @param leVersion The value of sfLEVersion, or nullopt if absent.
|
||||
*
|
||||
* @return The decoded LEVersion, or VaultVersion::Legacy if absent.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion);
|
||||
|
||||
/**
|
||||
* Resolves which LEVersion a newly-created Vault should get under the
|
||||
* currently active amendments. This is the only place that decides Vault
|
||||
* version policy; VaultCreate calls it instead of checking amendments
|
||||
* directly.
|
||||
*
|
||||
* @param rules The active ledger rules.
|
||||
*
|
||||
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_2,
|
||||
* fixCleanup3_2_0 and fixCleanup3_4_0 are all enabled; VaultVersion::CashBasis
|
||||
* once featureLendingProtocolV1_1 is enabled; VaultVersion::Legacy otherwise.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultDefaultVersion(Rules const& rules);
|
||||
|
||||
/**
|
||||
* Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when
|
||||
* sfVaultKind is present and equal to that value; anything else (including an
|
||||
|
||||
@@ -311,24 +311,46 @@ constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1;
|
||||
* Default IOU scale factor for a Vault
|
||||
*/
|
||||
constexpr std::uint8_t kVaultDefaultIouScale = 6;
|
||||
|
||||
/**
|
||||
* Maximum scale factor for a Vault. The number is chosen to ensure that
|
||||
* 1 IOU can be always converted to shares.
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18
|
||||
* Maximum Scale for a Vault created before featureLendingProtocolV1_2.
|
||||
* Chosen so 1 IOU can always convert to shares:
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18.
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumIouScale = 18;
|
||||
|
||||
/**
|
||||
* Vault ledger-entry schema versions. Assigned to newly created
|
||||
* Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before
|
||||
* activation are left without LEVersion (implicit legacy version 0,
|
||||
* instant interest recognition).
|
||||
* Maximum Scale for a Vault created under featureLendingProtocolV1_2.
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumFixedPrecisionIouScale = 10;
|
||||
|
||||
/**
|
||||
* Vault ledger-entry schema versions, persisted as sfLEVersion.
|
||||
*
|
||||
* LEVersion records which protocol a Vault was created under so later
|
||||
* amendments can change the rules for new Vaults without rewriting
|
||||
* existing ones. VaultCreate writes it from the then-active lending
|
||||
* amendments; later transactions do not update it. A Vault created
|
||||
* under an older amendment keeps that amendment's behaviour for its
|
||||
* lifetime, even after a newer lending amendment activates.
|
||||
*
|
||||
* Absent sfLEVersion is implicit Legacy (version 0): instant interest
|
||||
* recognition and a dynamic AssetsTotal scale. CashBasis (V1.1) uses
|
||||
* cash-basis recognition on the same dynamic scale. FixedPrecision
|
||||
* (V1.2) keeps cash-basis recognition and adds the lifetime base grid.
|
||||
*/
|
||||
enum class VaultVersion : uint8_t {
|
||||
Legacy = 0,
|
||||
CashBasis,
|
||||
FixedPrecision,
|
||||
};
|
||||
|
||||
// Code compares VaultVersion values with < and >= (e.g. "CashBasis or later"),
|
||||
// so each later version must stay numerically larger than the one before.
|
||||
static_assert(
|
||||
VaultVersion::Legacy < VaultVersion::CashBasis &&
|
||||
VaultVersion::CashBasis < VaultVersion::FixedPrecision);
|
||||
|
||||
/**
|
||||
* Vault kind. Distinguishes closed-ended vaults from the default open-ended
|
||||
* kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
|
||||
// Requires LendingProtocolV1_1. New vaults take FixedPrecision plus cash-basis.
|
||||
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -514,6 +514,8 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfAssetsAvailable, SoeDefault},
|
||||
{sfAssetsMaximum, SoeDefault},
|
||||
{sfLossUnrealized, SoeDefault},
|
||||
{sfYieldUnrealized, SoeDefault},
|
||||
{sfAssetsDeployed, SoeDefault},
|
||||
{sfShareMPTID, SoeRequired},
|
||||
{sfWithdrawalPolicy, SoeRequired},
|
||||
{sfScale, SoeDefault},
|
||||
|
||||
@@ -240,6 +240,8 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
|
||||
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
|
||||
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfYieldUnrealized, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfAssetsDeployed, NUMBER, 19, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
|
||||
// 32-bit signed (common)
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
|
||||
@@ -242,6 +242,54 @@ public:
|
||||
return this->sle_->isFieldPresent(sfLossUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfYieldUnrealized (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getYieldUnrealized() const
|
||||
{
|
||||
if (hasYieldUnrealized())
|
||||
return this->sle_->at(sfYieldUnrealized);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfYieldUnrealized is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasYieldUnrealized() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfYieldUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAssetsDeployed (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getAssetsDeployed() const
|
||||
{
|
||||
if (hasAssetsDeployed())
|
||||
return this->sle_->at(sfAssetsDeployed);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAssetsDeployed is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAssetsDeployed() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfShareMPTID (SoeRequired)
|
||||
* @return The field value.
|
||||
@@ -571,6 +619,28 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfYieldUnrealized (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setYieldUnrealized(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfYieldUnrealized] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAssetsDeployed (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setAssetsDeployed(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAssetsDeployed] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfShareMPTID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -17,6 +21,7 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
@@ -24,6 +29,405 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int
|
||||
fixedBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::fixedBaseScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
// sfScale is never written for an integral asset (VaultCreate only sets
|
||||
// it when scale != 0, and integral assets always get scale 0), so it
|
||||
// must not be read here; vaultBaseScale ignores the scale argument for
|
||||
// an integral asset anyway.
|
||||
if (asset.integral())
|
||||
return 0;
|
||||
return vaultBaseScale(asset, vault->at(sfScale));
|
||||
}
|
||||
|
||||
// Thin, vault-specific wrapper over posteriorAssetScale: used by
|
||||
// getPosteriorVaultScale and creditToPosteriorAvailableScale.
|
||||
[[nodiscard]] int
|
||||
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
|
||||
{
|
||||
return detail::posteriorAssetScale(
|
||||
getVaultVersion(vault), vault->at(sfAsset), fixedBaseScale(vault), reference, delta);
|
||||
}
|
||||
|
||||
// FixedPrecision-only counterpart to clampToAssetsTotalScale, for cash
|
||||
// outflows (VaultWithdraw, VaultClawback): rounds the magnitude of delta
|
||||
// toward zero at AssetsAvailable's own posterior scale, the balance the cash
|
||||
// actually leaves, instead of the derived AssetsTotal cache. Same
|
||||
// tecPRECISION_LOSS and integral-asset and magnitude-never-exceeds-delta
|
||||
// rules as clampToAssetsTotalScale. Not part of the public interface; reached
|
||||
// only through clampVaultOutflow.
|
||||
//
|
||||
// For an outflow it is safe to round the delta directly, unlike the credit
|
||||
// case: amount on the posterior grid of AssetsAvailable always leaves
|
||||
// AssetsAvailable minus amount representable, since the posterior grid is
|
||||
// exactly the grid AssetsAvailable itself will canonicalize to after the
|
||||
// subtraction.
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAvailableScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::clampToAvailableScale : delta and vault asset match");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::clampToAvailableScale : FixedPrecision Vault");
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampToAvailableScale : outflow delta is negative");
|
||||
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount magnitude = delta.negative() ? -delta : delta;
|
||||
if (asset.integral())
|
||||
return magnitude;
|
||||
|
||||
STAmount const rounded = roundToScale(
|
||||
delta,
|
||||
posteriorScale(vault, vault->at(sfAssetsAvailable), delta),
|
||||
Number::RoundingMode::TowardsZero);
|
||||
STAmount actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
|
||||
XRPL_ASSERT(
|
||||
abs(actualDelta) <= abs(delta),
|
||||
"xrpl::clampToAvailableScale : actual delta smaller or equal to calculated delta");
|
||||
|
||||
// Reject changes below scale precision (1 ULP) to prevent share balance changes
|
||||
// without corresponding asset movements.
|
||||
if (actualDelta <= beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
// Write cached AssetsTotal from getAssetsTotal, as the exact total rounded
|
||||
// Downward to the Vault asset's 16-digit STAmount representation. Below
|
||||
// coarsening this equals the exact total; above it, the cache is a
|
||||
// deliberate floor of the exact value. Internal to adjustVaultBalances,
|
||||
// the only writer of AssetsAvailable, AssetsDeployed, AssetsTotal,
|
||||
// YieldUnrealized and LossUnrealized on a FixedPrecision Vault after
|
||||
// creation; not part of the public interface.
|
||||
void
|
||||
syncAssetsTotal(SLE::Ref vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::syncAssetsTotal : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
|
||||
// adjustVaultBalances writes AssetsAvailable as an already-canonical
|
||||
// STAmount before calling this, so the associateAsset pass a transactor
|
||||
// runs afterwards is always a no-op. If this fires, AssetsAvailable was
|
||||
// written some other way.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, vault->at(sfAssetsAvailable)} == vault->at(sfAssetsAvailable)),
|
||||
"xrpl::syncAssetsTotal : AssetsAvailable is a 16-digit STAmount value");
|
||||
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
vault->at(sfAssetsTotal) = STAmount{asset, getAssetsTotal(vault)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace detail {
|
||||
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale)
|
||||
{
|
||||
if (reference == beast::kZero)
|
||||
return baseScale;
|
||||
// Round Downward, not the ambient mode: the live exponent must match the
|
||||
// exponent syncAssetsTotal actually stores (also Downward), regardless of
|
||||
// what rounding mode the caller's own arithmetic is using.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return std::max(baseScale, scale(reference, asset));
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta)
|
||||
{
|
||||
// ToNearest for the sum and for scale()'s canonicalization of it, as the
|
||||
// Legacy/CashBasis clamp always did; liveScale sets its own mode.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
Number const posterior = reference + delta;
|
||||
|
||||
switch (version)
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(posterior, asset);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return liveScale(posterior, asset, baseScale);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::detail::posteriorAssetScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int atScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
// Floor the SUM (reference + raw), not just raw, at atScale. See
|
||||
// creditToPosteriorAvailableScale's doc: a delta floored on its own grid
|
||||
// can still leave a 17-digit sum once the reference has crossed a power
|
||||
// of ten.
|
||||
//
|
||||
// Both reference + raw and flooredSum - reference can carry more digits
|
||||
// than Number holds (the sum is on the posterior grid, the reference on
|
||||
// the finer one). Compute both under roundingMode, not the caller's
|
||||
// ambient mode, so the credit this returns never exceeds raw.
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
Number const flooredSum = roundToAsset(asset, reference + raw, atScale, roundingMode);
|
||||
return STAmount{asset, flooredSum - reference};
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::getPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::detail::getPosteriorVaultScale : delta and Vault asset match");
|
||||
return posteriorScale(vault, getAssetsTotal(vault), delta);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getAssetsTotal : valid Vault sle");
|
||||
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return vault->at(sfAssetsTotal);
|
||||
|
||||
// AssetsAvailable is at -Scale (or coarser) and AssetsDeployed is always at
|
||||
// -Scale, so the sum is exact while it fits 19 digits. Fix the mode so a
|
||||
// sum that does not fit rounds the same way for every caller.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return vault->at(sfAssetsAvailable) + vault->at(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::adjustVaultBalances : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::adjustVaultBalances : FixedPrecision Vault");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount const cash = change.cash.value_or(STAmount{asset, 0});
|
||||
XRPL_ASSERT(cash.asset() == asset, "xrpl::adjustVaultBalances : cash and Vault asset match");
|
||||
// This is the mode the ledger's own transfers (trust line / MPT balance
|
||||
// updates) normalize in; computing AA' with any other ambient mode would
|
||||
// not reproduce what the real transfer just did to the backing balance.
|
||||
XRPL_ASSERT(
|
||||
Number::getround() == Number::RoundingMode::ToNearest,
|
||||
"xrpl::adjustVaultBalances : ambient rounding mode is ToNearest");
|
||||
|
||||
// AA' is the exact trust-line/MPT arithmetic the ledger uses for the
|
||||
// transfer itself: a Number sum re-canonicalized to the asset's 16-digit
|
||||
// STAmount precision. MPT and XRP sums are already integral, so this is
|
||||
// a no-op rounding for them.
|
||||
//
|
||||
// AssetsDeployed, LossUnrealized and YieldUnrealized move no cash, so they stay
|
||||
// exact Number sums; the asserts below check each fits 16 digits, rather
|
||||
// than letting an STAmount conversion round it silently.
|
||||
STAmount const availableAfter{asset, Number(vault->at(sfAssetsAvailable)) + Number(cash)};
|
||||
Number const assetsDeployedAfter = Number(vault->at(sfAssetsDeployed)) + change.deployed;
|
||||
Number const lossUnrealizedAfter = Number(vault->at(sfLossUnrealized)) + change.loss;
|
||||
// LoanSet, LoanPay and default each move YieldUnrealized by the change in
|
||||
// the Loan's own TotalValueOutstanding - PrincipalOutstanding -
|
||||
// ManagementFeeOutstanding, so it always equals that sum over the
|
||||
// Vault's Loans and cannot go negative.
|
||||
Number const yieldUnrealizedAfter = Number(vault->at(sfYieldUnrealized)) + change.yield;
|
||||
|
||||
if (availableAfter < beast::kZero || assetsDeployedAfter < beast::kZero ||
|
||||
lossUnrealizedAfter < beast::kZero || yieldUnrealizedAfter < beast::kZero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "adjustVaultBalances: a balance would become negative."
|
||||
<< " AssetsAvailable: " << Number(availableAfter)
|
||||
<< ", AssetsDeployed: " << assetsDeployedAfter
|
||||
<< ", LossUnrealized: " << lossUnrealizedAfter
|
||||
<< ", YieldUnrealized: " << yieldUnrealizedAfter;
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// The impairment rule: a Loan can only be impaired for as much as it
|
||||
// still owes. Reachable only through LoanManage's impair.
|
||||
if (lossUnrealizedAfter > assetsDeployedAfter)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
// Every caller derives principal from a Loan's own PrincipalOutstanding so the new total must
|
||||
// stay representable at the asset's own 16-digit precision too.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, assetsDeployedAfter} == assetsDeployedAfter),
|
||||
"xrpl::adjustVaultBalances : AssetsDeployed is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, lossUnrealizedAfter} == lossUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : LossUnrealized is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, yieldUnrealizedAfter} == yieldUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : YieldUnrealized is a 16-digit STAmount value");
|
||||
|
||||
vault->at(sfAssetsAvailable) = availableAfter;
|
||||
vault->at(sfAssetsDeployed) = assetsDeployedAfter;
|
||||
vault->at(sfLossUnrealized) = lossUnrealizedAfter;
|
||||
vault->at(sfYieldUnrealized) = yieldUnrealizedAfter;
|
||||
syncAssetsTotal(vault);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultScale : valid Vault sle");
|
||||
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(assetsTotal, vault->at(sfAsset));
|
||||
case VaultVersion::FixedPrecision:
|
||||
return detail::liveScale(assetsTotal, vault->at(sfAsset), fixedBaseScale(vault));
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultBaseScale : valid Vault sle");
|
||||
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return getVaultScale(vault);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return fixedBaseScale(vault);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultBaseScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::roundToPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::roundToPosteriorVaultScale : amount and Vault asset match");
|
||||
if (amount.integral())
|
||||
return amount;
|
||||
return roundToScale(amount, detail::getPosteriorVaultScale(vault, amount), roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::creditToPosteriorAvailableScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
if (asset.integral())
|
||||
{
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
return STAmount{asset, raw};
|
||||
}
|
||||
|
||||
Number const reference = vault->at(sfAssetsAvailable);
|
||||
int const scale = posteriorScale(vault, reference, raw);
|
||||
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultOpenLimit : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::getVaultOpenLimit : FixedPrecision Vault");
|
||||
return Number{9, 15 + getVaultBaseScale(vault)};
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::checkOptionalVaultInflow : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::checkOptionalVaultInflow : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalVaultInflow : non-negative amount");
|
||||
if (amount.negative())
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return tesSUCCESS;
|
||||
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero);
|
||||
int const baseScale = getVaultBaseScale(vault);
|
||||
// Keep this explicit even though a non-negative YieldUnrealized makes the
|
||||
// Open-zone capacity ceiling reject every coarsening transition too. The
|
||||
// protocol defines both conditions independently.
|
||||
if (detail::getPosteriorVaultScale(vault, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
if (vaultOpenZoneCapacity(vault, rounded) > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::vaultOpenZoneCapacity : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::vaultOpenZoneCapacity : FixedPrecision Vault");
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return getAssetsTotal(vault) + vault->at(sfYieldUnrealized) + roundedAmount;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& assets)
|
||||
{
|
||||
@@ -34,7 +438,7 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (assets.negative() || assets.asset() != vault->at(sfAsset))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
STAmount shares{vault->at(sfShareMPTID)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -58,7 +462,7 @@ sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
STAmount assets{vault->at(sfAsset)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -85,37 +489,52 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
return magnitude;
|
||||
}
|
||||
Number const assetsTotal = vault->at(sfAssetsTotal);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
}();
|
||||
|
||||
STAmount actualDelta;
|
||||
if (delta.negative())
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
// FixedPrecision only reaches this branch for credits (VaultDeposit);
|
||||
// outflows go through clampVaultOutflow -> clampToAvailableScale,
|
||||
// which rounds at AssetsAvailable's own posterior scale instead.
|
||||
XRPL_ASSERT(
|
||||
!delta.negative(), "xrpl::clampToAssetsTotalScale : FixedPrecision credit only");
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero);
|
||||
actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
}();
|
||||
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
if (delta.negative())
|
||||
{
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
}();
|
||||
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
}
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
@@ -130,10 +549,36 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampVaultOutflow : outflow delta is negative");
|
||||
return getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? clampToAvailableScale(vault, delta)
|
||||
: clampToAssetsTotalScale(vault, delta);
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale)
|
||||
{
|
||||
if (asset.integral())
|
||||
return 0;
|
||||
return -static_cast<int>(scale);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale)
|
||||
{
|
||||
// isRepresentable first: building an STAmount from an out-of-range value
|
||||
// throws, and this checks user-supplied values in preclaim.
|
||||
return isRepresentable(asset, value) && STAmount{asset, value} == value &&
|
||||
roundToAsset(asset, value, baseScale, Number::RoundingMode::TowardsZero) == value;
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
|
||||
{
|
||||
Number assetTotal = vault->at(sfAssetsTotal);
|
||||
Number assetTotal = getAssetsTotal(vault);
|
||||
if (waive == WaiveUnrealizedLoss::No)
|
||||
assetTotal -= vault->at(sfLossUnrealized);
|
||||
return assetTotal;
|
||||
@@ -147,6 +592,14 @@ debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount
|
||||
return STAmount{asset, total - amount} == STAmount{asset, total};
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal)
|
||||
{
|
||||
return getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? Number(vault->at(sfAssetsAvailable))
|
||||
: assetsTotal;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesWithdraw(
|
||||
SLE::ConstRef vault,
|
||||
@@ -218,34 +671,39 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault)
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
|
||||
if (!vault->isFieldPresent(sfLEVersion))
|
||||
if (!leVersion)
|
||||
return VaultVersion::Legacy;
|
||||
|
||||
auto const version = vault->at(sfLEVersion);
|
||||
if (version > std::to_underlying(VaultVersion::CashBasis))
|
||||
if (*leVersion > std::to_underlying(VaultVersion::FixedPrecision))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultVersion : invalid vault version");
|
||||
UNREACHABLE("xrpl::decodeVaultVersion : invalid vault version");
|
||||
return VaultVersion::Legacy;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return static_cast<VaultVersion>(version);
|
||||
return static_cast<VaultVersion>(*leVersion);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault)
|
||||
{
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
|
||||
return decodeVaultVersion(vault->at(~sfLEVersion));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultDefaultVersion(Rules const& rules)
|
||||
{
|
||||
// FixedPrecision requires both cleanups, so a FixedPrecision Vault always
|
||||
// sees them enabled.
|
||||
if (rules.enabled(featureLendingProtocolV1_2) && rules.enabled(fixCleanup3_2_0) &&
|
||||
rules.enabled(fixCleanup3_4_0))
|
||||
return VaultVersion::FixedPrecision;
|
||||
if (rules.enabled(featureLendingProtocolV1_1))
|
||||
return VaultVersion::CashBasis;
|
||||
return VaultVersion::Legacy;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
getVaultKind(SLE::ConstRef vault)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STIssue.h>
|
||||
@@ -20,17 +21,19 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// True unit test of `clampToAssetsTotalScale`. The function under test only
|
||||
// reads sfAsset and sfAssetsTotal from the vault SLE and never touches a
|
||||
// ledger view or Rules, so a bare in-memory ltVAULT SLE is enough; there is
|
||||
// no jtx::Env and no transaction submitted anywhere in this file.
|
||||
// True unit test of clampToAssetsTotalScale. The function under test reads
|
||||
// only fields from the vault SLE and never touches a ledger view or Rules, so
|
||||
// a bare in-memory ltVAULT SLE is enough; there is no jtx::Env and no
|
||||
// transaction submitted anywhere in this file.
|
||||
//
|
||||
// Number regime: this suite relies on the default thread_local Number
|
||||
// mantissa range, which src/libxrpl/basics/Number.cpp initializes to
|
||||
@@ -39,17 +42,17 @@ namespace xrpl {
|
||||
// thread_local std::reference_wrapper<MantissaRange const> Number::kRange =
|
||||
// MantissaRange::Access::mantissaRange(MantissaRange::MantissaScale::Large330);
|
||||
//
|
||||
// Unlike transaction processing, this test never constructs a ledger `Rules`
|
||||
// object, so `STAmount::operator=(Number const&)` always takes its
|
||||
// `!getCurrentTransactionRules()` branch and calls `fromNumber`, independent
|
||||
// Unlike transaction processing, this test never constructs a ledger Rules
|
||||
// object, so STAmount::operator=(Number const&) always takes its
|
||||
// !getCurrentTransactionRules() branch and calls fromNumber, independent
|
||||
// of amendment state. testProbeLarge330Regime() below asserts directly on a
|
||||
// value that only round-trips exactly under Large330, pinning the regime
|
||||
// rather than merely asserting it by comment.
|
||||
class VaultHelpers_test : public beast::unit_test::Suite
|
||||
{
|
||||
private:
|
||||
// A single row of the clampToAssetsTotalScale table. `assetsTotal` and
|
||||
// `delta` must already be genuine, on-grid STAmount values for `asset`.
|
||||
// A single row of the clampToAssetsTotalScale table. assetsTotal and
|
||||
// delta must already be genuine, on-grid STAmount values for asset.
|
||||
struct Case
|
||||
{
|
||||
char const* name = nullptr;
|
||||
@@ -58,27 +61,44 @@ private:
|
||||
std::optional<Number> expected; // nullopt means tecPRECISION_LOSS
|
||||
};
|
||||
|
||||
// Builds a bare ltVAULT SLE with only sfAsset and sfAssetsTotal set,
|
||||
// mirroring what a transactor does: set the STNumber field, then call
|
||||
// associateAsset() so it is quantized to the asset's STAmount grid, the
|
||||
// same way VaultDeposit::doApply does for a real vault (see
|
||||
// src/libxrpl/tx/transactors/vault/VaultDeposit.cpp).
|
||||
// Builds a bare ltVAULT SLE. With fixedScale absent, sfLEVersion stays
|
||||
// absent too, preserving the pre-V1.2 Legacy behavior exercised by the
|
||||
// existing clamp table. With fixedScale set, the SLE is stamped
|
||||
// FixedPrecision with that Scale, exercising clampToAssetsTotalScale's
|
||||
// roundToPosteriorVaultScale branch instead.
|
||||
static std::shared_ptr<SLE>
|
||||
makeVault(Asset const& asset, Number const& assetsTotal)
|
||||
makeVault(
|
||||
Asset const& asset,
|
||||
Number const& assetsTotal,
|
||||
std::optional<std::uint8_t> fixedScale = std::nullopt)
|
||||
{
|
||||
auto vault = std::make_shared<SLE>(keylet::vault(UInt256(1)));
|
||||
vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
|
||||
vault->at(sfAssetsTotal) = assetsTotal;
|
||||
associateAsset(*vault, asset);
|
||||
if (fixedScale)
|
||||
{
|
||||
vault->at(sfLEVersion) = std::to_underlying(VaultVersion::FixedPrecision);
|
||||
vault->at(sfScale) = *fixedScale;
|
||||
// FixedPrecision derives its total from these two fields.
|
||||
vault->at(sfAssetsAvailable) = assetsTotal;
|
||||
vault->at(sfAssetsDeployed) = Number{0};
|
||||
}
|
||||
return vault;
|
||||
}
|
||||
|
||||
// Runs every case in `cases` against `asset`, once per ambient rounding
|
||||
// Runs every case in cases against asset, once per ambient rounding
|
||||
// mode. The function must give the same answer under all four modes,
|
||||
// and its answer must match the hand-derived `expected` value.
|
||||
// and its answer must match the hand-derived expected value.
|
||||
// fixedScale, when set, builds a FixedPrecision vault at that Scale
|
||||
// instead of the default Legacy vault.
|
||||
template <std::size_t N>
|
||||
void
|
||||
runCases(Asset const& asset, std::array<Case, N> const& cases)
|
||||
runCases(
|
||||
Asset const& asset,
|
||||
std::array<Case, N> const& cases,
|
||||
std::optional<std::uint8_t> fixedScale = std::nullopt,
|
||||
bool viaClampVaultOutflow = false)
|
||||
{
|
||||
std::array<Number::RoundingMode, 4> const modes{
|
||||
Number::RoundingMode::ToNearest,
|
||||
@@ -91,7 +111,7 @@ private:
|
||||
{
|
||||
testcase(c.name);
|
||||
|
||||
auto const vault = makeVault(asset, c.assetsTotal);
|
||||
auto const vault = makeVault(asset, c.assetsTotal, fixedScale);
|
||||
BEAST_EXPECTS(
|
||||
Number(vault->at(sfAssetsTotal)) == c.assetsTotal,
|
||||
std::string(c.name) +
|
||||
@@ -107,7 +127,8 @@ private:
|
||||
for (auto const mode : modes)
|
||||
{
|
||||
NumberRoundModeGuard const rg(mode);
|
||||
auto const result = clampToAssetsTotalScale(vault, delta);
|
||||
auto const result = viaClampVaultOutflow ? clampVaultOutflow(vault, delta)
|
||||
: clampToAssetsTotalScale(vault, delta);
|
||||
|
||||
// The function must be insensitive to the caller's ambient
|
||||
// rounding mode: every mode must agree with the first one
|
||||
@@ -151,7 +172,13 @@ private:
|
||||
continue;
|
||||
}
|
||||
|
||||
STAmount const expected{asset, *c.expected};
|
||||
// c.expected is guaranteed set here: the !c.expected case
|
||||
// above always continues to the next loop iteration.
|
||||
STAmount const expected{
|
||||
asset,
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
*c.expected,
|
||||
};
|
||||
if (!BEAST_EXPECTS(
|
||||
result.has_value(),
|
||||
std::string(c.name) + ": expected success (" + expected.getText() +
|
||||
@@ -176,9 +203,9 @@ private:
|
||||
// representable at that scale.
|
||||
//
|
||||
// For debits this holds directly at postScale, because the
|
||||
// result IS `roundToScale(magnitude, postScale, Downward)` by
|
||||
// result IS roundToScale(magnitude, postScale, Downward) by
|
||||
// construction. For credits the result is
|
||||
// `roundedPosterior - assetsTotal`, where roundedPosterior
|
||||
// roundedPosterior - assetsTotal, where roundedPosterior
|
||||
// sits exactly on the postScale grid but assetsTotal sits on
|
||||
// its own (possibly finer) natural grid; the difference of a
|
||||
// multiple of 10^postScale and a multiple of 10^assetsScale
|
||||
@@ -347,7 +374,7 @@ private:
|
||||
// = 4.999999999999991.
|
||||
// This mirrors testBugVaultDepositOvercreditsAcrossScaleBoundary
|
||||
// in VaultBugs_test.cpp (same seed/deposit values), which
|
||||
// asserts post-fix `credited <= paid` rather than an exact
|
||||
// asserts post-fix credited <= paid rather than an exact
|
||||
// number; this row pins the exact value.
|
||||
.name = "IOU credit: overcredit fix across a scale boundary",
|
||||
.assetsTotal = Number{9'999'999'999'999'999LL, -15},
|
||||
@@ -463,6 +490,100 @@ private:
|
||||
runCases(xrp, xrpCases);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// FixedPrecision vaults, credits: clampToAssetsTotalScale takes the
|
||||
// roundToPosteriorVaultScale branch instead of the Legacy/CashBasis
|
||||
// scale()-of-the-sum branch; it is FixedPrecision's only credit path
|
||||
// (VaultDeposit). All rows below use a Scale-6 vault (baseScale -6)
|
||||
// with assetsTotal already sitting on that grid, so TowardsZero-
|
||||
// truncating delta itself to scale -6 is the whole story: no case
|
||||
// here forces liveScale to coarsen past baseScale (that scenario needs
|
||||
// a non-unit share price and is exercised by
|
||||
// VaultFixedPrecision_test.cpp's testPartialTowardZeroRounding / dust
|
||||
// tests through real transactions instead).
|
||||
// -------------------------------------------------------------------
|
||||
void
|
||||
testFixedPrecisionCredit(Asset const& iou)
|
||||
{
|
||||
std::uint8_t const fixedScale = 6;
|
||||
Number const onGrid{3'234'567, -6}; // 3.234567, exact at scale -6.
|
||||
|
||||
std::array<Case, 3> const cases{
|
||||
Case{
|
||||
// delta already exact at the base grid: passes through
|
||||
// unchanged, same as a Legacy on-grid credit.
|
||||
.name = "FixedPrecision credit: exact on the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{2, -6},
|
||||
.expected = Number{2, -6},
|
||||
},
|
||||
Case{
|
||||
// delta = +2.3 base units, truncates to 2 base units.
|
||||
.name = "FixedPrecision credit: truncated toward zero on the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{23, -7},
|
||||
.expected = Number{2, -6},
|
||||
},
|
||||
Case{
|
||||
// delta = +0.4 base units: sub-ULP at the fixed grid, so
|
||||
// TowardsZero truncates it entirely to zero.
|
||||
.name = "FixedPrecision credit: sub-ULP dust rejected at the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{4, -7},
|
||||
.expected = std::nullopt,
|
||||
},
|
||||
};
|
||||
|
||||
runCases(iou, cases, fixedScale);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// FixedPrecision vaults, debits: outflows (VaultWithdraw, VaultClawback)
|
||||
// never reach clampToAssetsTotalScale's FixedPrecision branch, which
|
||||
// only accepts non-negative deltas; they go through the public
|
||||
// clampVaultOutflow dispatcher instead, which rounds at
|
||||
// AssetsAvailable's own posterior scale (clampToAvailableScale). These
|
||||
// rows pin the same truncation behavior through that entry point.
|
||||
// -------------------------------------------------------------------
|
||||
void
|
||||
testFixedPrecisionDebit(Asset const& iou)
|
||||
{
|
||||
std::uint8_t const fixedScale = 6;
|
||||
Number const onGrid{3'234'567, -6}; // 3.234567, exact at scale -6.
|
||||
|
||||
std::array<Case, 3> const cases{
|
||||
Case{
|
||||
// delta already exact at the base grid: passes through
|
||||
// unchanged, same as a Legacy on-grid debit.
|
||||
.name = "FixedPrecision debit: exact on the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{-1, -6},
|
||||
.expected = Number{1, -6},
|
||||
},
|
||||
Case{
|
||||
// delta = -1.7 base units. TowardsZero truncates the
|
||||
// magnitude to 1 base unit -- this is the "books the
|
||||
// truncated amount" behavior VaultFixedPrecision_test's
|
||||
// testPartialTowardZeroRounding exercises end-to-end via
|
||||
// VaultWithdraw; this row pins it at the helper level.
|
||||
.name = "FixedPrecision debit: truncated toward zero on the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{-17, -7},
|
||||
.expected = Number{1, -6},
|
||||
},
|
||||
Case{
|
||||
// delta = -0.3 base units: sub-ULP at the fixed grid, so
|
||||
// TowardsZero truncates it entirely to zero.
|
||||
.name = "FixedPrecision debit: sub-ULP dust rejected at the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{-3, -7},
|
||||
.expected = std::nullopt,
|
||||
},
|
||||
};
|
||||
|
||||
runCases(iou, cases, fixedScale, /* viaClampVaultOutflow */ true);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -477,6 +598,8 @@ public:
|
||||
testIouDebits(iou);
|
||||
testIouCredits(iou);
|
||||
testIntegralAssets(mpt, xrp);
|
||||
testFixedPrecisionCredit(iou);
|
||||
testFixedPrecisionDebit(iou);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
598
src/tests/libxrpl/protocol/VaultBalanceTests.cpp
Normal file
598
src/tests/libxrpl/protocol/VaultBalanceTests.cpp
Normal file
@@ -0,0 +1,598 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <protocol/VaultTestHelpers.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
namespace {
|
||||
|
||||
beast::Journal const kNullJournal{beast::Journal::getNullSink()};
|
||||
|
||||
// Snapshots every field adjustVaultBalances can touch, for atomicity checks.
|
||||
struct BalanceSnapshot
|
||||
{
|
||||
Number available;
|
||||
Number assetsDeployed;
|
||||
Number loss;
|
||||
Number yield;
|
||||
Number total;
|
||||
|
||||
static BalanceSnapshot
|
||||
of(SLE::ConstRef vault)
|
||||
{
|
||||
return {
|
||||
.available = vault->at(sfAssetsAvailable),
|
||||
.assetsDeployed = vault->at(sfAssetsDeployed),
|
||||
.loss = vault->at(sfLossUnrealized),
|
||||
.yield = vault->at(sfYieldUnrealized),
|
||||
.total = vault->at(sfAssetsTotal),
|
||||
};
|
||||
}
|
||||
|
||||
bool
|
||||
operator==(BalanceSnapshot const&) const = default;
|
||||
};
|
||||
|
||||
class VaultBalance : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
test::Account const issuer_{"issuer"};
|
||||
Issue const iou_{toCurrency("USD"), issuer_.id()};
|
||||
|
||||
// FixedPrecision IOU vault; stored is the (derived, hence possibly
|
||||
// stale) sfAssetsTotal cache.
|
||||
[[nodiscard]] std::shared_ptr<SLE>
|
||||
iouVault(
|
||||
Number const& assetsDeployed,
|
||||
Number const& available,
|
||||
Number const& stored = Number{0},
|
||||
std::uint8_t scale = 6) const
|
||||
{
|
||||
return makeVault(
|
||||
iou_, stored, VaultVersion::FixedPrecision, scale, assetsDeployed, available);
|
||||
}
|
||||
|
||||
// AssetsDeployed 500, available 100.
|
||||
[[nodiscard]] std::shared_ptr<SLE>
|
||||
standardVault() const
|
||||
{
|
||||
return iouVault(Number{500}, Number{100});
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
iouAmount(Number const& n) const
|
||||
{
|
||||
return STAmount{iou_, n};
|
||||
}
|
||||
|
||||
// Zero cash change: syncs the cached sfAssetsTotal.
|
||||
static void
|
||||
syncTotal(std::shared_ptr<SLE> const& vault)
|
||||
{
|
||||
ASSERT_EQ(adjustVaultBalances(vault, {}, kNullJournal), tesSUCCESS);
|
||||
}
|
||||
|
||||
// Above coarsening the cached sfAssetsTotal (Downward, 16 digits) is a
|
||||
// floor of the exact total: never above it, and within one unit of its
|
||||
// own live exponent.
|
||||
void
|
||||
expectCacheFloorsExactTotal(std::shared_ptr<SLE> const& vault) const
|
||||
{
|
||||
Number const exact = getAssetsTotal(vault);
|
||||
Number const cached = vault->at(sfAssetsTotal);
|
||||
EXPECT_LE(cached, exact);
|
||||
int const liveExponent = scale(cached, iou_);
|
||||
EXPECT_LE(exact - cached, (Number{1, liveExponent}));
|
||||
}
|
||||
|
||||
// Adjusts asset's integral vault by +25 and by an overdraft.
|
||||
static void
|
||||
checkIntegralControl(Asset const& asset)
|
||||
{
|
||||
auto const vault =
|
||||
makeVault(asset, Number{0}, VaultVersion::FixedPrecision, 0, Number{500}, Number{100});
|
||||
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = STAmount{asset, Number{25}}}, kNullJournal),
|
||||
tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{125});
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
|
||||
EXPECT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = STAmount{asset, Number{-1'000}}}, kNullJournal),
|
||||
tefBAD_LEDGER);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getAssetsTotal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(VaultBalance, get_assets_total_stored_for_non_fixed_precision)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
VaultVersion version = VaultVersion::Legacy;
|
||||
std::int64_t stored = 0;
|
||||
std::int64_t available = 0;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{.name = "Legacy", .version = VaultVersion::Legacy, .stored = 1'234'567, .available = 999},
|
||||
{
|
||||
.name = "CashBasis",
|
||||
.version = VaultVersion::CashBasis,
|
||||
.stored = 9'876'543,
|
||||
.available = 100,
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
auto const vault =
|
||||
makeVault(iou_, Number{row.stored}, row.version, 6, Number{0}, Number{row.available});
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{row.stored});
|
||||
EXPECT_FALSE(vault->isFieldPresent(sfAssetsDeployed));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, get_assets_total_fixed_precision_derived_sum)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
Number assetsDeployed;
|
||||
Number available;
|
||||
// The synced cache equals the exact total (not merely a floor of it).
|
||||
bool cacheExact = false;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{
|
||||
.name = "ExactSum",
|
||||
.assetsDeployed = Number{250'000},
|
||||
.available = Number{1'000'000},
|
||||
.cacheExact = true,
|
||||
},
|
||||
// available is already 16 significant digits; assetsDeployed pushes the exact
|
||||
// sum to 17, past what a 16-digit STAmount represents exactly.
|
||||
{
|
||||
.name = "ExactPastSixteenDigits",
|
||||
.assetsDeployed = Number{8, -6},
|
||||
.available = Number{9'999'999'999'999'999, -6},
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
auto const vault = iouVault(row.assetsDeployed, row.available);
|
||||
Number const exact = row.available + row.assetsDeployed;
|
||||
|
||||
// Exact sum: no rounding, no conversion to a 16-digit STAmount.
|
||||
EXPECT_EQ(getAssetsTotal(vault), exact);
|
||||
EXPECT_GE(getAssetsTotal(vault), vault->at(sfAssetsAvailable));
|
||||
|
||||
syncTotal(vault);
|
||||
if (row.cacheExact)
|
||||
{
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
}
|
||||
expectCacheFloorsExactTotal(vault);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, get_assets_total_fixed_precision_integral_exact)
|
||||
{
|
||||
auto const vault = makeVault(
|
||||
xrpIssue(), Number{0}, VaultVersion::FixedPrecision, 0, Number{250}, Number{1'000});
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{1'250});
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, get_assets_total_ignores_stale_cache)
|
||||
{
|
||||
Number const available{4'000'000};
|
||||
Number const assetsDeployed{1'000'000};
|
||||
auto const vault = iouVault(assetsDeployed, available, Number{99});
|
||||
EXPECT_EQ(getAssetsTotal(vault), available + assetsDeployed);
|
||||
EXPECT_NE(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
}
|
||||
|
||||
// The exact total, and the synced cache once Downward-rounded, must never
|
||||
// fall below AssetsAvailable even when assetsDeployed coarsens the scale.
|
||||
TEST_F(VaultBalance, get_assets_total_never_below_available)
|
||||
{
|
||||
Number const available{9'999'999'999'999'999, -6};
|
||||
Number const assetsDeployed{5'000'000'000, -6};
|
||||
auto const vault = iouVault(assetsDeployed, available);
|
||||
EXPECT_GT(getVaultScale(vault), getVaultBaseScale(vault));
|
||||
EXPECT_GE(getAssetsTotal(vault), available);
|
||||
|
||||
syncTotal(vault);
|
||||
EXPECT_GE(Number(vault->at(sfAssetsTotal)), available);
|
||||
}
|
||||
|
||||
// available and assetsDeployed are each at most 16 digits, so associateAsset leaves them
|
||||
// alone. Their sum, 99999999999999995 at exponent -4, has a 17th digit (5)
|
||||
// that would round up under ToNearest and carry through the run of nines,
|
||||
// bumping the exponent. Downward truncation drops it without a bump.
|
||||
TEST_F(VaultBalance, live_scale_downward_keeps_lower_exponent_at_carry_boundary)
|
||||
{
|
||||
auto vault = iouVault(Number{35, -4}, Number{9'999'999'999'999'996LL, -3});
|
||||
|
||||
int const downwardExponent = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return scale(getAssetsTotal(vault), iou_);
|
||||
}();
|
||||
int const toNearestExponent = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(getAssetsTotal(vault), iou_);
|
||||
}();
|
||||
EXPECT_LT(downwardExponent, toNearestExponent);
|
||||
EXPECT_EQ(getVaultScale(vault), downwardExponent);
|
||||
|
||||
syncTotal(vault);
|
||||
EXPECT_EQ(scale(vault->at(sfAssetsTotal), iou_), downwardExponent);
|
||||
EXPECT_EQ(getVaultScale(vault), scale(vault->at(sfAssetsTotal), iou_));
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, scale_and_open_include_assets_deployed)
|
||||
{
|
||||
// Available alone stays at base scale; Available + AssetsDeployed coarsens.
|
||||
Number const available{1'000'000'000};
|
||||
Number const assetsDeployed{9'000'000'000};
|
||||
auto const vault = iouVault(assetsDeployed, available, available);
|
||||
|
||||
EXPECT_EQ(getAssetsTotal(vault), available + assetsDeployed);
|
||||
EXPECT_GT(getVaultScale(vault), getVaultBaseScale(vault));
|
||||
|
||||
// Open capacity uses the derived total, so assetsDeployed counts against the
|
||||
// ceiling: with assetsDeployed filling most of Open, the inflow is rejected.
|
||||
auto const nearOpen = iouVault(Number{1, 9}, Number{8, 9});
|
||||
EXPECT_EQ(checkOptionalVaultInflow(nearOpen, iouAmount(Number{1})), tecLIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// adjustVaultBalances
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(VaultBalance, fixed_precision_adjust_balances)
|
||||
{
|
||||
Number const assetsDeployed{300};
|
||||
auto const vault = iouVault(assetsDeployed, Number{100}, Number{99});
|
||||
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = iouAmount(Number{50})}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{150});
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), assetsDeployed);
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{450});
|
||||
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = iouAmount(Number{-20})}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{130});
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), assetsDeployed);
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{430});
|
||||
|
||||
// Clear both rails together, the way a final VaultWithdraw does.
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(
|
||||
vault,
|
||||
{.cash = iouAmount(Number{-130}), .deployed = -Number(assetsDeployed)},
|
||||
kNullJournal),
|
||||
tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{0});
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), Number{0});
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), Number{0});
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{0});
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, fixed_precision_add_above_coarsening_syncs)
|
||||
{
|
||||
Number const assetsDeployed{5, -6};
|
||||
auto const vault = iouVault(assetsDeployed, Number{9'999'999'999'999'990, -6});
|
||||
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = iouAmount(Number{10, -6})}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), assetsDeployed);
|
||||
expectCacheFloorsExactTotal(vault);
|
||||
}
|
||||
|
||||
// For each (balance, delta), AssetsAvailable after adjustVaultBalances must
|
||||
// equal the plain STAmount sum, the arithmetic the ledger uses for the real
|
||||
// trust-line/MPT transfer.
|
||||
TEST_F(VaultBalance, trust_line_arithmetic_mirror)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
Number balance;
|
||||
Number delta;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{.name = "Plain", .balance = Number{1'000'000}, .delta = Number{500'000}},
|
||||
{
|
||||
.name = "CreditCrossesPowerOfTenUp",
|
||||
.balance = Number{9'999'999'999'999'999, -9},
|
||||
.delta = Number{2, -9},
|
||||
},
|
||||
{
|
||||
.name = "DebitCrossesPowerOfTenDown",
|
||||
.balance = Number{1'000'000'000'000'000, -9},
|
||||
.delta = Number{-1, -9},
|
||||
},
|
||||
{.name = "DebitPartial", .balance = Number{5'000'000}, .delta = Number{-3'000'000}},
|
||||
{.name = "DebitToZero", .balance = Number{2'000'000}, .delta = Number{-2'000'000}},
|
||||
{
|
||||
.name = "CreditFinerThanBalanceGrid",
|
||||
.balance = Number{1'873'013'129'122'272LL, -9},
|
||||
.delta = Number{7, -10},
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
STAmount const balance{iou_, row.balance};
|
||||
STAmount const delta{iou_, row.delta};
|
||||
STAmount const expected{iou_, Number(balance) + Number(delta)};
|
||||
|
||||
auto const vault = iouVault(Number{0}, row.balance, Number{0}, 9);
|
||||
ASSERT_EQ(adjustVaultBalances(vault, {.cash = delta}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(Number(vault->at(sfAssetsAvailable)), Number(expected));
|
||||
}
|
||||
}
|
||||
|
||||
// Each row starts from assetsDeployed 500, available 100.
|
||||
TEST_F(VaultBalance, fields_move)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
std::int64_t cash = 0;
|
||||
std::int64_t deployed = 0;
|
||||
std::int64_t yield = 0;
|
||||
std::int64_t loss = 0;
|
||||
std::int64_t expectedAvailable = 100;
|
||||
std::int64_t expectedAssetsDeployed = 500;
|
||||
std::int64_t expectedYield = 0;
|
||||
std::int64_t expectedLoss = 0;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{.name = "CashAlone", .cash = 25, .expectedAvailable = 125},
|
||||
{.name = "AssetsDeployedAlone", .deployed = -200, .expectedAssetsDeployed = 300},
|
||||
{.name = "YieldAlone", .yield = 40, .expectedYield = 40},
|
||||
{.name = "LossAlone", .loss = 150, .expectedLoss = 150},
|
||||
{
|
||||
.name = "AllTogether",
|
||||
.cash = 40,
|
||||
.deployed = -100,
|
||||
.yield = 10,
|
||||
.loss = 50,
|
||||
.expectedAvailable = 140,
|
||||
.expectedAssetsDeployed = 400,
|
||||
.expectedYield = 10,
|
||||
.expectedLoss = 50,
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
auto const vault = standardVault();
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(
|
||||
vault,
|
||||
{.cash = iouAmount(Number{row.cash}),
|
||||
.deployed = Number{row.deployed},
|
||||
.yield = Number{row.yield},
|
||||
.loss = Number{row.loss}},
|
||||
kNullJournal),
|
||||
tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{row.expectedAvailable});
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), Number{row.expectedAssetsDeployed});
|
||||
EXPECT_EQ(vault->at(sfYieldUnrealized), Number{row.expectedYield});
|
||||
EXPECT_EQ(vault->at(sfLossUnrealized), Number{row.expectedLoss});
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
}
|
||||
}
|
||||
|
||||
// A failing field rejects the whole change as a unit, including any valid
|
||||
// cash leg. Each row starts from assetsDeployed 500, available 100.
|
||||
TEST_F(VaultBalance, invalid_change_fails_atomically)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
std::int64_t cash = 0;
|
||||
std::int64_t deployed = 0;
|
||||
std::int64_t loss = 0;
|
||||
TER expected;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{.name = "AssetsAvailableNegative", .cash = -200, .expected = tefBAD_LEDGER},
|
||||
{.name = "AssetsDeployedNegative", .deployed = -600, .expected = tefBAD_LEDGER},
|
||||
// The +50 credit is valid on its own; the deployed change is not.
|
||||
{
|
||||
.name = "AssetsDeployedNegativeWithValidCash",
|
||||
.cash = 50,
|
||||
.deployed = -600,
|
||||
.expected = tefBAD_LEDGER,
|
||||
},
|
||||
// The -50 debit is valid on its own (available stays at 50); the
|
||||
// loss exceeds AssetsDeployed.
|
||||
{
|
||||
.name = "LossExceedsAssetsDeployedWithValidCash",
|
||||
.cash = -50,
|
||||
.loss = 600,
|
||||
.expected = tecLIMIT_EXCEEDED,
|
||||
},
|
||||
{.name = "LossUnrealizedNegative", .loss = -1, .expected = tefBAD_LEDGER},
|
||||
{.name = "LossExceedsAssetsDeployed", .loss = 600, .expected = tecLIMIT_EXCEEDED},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
auto const vault = standardVault();
|
||||
BalanceSnapshot const before = BalanceSnapshot::of(vault);
|
||||
|
||||
EXPECT_EQ(
|
||||
adjustVaultBalances(
|
||||
vault,
|
||||
{.cash = iouAmount(Number{row.cash}),
|
||||
.deployed = Number{row.deployed},
|
||||
.loss = Number{row.loss}},
|
||||
kNullJournal),
|
||||
row.expected);
|
||||
EXPECT_EQ(BalanceSnapshot::of(vault), before);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, loss_at_assets_deployed_boundary_allowed)
|
||||
{
|
||||
auto const vault = standardVault();
|
||||
EXPECT_EQ(adjustVaultBalances(vault, {.loss = Number{500}}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfLossUnrealized), Number{500});
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, negative_yield_unrealized_rejected)
|
||||
{
|
||||
auto const vault = standardVault();
|
||||
vault->at(sfYieldUnrealized) = Number{5};
|
||||
auto const before = BalanceSnapshot::of(vault);
|
||||
|
||||
EXPECT_EQ(adjustVaultBalances(vault, {.yield = Number{-20}}, kNullJournal), tefBAD_LEDGER);
|
||||
EXPECT_EQ(BalanceSnapshot::of(vault), before);
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, assets_total_equals_floor16_of_available_plus_assets_deployed)
|
||||
{
|
||||
Number const assetsDeployed{7, -9};
|
||||
auto vault = iouVault(assetsDeployed, Number{9'999'999'999'999'999, -9}, Number{0}, 9);
|
||||
|
||||
syncTotal(vault);
|
||||
|
||||
Number const expected = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return Number(STAmount{iou_, Number(vault->at(sfAssetsAvailable)) + assetsDeployed});
|
||||
}();
|
||||
EXPECT_EQ(Number(vault->at(sfAssetsTotal)), expected);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integral assets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(VaultBalance, xrp_control)
|
||||
{
|
||||
checkIntegralControl(xrpIssue());
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, mpt_control)
|
||||
{
|
||||
checkIntegralControl(MPTIssue{makeMptID(1, issuer_.id())});
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_scale_ignores_ambient_rounding_mode)
|
||||
{
|
||||
// reference is on a fine grid (scale -10); raw needs a coarser atScale once
|
||||
// reference + raw is floored, so flooredSum - reference needs more digits
|
||||
// than fit in a 16-digit STAmount. creditToPosteriorScale must build that
|
||||
// difference under its own roundingMode (Downward here), not whatever
|
||||
// rounding mode happens to be ambient when it is called.
|
||||
Number const reference{4'218'667'505'995'833LL, -10};
|
||||
STAmount const raw{iou_, std::int64_t{4'004'426'170'440'612LL}, -8};
|
||||
int const atScale = -7;
|
||||
|
||||
// The exact (pre-STAmount) Downward credit, computed independently, floors
|
||||
// strictly below raw: part of raw is lost rounding reference + raw onto
|
||||
// the coarser atScale grid.
|
||||
Number const exactDownwardCredit = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
Number const flooredSum =
|
||||
roundToAsset(iou_, reference + Number(raw), atScale, Number::RoundingMode::Downward);
|
||||
return Number(STAmount{iou_, flooredSum - reference});
|
||||
}();
|
||||
EXPECT_LT(exactDownwardCredit, Number(raw));
|
||||
|
||||
// Calling under an ambient ToNearest mode (the common case: no caller sets
|
||||
// an explicit guard around this call) must still produce the Downward
|
||||
// result, not a value rounded under the ambient mode.
|
||||
NumberRoundModeGuard const ambient(Number::RoundingMode::ToNearest);
|
||||
STAmount const credit = detail::creditToPosteriorScale(
|
||||
iou_, reference, atScale, raw, Number::RoundingMode::Downward);
|
||||
EXPECT_LT(Number(credit), Number(raw));
|
||||
EXPECT_EQ(Number(credit), exactDownwardCredit);
|
||||
}
|
||||
|
||||
// creditToPosteriorAvailableScale returns
|
||||
// floor16(AssetsAvailable + raw) - AssetsAvailable, never finer than -Scale.
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_available_scale_integral_passes_through)
|
||||
{
|
||||
auto const vault = makeVault(xrpIssue(), Number{1'000}, VaultVersion::FixedPrecision);
|
||||
STAmount const credit =
|
||||
creditToPosteriorAvailableScale(vault, Number{250}, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, STAmount(xrpIssue(), 250));
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_available_scale_floors_to_base_grid)
|
||||
{
|
||||
// Scale 6: the base grid is 1e-6, and AssetsAvailable is small enough
|
||||
// that the posterior grid is the base grid.
|
||||
auto const vault = iouVault(Number{0}, Number{100});
|
||||
STAmount const credit = creditToPosteriorAvailableScale(
|
||||
vault, Number{12'345'678, -7}, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, iouAmount(Number{1'234'567, -6}));
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_available_scale_floors_the_sum_across_power_of_ten)
|
||||
{
|
||||
// 9999999999.999999 + 0.000011 crosses 10^10, where the 16-digit grid is
|
||||
// 1e-5. Flooring the delta alone would give 0.00001; flooring the sum
|
||||
// gives 10000000000.00001, so the credit is exactly 0.000011.
|
||||
auto const vault = iouVault(Number{0}, Number{9'999'999'999'999'999, -6});
|
||||
STAmount const credit =
|
||||
creditToPosteriorAvailableScale(vault, Number{11, -6}, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, iouAmount(Number{11, -6}));
|
||||
EXPECT_EQ(
|
||||
STAmount(iou_, Number{9'999'999'999'999'999, -6} + Number(credit)),
|
||||
iouAmount(Number{1'000'000'000'000'001, -5}));
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_available_scale_uses_the_exact_raw_amount)
|
||||
{
|
||||
// raw has 17 significant digits. Exact: 0.000009 + 12345678901.234561 =
|
||||
// 12345678901.23457 on the 1e-5 grid, so the credit is
|
||||
// 12345678901.234561, stored as 12345678901.23456. Rounding raw to 16
|
||||
// digits first (12345678901.23456) would floor the sum to
|
||||
// 12345678901.23456 and give a credit one unit lower, 12345678901.23455.
|
||||
auto const vault = iouVault(Number{0}, Number{9, -6});
|
||||
STAmount const credit = creditToPosteriorAvailableScale(
|
||||
vault, Number{12'345'678'901'234'561, -6}, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, iouAmount(Number{1'234'567'890'123'456, -5}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl
|
||||
376
src/tests/libxrpl/protocol/VaultGridTests.cpp
Normal file
376
src/tests/libxrpl/protocol/VaultGridTests.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/hash/uhash.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <protocol/VaultTestHelpers.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace xrpl {
|
||||
namespace {
|
||||
|
||||
TEST(VaultGrid, base_and_live_scale)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
|
||||
auto const legacy = makeVault(iou, Number{1'000'000}, VaultVersion::Legacy, 6);
|
||||
EXPECT_EQ(getVaultScale(legacy), -9);
|
||||
EXPECT_EQ(getVaultBaseScale(legacy), getVaultScale(legacy));
|
||||
|
||||
auto const empty = makeVault(iou, Number{0}, VaultVersion::FixedPrecision, 6);
|
||||
EXPECT_EQ(getVaultScale(empty), -6);
|
||||
EXPECT_EQ(getVaultBaseScale(empty), -6);
|
||||
|
||||
auto const small = makeVault(iou, Number{1'000'000}, VaultVersion::FixedPrecision, 6);
|
||||
EXPECT_EQ(getVaultScale(small), -6);
|
||||
EXPECT_EQ(getVaultBaseScale(small), -6);
|
||||
|
||||
auto const coarsened = makeVault(iou, Number{10'000'000'000}, VaultVersion::FixedPrecision, 6);
|
||||
EXPECT_GT(getVaultScale(coarsened), getVaultBaseScale(coarsened));
|
||||
EXPECT_EQ(getVaultBaseScale(coarsened), -6);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, pre_v12_behavior_is_preserved)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
Number const assetsTotal{1'000'000};
|
||||
STAmount const onGrid{iou, Number{2, -9}};
|
||||
STAmount const dust{iou, Number{4, -10}};
|
||||
STAmount const overOpen{iou, Number{10, 9}};
|
||||
auto const downward = Number::RoundingMode::Downward;
|
||||
|
||||
auto const legacy = makeVault(iou, assetsTotal, VaultVersion::Legacy);
|
||||
int const expectedLive = getVaultScale(legacy);
|
||||
int const expectedPosterior = detail::getPosteriorVaultScale(legacy, onGrid);
|
||||
STAmount const expectedPosteriorRound = roundToPosteriorVaultScale(legacy, dust, downward);
|
||||
|
||||
for (auto const version : {
|
||||
std::optional<VaultVersion>{},
|
||||
std::optional{VaultVersion::Legacy},
|
||||
std::optional{VaultVersion::CashBasis},
|
||||
})
|
||||
{
|
||||
auto const vault = makeVault(iou, assetsTotal, version);
|
||||
EXPECT_EQ(getVaultScale(vault), expectedLive);
|
||||
EXPECT_EQ(getVaultBaseScale(vault), expectedLive);
|
||||
EXPECT_EQ(detail::getPosteriorVaultScale(vault, onGrid), expectedPosterior);
|
||||
EXPECT_EQ(roundToPosteriorVaultScale(vault, dust, downward), expectedPosteriorRound);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(vault, overOpen), tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VaultGrid, pre_v12_credit_clamp_floors_posterior_total)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
Number const assetsTotal{9'999'999'999'999'999LL, -15};
|
||||
STAmount const delta{iou, Number{5}};
|
||||
STAmount const expected{iou, Number{4'999'999'999'999'991LL, -15}};
|
||||
|
||||
for (auto const version : {
|
||||
std::optional<VaultVersion>{},
|
||||
std::optional{VaultVersion::Legacy},
|
||||
std::optional{VaultVersion::CashBasis},
|
||||
})
|
||||
{
|
||||
auto const result = clampToAssetsTotalScale(makeVault(iou, assetsTotal, version), delta);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VaultGrid, integral_scale_is_zero)
|
||||
{
|
||||
auto const vault = makeVault(xrpIssue(), Number{1'000}, VaultVersion::FixedPrecision, 0);
|
||||
STAmount const delta{xrpIssue(), 7};
|
||||
|
||||
EXPECT_EQ(getVaultScale(vault), 0);
|
||||
EXPECT_EQ(getVaultBaseScale(vault), 0);
|
||||
EXPECT_EQ(detail::getPosteriorVaultScale(vault, delta), 0);
|
||||
EXPECT_EQ(roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero), delta);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, posterior_scale_rounds_delta)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault =
|
||||
makeVault(iou, Number{9'999'999'999'999'999, -6}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const delta{iou, Number{21, -6}};
|
||||
|
||||
EXPECT_EQ(getVaultScale(vault), -6);
|
||||
EXPECT_EQ(detail::getPosteriorVaultScale(vault, delta), -5);
|
||||
EXPECT_EQ(
|
||||
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero),
|
||||
STAmount(iou, Number{20, -6}));
|
||||
}
|
||||
|
||||
TEST(VaultGrid, posterior_scale_rejects_dust_at_call_site)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault = makeVault(iou, Number{10'000'000'000}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const dust{iou, Number{1, -6}};
|
||||
|
||||
EXPECT_EQ(
|
||||
roundToPosteriorVaultScale(vault, dust, Number::RoundingMode::TowardsZero), beast::kZero);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, posterior_outflow_can_refine_scale)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault =
|
||||
makeVault(iou, Number{1'000'000'000'000'001, -5}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const delta{iou, -Number{11, -6}};
|
||||
|
||||
EXPECT_EQ(getVaultScale(vault), -5);
|
||||
EXPECT_EQ(detail::getPosteriorVaultScale(vault, delta), -6);
|
||||
EXPECT_EQ(roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero), delta);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, optional_inflow_capacity_boundaries)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
|
||||
auto const fixedIou = makeVault(iou, Number{9, 5}, VaultVersion::FixedPrecision, 10);
|
||||
STAmount const iouDust{iou, Number{1, -10}};
|
||||
EXPECT_EQ(getVaultOpenLimit(fixedIou), (Number{9, 5}));
|
||||
EXPECT_EQ(checkOptionalVaultInflow(fixedIou, STAmount{iou}), tesSUCCESS);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(fixedIou, iouDust), tecLIMIT_EXCEEDED);
|
||||
|
||||
auto const fixedXrp = makeVault(xrpIssue(), Number{9, 15}, VaultVersion::FixedPrecision, 0);
|
||||
STAmount const xrpUnit{xrpIssue(), 1};
|
||||
EXPECT_EQ(getVaultOpenLimit(fixedXrp), (Number{9, 15}));
|
||||
EXPECT_EQ(checkOptionalVaultInflow(fixedXrp, STAmount{xrpIssue()}), tesSUCCESS);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(fixedXrp, xrpUnit), tecLIMIT_EXCEEDED);
|
||||
|
||||
auto const legacy = makeVault(iou, Number{10, 5}, VaultVersion::Legacy, 10);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(legacy, iouDust), tesSUCCESS);
|
||||
|
||||
auto const coarsening =
|
||||
makeVault(iou, Number{9'999'999'999'999'999, -6}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const coarseningDelta{iou, Number{21, -6}};
|
||||
EXPECT_EQ(checkOptionalVaultInflow(coarsening, coarseningDelta), tecLIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, optional_inflow_includes_yield_unrealized)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault = makeVault(iou, Number{8'999'999'999}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const amount{iou, Number{1}};
|
||||
|
||||
EXPECT_EQ(checkOptionalVaultInflow(vault, amount), tesSUCCESS);
|
||||
|
||||
vault->at(sfYieldUnrealized) = Number{1};
|
||||
associateAsset(*vault, iou);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(vault, amount), tecLIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
// AssetsAvailable + raw needs 20 digits. Under the ToNearest ambient mode the
|
||||
// sum would round up to the next 10^-6 before the Downward floor, crediting
|
||||
// 0.000001 for a raw 0.0000009996.
|
||||
TEST(VaultGrid, credit_floors_sum_in_requested_mode)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault = makeVault(
|
||||
iou,
|
||||
Number{0},
|
||||
VaultVersion::FixedPrecision,
|
||||
6,
|
||||
Number{0},
|
||||
Number{1'234'567'890'123'456LL, -6});
|
||||
Number const raw{9'996, -10};
|
||||
|
||||
NumberRoundModeGuard const ambient(Number::RoundingMode::ToNearest);
|
||||
STAmount const credit =
|
||||
creditToPosteriorAvailableScale(vault, raw, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, STAmount(iou, 0));
|
||||
EXPECT_LE(Number(credit), raw);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, is_on_vault_base_grid)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
int const baseScale = vaultBaseScale(iou, 6);
|
||||
|
||||
struct Row
|
||||
{
|
||||
Number value;
|
||||
bool expected{};
|
||||
};
|
||||
Row const rows[] = {
|
||||
{.value = Number{0}, .expected = true},
|
||||
{.value = Number{1}, .expected = true},
|
||||
{.value = Number{1, -6}, .expected = true},
|
||||
{.value = Number{1, 20}, .expected = true},
|
||||
{.value = Number{1'234'567'890'123'456LL, -6}, .expected = true},
|
||||
// A digit below baseScale.
|
||||
{.value = Number{1, -7}, .expected = false},
|
||||
{.value = Number{15, -7}, .expected = false},
|
||||
// A 17th significant digit, on the base grid but not an STAmount.
|
||||
{.value = Number{12'345'678'901'234'567LL, 0}, .expected = false},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
EXPECT_EQ(isOnVaultBaseGrid(iou, row.value, baseScale), row.expected) << row.value;
|
||||
|
||||
EXPECT_EQ(vaultBaseScale(xrpIssue(), 6), 0);
|
||||
EXPECT_TRUE(isOnVaultBaseGrid(xrpIssue(), Number{7}, 0));
|
||||
|
||||
// Out of range for the asset: rejected instead of throwing.
|
||||
EXPECT_FALSE(isOnVaultBaseGrid(
|
||||
xrpIssue(), Number{static_cast<std::int64_t>(STAmount::kMaxNativeN)} + 1, 0));
|
||||
EXPECT_FALSE(isOnVaultBaseGrid(iou, Number{1, 100}, baseScale));
|
||||
}
|
||||
|
||||
TEST(VaultGrid, clamp_vault_outflow)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
Asset const mpt{MPTIssue{makeMptID(1, issuer.id())}};
|
||||
|
||||
struct Row
|
||||
{
|
||||
char const* name = nullptr;
|
||||
Asset asset;
|
||||
Number available;
|
||||
Number delta;
|
||||
std::optional<Number> expected; // nullopt: tecPRECISION_LOSS
|
||||
};
|
||||
Row const rows[] = {
|
||||
{
|
||||
.name = "XRP outflow is returned unrounded",
|
||||
.asset = xrpIssue(),
|
||||
.available = Number{1'000},
|
||||
.delta = Number{-7},
|
||||
.expected = Number{7},
|
||||
},
|
||||
{
|
||||
.name = "MPT outflow is returned unrounded",
|
||||
.asset = mpt,
|
||||
.available = Number{1'000},
|
||||
.delta = Number{-7},
|
||||
.expected = Number{7},
|
||||
},
|
||||
{
|
||||
.name = "IOU outflow rounds towards zero at the posterior scale",
|
||||
.asset = iou,
|
||||
.available = Number{1'000'000},
|
||||
.delta = Number{-15, -7},
|
||||
.expected = Number{1, -6},
|
||||
},
|
||||
{
|
||||
.name = "IOU outflow below one unit at the posterior scale is rejected",
|
||||
.asset = iou,
|
||||
.available = Number{1'000'000},
|
||||
.delta = Number{-4, -7},
|
||||
.expected = std::nullopt,
|
||||
},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
auto const vault = makeVault(
|
||||
row.asset,
|
||||
row.available,
|
||||
VaultVersion::FixedPrecision,
|
||||
row.asset.integral() ? 0 : 6,
|
||||
Number{0},
|
||||
row.available);
|
||||
auto const result = clampVaultOutflow(vault, STAmount{row.asset, row.delta});
|
||||
if (row.expected)
|
||||
{
|
||||
ASSERT_TRUE(result.has_value()) << row.name;
|
||||
EXPECT_EQ(*result, (STAmount{row.asset, *row.expected})) << row.name;
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT_FALSE(result.has_value()) << row.name;
|
||||
EXPECT_EQ(result.error(), tecPRECISION_LOSS) << row.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VaultGrid, get_vault_default_version)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
char const* name = nullptr;
|
||||
std::unordered_set<UInt256, beast::Uhash<>> enabled;
|
||||
VaultVersion expected;
|
||||
};
|
||||
Row const rows[] = {
|
||||
{.name = "no lending amendments: Legacy", .enabled = {}, .expected = VaultVersion::Legacy},
|
||||
{
|
||||
.name = "LendingProtocolV1_1: CashBasis",
|
||||
.enabled = {featureLendingProtocolV1_1},
|
||||
.expected = VaultVersion::CashBasis,
|
||||
},
|
||||
{
|
||||
.name = "V1_1 and V1_2 without the cleanups: CashBasis",
|
||||
.enabled = {featureLendingProtocolV1_1, featureLendingProtocolV1_2},
|
||||
.expected = VaultVersion::CashBasis,
|
||||
},
|
||||
{
|
||||
.name = "V1_1, V1_2 and fixCleanup3_2_0 without fixCleanup3_4_0: CashBasis",
|
||||
.enabled = {featureLendingProtocolV1_1, featureLendingProtocolV1_2, fixCleanup3_2_0},
|
||||
.expected = VaultVersion::CashBasis,
|
||||
},
|
||||
{
|
||||
.name = "V1_1, V1_2 and fixCleanup3_4_0 without fixCleanup3_2_0: CashBasis",
|
||||
.enabled = {featureLendingProtocolV1_1, featureLendingProtocolV1_2, fixCleanup3_4_0},
|
||||
.expected = VaultVersion::CashBasis,
|
||||
},
|
||||
{
|
||||
.name = "V1_2 and both cleanups: FixedPrecision",
|
||||
.enabled = {featureLendingProtocolV1_2, fixCleanup3_2_0, fixCleanup3_4_0},
|
||||
.expected = VaultVersion::FixedPrecision,
|
||||
},
|
||||
{
|
||||
.name = "V1_1, V1_2 and both cleanups: FixedPrecision",
|
||||
.enabled =
|
||||
{
|
||||
featureLendingProtocolV1_1,
|
||||
featureLendingProtocolV1_2,
|
||||
fixCleanup3_2_0,
|
||||
fixCleanup3_4_0,
|
||||
},
|
||||
.expected = VaultVersion::FixedPrecision,
|
||||
},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
// Rules keeps a reference to the preset set; row.enabled outlives it.
|
||||
Rules const rules{row.enabled};
|
||||
EXPECT_EQ(getVaultDefaultVersion(rules), row.expected) << row.name;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl
|
||||
65
src/tests/libxrpl/protocol/VaultTestHelpers.h
Normal file
65
src/tests/libxrpl/protocol/VaultTestHelpers.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
// Needed for standalone compilation of sfScale/sfLEVersion assignment below;
|
||||
// full-TU builds already see it transitively, so misc-include-cleaner flags
|
||||
// it as unused there. Keep it so this header still compiles on its own.
|
||||
#include <xrpl/protocol/STInteger.h> // NOLINT(misc-include-cleaner)
|
||||
#include <xrpl/protocol/STIssue.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Shared by VaultGridTests, VaultBalanceTests and LoanBrokerCoverTests. For
|
||||
// FixedPrecision, getAssetsTotal derives from AssetsAvailable plus
|
||||
// AssetsDeployed rather than reading the stored AssetsTotal cache, so a
|
||||
// FixedPrecision vault must have AssetsAvailable (and, if requested, a
|
||||
// non-zero AssetsDeployed) seeded separately from assetsTotal. By default
|
||||
// AssetsAvailable is assetsTotal - assetsDeployed, so the derived total
|
||||
// equals assetsTotal; pass assetsAvailable explicitly to decouple it (e.g.
|
||||
// to leave assetsTotal as a deliberately stale cache). Legacy/CashBasis
|
||||
// vaults have no AssetsDeployed field and normally leave AssetsAvailable
|
||||
// unset, but some cash-flow tests need it seeded too, so assetsAvailable is
|
||||
// honored for those versions as well when given.
|
||||
inline std::shared_ptr<SLE>
|
||||
makeVault(
|
||||
Asset const& asset,
|
||||
Number const& assetsTotal,
|
||||
std::optional<VaultVersion> version,
|
||||
std::uint8_t scaleValue = kVaultDefaultIouScale,
|
||||
Number const& assetsDeployed = Number{0},
|
||||
std::optional<Number> assetsAvailable = std::nullopt)
|
||||
{
|
||||
auto vault = std::make_shared<SLE>(keylet::vault(uint256(1)));
|
||||
vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
|
||||
vault->at(sfAssetsTotal) = assetsTotal;
|
||||
if (!asset.integral())
|
||||
vault->at(sfScale) = scaleValue;
|
||||
if (version)
|
||||
vault->at(sfLEVersion) = std::to_underlying(*version);
|
||||
if (version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
vault->at(sfAssetsAvailable) = assetsAvailable.value_or(assetsTotal - assetsDeployed);
|
||||
vault->at(sfAssetsDeployed) = assetsDeployed;
|
||||
}
|
||||
else if (assetsAvailable)
|
||||
{
|
||||
vault->at(sfAssetsAvailable) = *assetsAvailable;
|
||||
}
|
||||
associateAsset(*vault, asset);
|
||||
return vault;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -32,6 +32,8 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
auto const assetsAvailableValue = canonical_NUMBER();
|
||||
auto const assetsMaximumValue = canonical_NUMBER();
|
||||
auto const lossUnrealizedValue = canonical_NUMBER();
|
||||
auto const yieldUnrealizedValue = canonical_NUMBER();
|
||||
auto const assetsDeployedValue = canonical_NUMBER();
|
||||
auto const shareMPTIDValue = canonical_UINT192();
|
||||
auto const withdrawalPolicyValue = canonical_UINT8();
|
||||
auto const scaleValue = canonical_UINT8();
|
||||
@@ -57,6 +59,8 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
builder.setAssetsAvailable(assetsAvailableValue);
|
||||
builder.setAssetsMaximum(assetsMaximumValue);
|
||||
builder.setLossUnrealized(lossUnrealizedValue);
|
||||
builder.setYieldUnrealized(yieldUnrealizedValue);
|
||||
builder.setAssetsDeployed(assetsDeployedValue);
|
||||
builder.setScale(scaleValue);
|
||||
builder.setLEVersion(lEVersionValue);
|
||||
builder.setVaultKind(vaultKindValue);
|
||||
@@ -166,6 +170,22 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasLossUnrealized());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = yieldUnrealizedValue;
|
||||
auto const actualOpt = entry.getYieldUnrealized();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfYieldUnrealized");
|
||||
EXPECT_TRUE(entry.hasYieldUnrealized());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = assetsDeployedValue;
|
||||
auto const actualOpt = entry.getAssetsDeployed();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfAssetsDeployed");
|
||||
EXPECT_TRUE(entry.hasAssetsDeployed());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = scaleValue;
|
||||
auto const actualOpt = entry.getScale();
|
||||
@@ -231,6 +251,8 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
auto const assetsAvailableValue = canonical_NUMBER();
|
||||
auto const assetsMaximumValue = canonical_NUMBER();
|
||||
auto const lossUnrealizedValue = canonical_NUMBER();
|
||||
auto const yieldUnrealizedValue = canonical_NUMBER();
|
||||
auto const assetsDeployedValue = canonical_NUMBER();
|
||||
auto const shareMPTIDValue = canonical_UINT192();
|
||||
auto const withdrawalPolicyValue = canonical_UINT8();
|
||||
auto const scaleValue = canonical_UINT8();
|
||||
@@ -253,6 +275,8 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfAssetsAvailable) = assetsAvailableValue;
|
||||
sle->at(sfAssetsMaximum) = assetsMaximumValue;
|
||||
sle->at(sfLossUnrealized) = lossUnrealizedValue;
|
||||
sle->at(sfYieldUnrealized) = yieldUnrealizedValue;
|
||||
sle->at(sfAssetsDeployed) = assetsDeployedValue;
|
||||
sle->at(sfShareMPTID) = shareMPTIDValue;
|
||||
sle->at(sfWithdrawalPolicy) = withdrawalPolicyValue;
|
||||
sle->at(sfScale) = scaleValue;
|
||||
@@ -425,6 +449,32 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfLossUnrealized");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = yieldUnrealizedValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getYieldUnrealized();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getYieldUnrealized();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfYieldUnrealized");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfYieldUnrealized");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = assetsDeployedValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getAssetsDeployed();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getAssetsDeployed();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfAssetsDeployed");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfAssetsDeployed");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = scaleValue;
|
||||
|
||||
@@ -570,6 +620,10 @@ TEST(VaultTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getAssetsMaximum().has_value());
|
||||
EXPECT_FALSE(entry.hasLossUnrealized());
|
||||
EXPECT_FALSE(entry.getLossUnrealized().has_value());
|
||||
EXPECT_FALSE(entry.hasYieldUnrealized());
|
||||
EXPECT_FALSE(entry.getYieldUnrealized().has_value());
|
||||
EXPECT_FALSE(entry.hasAssetsDeployed());
|
||||
EXPECT_FALSE(entry.getAssetsDeployed().has_value());
|
||||
EXPECT_FALSE(entry.hasScale());
|
||||
EXPECT_FALSE(entry.getScale().has_value());
|
||||
EXPECT_FALSE(entry.hasLEVersion());
|
||||
|
||||
Reference in New Issue
Block a user