mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
feat: Add FixedPrecision vault scale helpers and Open-zone deposit checks
New vaults created under V1.2 stay on a lifetime base grid so optional inflows cannot coarsen AssetsTotal, while pre-V1.2 vaults keep the dynamic scale they were created with.
This commit is contained in:
@@ -17,6 +17,69 @@ namespace xrpl {
|
||||
|
||||
class STTx;
|
||||
|
||||
/**
|
||||
* Return the Vault's current live exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use the exponent of AssetsTotal. FixedPrecision
|
||||
* Vaults floor that exponent at their lifetime base exponent.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::const_ref 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::const_ref vault);
|
||||
|
||||
/**
|
||||
* Return the Vault's posterior live exponent after applying an unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::const_ref vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Round an amount at the Vault's current live exponent.
|
||||
*
|
||||
* Reserved for LoanPay. Vault deposit, withdraw, and clawback round at the
|
||||
* posterior live exponent instead.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToVaultScale(SLE::const_ref vault, STAmount const& amount, Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Round an amount at the Vault's posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::const_ref vault,
|
||||
STAmount const& amount,
|
||||
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::const_ref 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::const_ref vault, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -53,9 +116,10 @@ sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
|
||||
* 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 credits: Floors the resulting total asset balance and
|
||||
* returns the difference from the current total.
|
||||
* - FixedPrecision credits: Rounds the delta toward zero at the posterior live
|
||||
* scale.
|
||||
*
|
||||
* Key rules:
|
||||
* - The returned magnitude never exceeds `|delta|`.
|
||||
|
||||
@@ -319,14 +319,20 @@ constexpr std::uint8_t kVaultDefaultIouScale = 6;
|
||||
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,
|
||||
* Maximum fixed-precision IOU scale factor for a Vault.
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumFixedIouScale = 10;
|
||||
|
||||
/**
|
||||
* Vault ledger-entry schema versions. Assigned to newly created Vaults by
|
||||
* featureLendingProtocolV1_1 and later protocol amendments. Vaults created
|
||||
* before activation are left without LEVersion (implicit legacy version 0,
|
||||
* accrual-basis accounting).
|
||||
*/
|
||||
enum class VaultVersion : uint8_t {
|
||||
Legacy = 0,
|
||||
CashBasis,
|
||||
FixedPrecision,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
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)
|
||||
|
||||
@@ -513,6 +513,7 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfAssetsAvailable, SoeDefault},
|
||||
{sfAssetsMaximum, SoeDefault},
|
||||
{sfLossUnrealized, SoeDefault},
|
||||
{sfYieldUnrealized, SoeDefault},
|
||||
{sfShareMPTID, SoeRequired},
|
||||
{sfWithdrawalPolicy, SoeRequired},
|
||||
{sfScale, SoeDefault},
|
||||
|
||||
@@ -239,6 +239,7 @@ 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)
|
||||
|
||||
// 32-bit signed (common)
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
|
||||
@@ -242,6 +242,30 @@ 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 sfShareMPTID (SoeRequired)
|
||||
* @return The field value.
|
||||
@@ -571,6 +595,17 @@ 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 sfShareMPTID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -248,14 +248,13 @@ loanPaymentDeltas(LoanPaymentParts const& parts)
|
||||
|
||||
namespace {
|
||||
|
||||
// Cash-basis accounting applies only when featureLendingProtocolV1_1 is
|
||||
// enabled AND the specific Vault was created under it (LEVersion ==
|
||||
// VaultVersion::CashBasis). Vaults created before activation keep accrual-basis
|
||||
// accounting forever, even after the amendment later turns on.
|
||||
// Cash-basis accounting applies to Vaults created under
|
||||
// featureLendingProtocolV1_1 or a later version. Vaults created before
|
||||
// activation keep accrual-basis accounting forever.
|
||||
bool
|
||||
cashBasisEnabled(SLE::const_ref vaultSle)
|
||||
{
|
||||
return getVaultVersion(vaultSle) == VaultVersion::CashBasis;
|
||||
return getVaultVersion(vaultSle) >= VaultVersion::CashBasis;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -17,6 +18,7 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
@@ -24,6 +26,169 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int
|
||||
fixedBaseScale(SLE::const_ref vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::fixedBaseScale : valid Vault sle");
|
||||
if (vault->at(sfAsset).integral())
|
||||
return 0;
|
||||
return -static_cast<int>(vault->at(sfScale));
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale)
|
||||
{
|
||||
if (reference == beast::kZero)
|
||||
return baseScale;
|
||||
return std::max(baseScale, scale(reference, asset));
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::const_ref vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultScale : valid Vault sle");
|
||||
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(vault->at(sfAssetsTotal), vault->at(sfAsset));
|
||||
case VaultVersion::FixedPrecision:
|
||||
return liveScale(vault->at(sfAssetsTotal), vault->at(sfAsset), fixedBaseScale(vault));
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::const_ref 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]] int
|
||||
getPosteriorVaultScale(SLE::const_ref vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::getPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::getPosteriorVaultScale : delta and Vault asset match");
|
||||
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return vault->at(sfAssetsTotal) + delta;
|
||||
}();
|
||||
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(posterior, vault->at(sfAsset));
|
||||
case VaultVersion::FixedPrecision:
|
||||
return liveScale(posterior, vault->at(sfAsset), fixedBaseScale(vault));
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getPosteriorVaultScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToVaultScale(SLE::const_ref vault, STAmount const& amount, Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::roundToVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::roundToVaultScale : amount and Vault asset match");
|
||||
if (amount.integral())
|
||||
return amount;
|
||||
return roundToScale(amount, getVaultScale(vault), roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::const_ref 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, getPosteriorVaultScale(vault, amount), roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::const_ref 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::const_ref 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 (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 (getPosteriorVaultScale(vault, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
Number const capacity = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return vault->at(sfAssetsTotal) + vault->at(sfYieldUnrealized) + rounded;
|
||||
}();
|
||||
if (capacity > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& assets)
|
||||
{
|
||||
@@ -85,37 +250,47 @@ clampToAssetsTotalScale(SLE::const_ref 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);
|
||||
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 = 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);
|
||||
}();
|
||||
|
||||
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(
|
||||
@@ -225,7 +400,7 @@ getVaultVersion(SLE::const_ref vault)
|
||||
return VaultVersion::Legacy;
|
||||
|
||||
auto const version = vault->at(sfLEVersion);
|
||||
if (version > std::to_underlying(VaultVersion::CashBasis))
|
||||
if (version > std::to_underlying(VaultVersion::FixedPrecision))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultVersion : invalid vault version");
|
||||
@@ -235,18 +410,6 @@ getVaultVersion(SLE::const_ref vault)
|
||||
return static_cast<VaultVersion>(version);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
getVaultKind(SLE::const_ref vault)
|
||||
{
|
||||
|
||||
@@ -347,7 +347,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
// already at AssetsMaximum cannot take another loan. Cash-basis origination
|
||||
// does not change AssetsTotal (see cash_basis::loanOriginationDeltas), so
|
||||
// this leftover accrual gate must not apply there.
|
||||
if (getVaultVersion(vault) != VaultVersion::CashBasis && vault->at(sfAssetsMaximum) != 0 &&
|
||||
if (getVaultVersion(vault) < VaultVersion::CashBasis && vault->at(sfAssetsMaximum) != 0 &&
|
||||
vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Vault at maximum assets limit. Can't add another loan.";
|
||||
@@ -496,7 +496,7 @@ LoanSet::doApply()
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
*vaultSle->at(sfAssetsMaximum) == 0 ||
|
||||
getVaultVersion(vaultSle) == VaultVersion::CashBasis ||
|
||||
getVaultVersion(vaultSle) >= VaultVersion::CashBasis ||
|
||||
*vaultSle->at(sfAssetsMaximum) > *vaultTotalProxy,
|
||||
"xrpl::LoanSet::doApply",
|
||||
"accrual vault is below maximum limit");
|
||||
|
||||
@@ -101,7 +101,10 @@ VaultCreate::preflight(PreflightContext const& ctx)
|
||||
if (vaultAsset.holds<MPTIssue>() || vaultAsset.native())
|
||||
return temMALFORMED;
|
||||
|
||||
if (scale > kVaultMaximumIouScale)
|
||||
auto const maximumScale = ctx.rules.enabled(featureLendingProtocolV1_2)
|
||||
? kVaultMaximumFixedIouScale
|
||||
: kVaultMaximumIouScale;
|
||||
if (scale > maximumScale)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
@@ -273,10 +276,24 @@ VaultCreate::doApply()
|
||||
}
|
||||
if (scale != 0u)
|
||||
vault->at(sfScale) = scale;
|
||||
if (view().rules().enabled(featureLendingProtocolV1_1))
|
||||
// featureLendingProtocolV1_2 is defined to require V1.1: new vaults get
|
||||
// FixedPrecision plus the V1.1 VaultKind fields even if a test enables
|
||||
// only V1.2. YieldUnrealized is SoeDefault, so writing zero stores the
|
||||
// field as absent, matching LossUnrealized.
|
||||
bool const fixedPrecision = view().rules().enabled(featureLendingProtocolV1_2);
|
||||
bool const cashBasis = view().rules().enabled(featureLendingProtocolV1_1);
|
||||
if (fixedPrecision)
|
||||
{
|
||||
vault->at(sfLEVersion) = std::to_underlying(VaultVersion::FixedPrecision);
|
||||
vault->at(sfYieldUnrealized) = Number(0);
|
||||
}
|
||||
else if (cashBasis)
|
||||
{
|
||||
vault->at(sfLEVersion) = std::to_underlying(VaultVersion::CashBasis);
|
||||
}
|
||||
|
||||
if (fixedPrecision || cashBasis)
|
||||
{
|
||||
auto const kind = getVaultKind(tx);
|
||||
vault->at(sfVaultKind) = std::to_underlying(kind);
|
||||
if (kind == VaultKind::ClosedEnded)
|
||||
|
||||
@@ -31,24 +31,6 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
[[nodiscard]]
|
||||
static STAmount
|
||||
roundToVaultScale(STAmount const& amount, SLE::const_ref vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::roundToVaultScale : valid vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset), "xrpl::roundToVaultScale : valid vault asset");
|
||||
|
||||
if (amount.integral())
|
||||
return amount;
|
||||
|
||||
int const postScale = [&]() {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(vault->at(sfAssetsTotal) + amount, vault->at(sfAsset));
|
||||
}();
|
||||
return roundToScale(amount, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
// True if debiting `assets` would leave the depositor's balance where it started, so the deposit
|
||||
// would mint shares against a transfer that never happened. Asking the balance directly whether it
|
||||
// notices the debit avoids having to infer the rounding step: it has to be the stored balance that
|
||||
@@ -187,7 +169,9 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
if (auto const ter = requireAuth(ctx.view, vaultAsset, account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
auto const roundedAmount = fix320Enabled ? roundToVaultScale(amount, vault) : amount;
|
||||
auto const roundedAmount = fix320Enabled
|
||||
? roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero)
|
||||
: amount;
|
||||
|
||||
if (fix320Enabled && roundedAmount == beast::kZero)
|
||||
{
|
||||
@@ -237,10 +221,11 @@ VaultDeposit::doApply()
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
|
||||
// Post-amendment IOU only: round Downward to the AssetsTotal precision so
|
||||
// Post-amendment IOU only: round toward zero to the AssetsTotal precision so
|
||||
// a sub-ULP tail can't be silently absorbed by one rail and not the other.
|
||||
auto const amount =
|
||||
fix320Enabled ? roundToVaultScale(ctx_.tx[sfAmount], vault) : ctx_.tx[sfAmount];
|
||||
auto const amount = fix320Enabled
|
||||
? roundToPosteriorVaultScale(vault, ctx_.tx[sfAmount], Number::RoundingMode::TowardsZero)
|
||||
: ctx_.tx[sfAmount];
|
||||
|
||||
// We validated zero-amount in preclaim, if we ended up with zero now, fail hard.
|
||||
if (amount == beast::kZero)
|
||||
@@ -374,6 +359,9 @@ VaultDeposit::doApply()
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
|
||||
if (auto const ter = checkOptionalVaultInflow(vault, assetsDeposited); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
XRPL_ASSERT(
|
||||
sharesCreated.asset() != assetsDeposited.asset(),
|
||||
"xrpl::VaultDeposit::doApply : assets are not shares");
|
||||
|
||||
@@ -471,7 +471,7 @@ VaultWithdraw::doApply()
|
||||
// re-derived: any trimmed residue stays with remaining shareholders.
|
||||
auto const maybeClamped = clampToAssetsTotalScale(vault, -assetsWithdrawn);
|
||||
if (!maybeClamped)
|
||||
return maybeClamped.error(); // LCOV_EXCL_LINE
|
||||
return maybeClamped.error();
|
||||
assetsWithdrawn = *maybeClamped;
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
|
||||
@@ -1675,7 +1675,6 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
Number const interestDue{75};
|
||||
|
||||
auto const legacyVault = makeVaultSle();
|
||||
auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis);
|
||||
|
||||
{
|
||||
testcase(
|
||||
@@ -1691,14 +1690,17 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
|
||||
{
|
||||
testcase(
|
||||
"loanOriginationDeltas dispatcher: amendment enabled, LEVersion == "
|
||||
"VaultVersion::CashBasis picks CashBasis");
|
||||
"loanOriginationDeltas dispatcher: CashBasis and FixedPrecision "
|
||||
"Vaults pick cash-basis accounting");
|
||||
Env const env{*this};
|
||||
auto const deltas =
|
||||
loanOriginationDeltas(cashBasisVault, principalRequested, interestDue);
|
||||
auto const expected = xrpl::cash_basis::loanOriginationDeltas(principalRequested);
|
||||
BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
|
||||
BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
|
||||
for (auto const version : {VaultVersion::CashBasis, VaultVersion::FixedPrecision})
|
||||
{
|
||||
auto const deltas =
|
||||
loanOriginationDeltas(makeVaultSle(version), principalRequested, interestDue);
|
||||
auto const expected = xrpl::cash_basis::loanOriginationDeltas(principalRequested);
|
||||
BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
|
||||
BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1713,7 +1715,6 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
Number const interestDue{101};
|
||||
|
||||
auto const legacyVault = makeVaultSle(std::nullopt, vaultMaximum, vaultTotal);
|
||||
auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis, vaultMaximum, vaultTotal);
|
||||
|
||||
{
|
||||
testcase(
|
||||
@@ -1728,12 +1729,16 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
|
||||
{
|
||||
testcase(
|
||||
"loanOriginationExceedsVaultMaximum dispatcher: amendment enabled, LEVersion == "
|
||||
"VaultVersion::CashBasis picks CashBasis");
|
||||
"loanOriginationExceedsVaultMaximum dispatcher: CashBasis and "
|
||||
"FixedPrecision Vaults pick cash-basis accounting");
|
||||
Env const env{*this};
|
||||
BEAST_EXPECT(
|
||||
loanOriginationExceedsVaultMaximum(cashBasisVault, vaultTotal, interestDue) ==
|
||||
false);
|
||||
for (auto const version : {VaultVersion::CashBasis, VaultVersion::FixedPrecision})
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
loanOriginationExceedsVaultMaximum(
|
||||
makeVaultSle(version, vaultMaximum, vaultTotal), vaultTotal, interestDue) ==
|
||||
false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1743,7 +1748,6 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
using namespace jtx;
|
||||
|
||||
auto const legacyVault = makeVaultSle();
|
||||
auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis);
|
||||
|
||||
{
|
||||
testcase("loanVaultExposure dispatcher: amendment enabled, legacy vault picks Accrual");
|
||||
@@ -1755,13 +1759,16 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
|
||||
{
|
||||
testcase(
|
||||
"loanVaultExposure dispatcher: amendment enabled, LEVersion == "
|
||||
"VaultVersion::CashBasis "
|
||||
"picks CashBasis");
|
||||
"loanVaultExposure dispatcher: CashBasis and FixedPrecision "
|
||||
"Vaults pick cash-basis accounting");
|
||||
Env const env{*this};
|
||||
auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50});
|
||||
BEAST_EXPECT(
|
||||
loanVaultExposure(cashBasisVault, sle) == xrpl::cash_basis::loanVaultExposure(sle));
|
||||
for (auto const version : {VaultVersion::CashBasis, VaultVersion::FixedPrecision})
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
loanVaultExposure(makeVaultSle(version), sle) ==
|
||||
xrpl::cash_basis::loanVaultExposure(sle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1777,7 +1784,6 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
.feePaid = Number{3}};
|
||||
|
||||
auto const legacyVault = makeVaultSle();
|
||||
auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis);
|
||||
|
||||
{
|
||||
testcase("loanPaymentDeltas dispatcher: amendment enabled, legacy vault picks Accrual");
|
||||
@@ -1790,14 +1796,16 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
|
||||
{
|
||||
testcase(
|
||||
"loanPaymentDeltas dispatcher: amendment enabled, LEVersion == "
|
||||
"VaultVersion::CashBasis "
|
||||
"picks CashBasis");
|
||||
"loanPaymentDeltas dispatcher: CashBasis and FixedPrecision "
|
||||
"Vaults pick cash-basis accounting");
|
||||
Env const env{*this};
|
||||
auto const deltas = loanPaymentDeltas(cashBasisVault, parts);
|
||||
auto const expected = xrpl::cash_basis::loanPaymentDeltas(parts);
|
||||
BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
|
||||
BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
|
||||
for (auto const version : {VaultVersion::CashBasis, VaultVersion::FixedPrecision})
|
||||
{
|
||||
auto const deltas = loanPaymentDeltas(makeVaultSle(version), parts);
|
||||
auto const expected = xrpl::cash_basis::loanPaymentDeltas(parts);
|
||||
BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
|
||||
BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,12 +83,11 @@ protected:
|
||||
// Ensure that all the features needed for Lending Protocol are included,
|
||||
// even if they are set to unsupported.
|
||||
//
|
||||
// featureLendingProtocolV1_1 is excluded from the default set: it changes
|
||||
// Vault/LoanBroker accounting (AssetsTotal/DebtTotal/LossUnrealized), and
|
||||
// most of this file's tests assert whole-life-specific expected values
|
||||
// for those fields. Tests that specifically exercise the amendment opt
|
||||
// it back in explicitly (e.g. `all_ | featureLendingProtocolV1_1`).
|
||||
FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1};
|
||||
// Later Lending amendments are excluded from the default set because
|
||||
// they change accounting and precision behavior. Tests that exercise an
|
||||
// amendment opt it back in explicitly.
|
||||
FeatureBitset const all_{
|
||||
jtx::testableAmendments() - featureLendingProtocolV1_1 - featureLendingProtocolV1_2};
|
||||
std::string const iouCurrency_{"IOU"};
|
||||
|
||||
struct BrokerParameters
|
||||
@@ -346,7 +345,7 @@ protected:
|
||||
{
|
||||
auto const expectedDebt =
|
||||
env.current()->rules().enabled(featureLendingProtocolV1_1) &&
|
||||
getVaultVersion(vaultSle) == VaultVersion::CashBasis
|
||||
getVaultVersion(vaultSle) >= VaultVersion::CashBasis
|
||||
? principalOutstanding
|
||||
: principalOutstanding + interestOwed;
|
||||
env.test.BEAST_EXPECT(brokerDebt == expectedDebt);
|
||||
@@ -451,7 +450,7 @@ protected:
|
||||
env.test.BEAST_EXPECT(
|
||||
vaultSle->at(sfLossUnrealized) ==
|
||||
(env.current()->rules().enabled(featureLendingProtocolV1_1) &&
|
||||
getVaultVersion(vaultSle) == VaultVersion::CashBasis
|
||||
getVaultVersion(vaultSle) >= VaultVersion::CashBasis
|
||||
? principalOutstanding
|
||||
: totalValue - managementFeeOutstanding));
|
||||
}
|
||||
@@ -666,7 +665,7 @@ protected:
|
||||
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable);
|
||||
auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) +
|
||||
(env.current()->rules().enabled(featureLendingProtocolV1_1) &&
|
||||
getVaultVersion(vaultSle) == VaultVersion::CashBasis
|
||||
getVaultVersion(vaultSle) >= VaultVersion::CashBasis
|
||||
? state.principalOutstanding
|
||||
: state.totalValue - state.managementFeeOutstanding);
|
||||
|
||||
|
||||
@@ -127,13 +127,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultWithdraw to destination at IOU precision boundary fires "
|
||||
"invariant (pre-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
runScenario(all_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw to destination at IOU precision boundary succeeds "
|
||||
"when destroyed amount is sub-ULP (post-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments(), tesSUCCESS);
|
||||
runScenario(all_, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,13 +192,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultDeposit by issuer at IOU edge fires "
|
||||
"tecINVARIANT_FAILED at finalize (pre-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
runScenario(all_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultDeposit by issuer at IOU edge rejects with "
|
||||
"tecPRECISION_LOSS proactively (post-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments(), tecPRECISION_LOSS);
|
||||
runScenario(all_, tecPRECISION_LOSS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,13 +271,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultDeposit across IOU scale boundary fires invariant "
|
||||
"(pre-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
runScenario(all_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultDeposit across IOU scale boundary succeeds "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments(), tecPRECISION_LOSS);
|
||||
runScenario(all_, tecPRECISION_LOSS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,13 +344,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultWithdraw across IOU scale boundary fires invariant "
|
||||
"(pre-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
runScenario(all_ - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw across IOU scale boundary succeeds "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments(), tesSUCCESS);
|
||||
runScenario(all_, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,14 +436,13 @@ private:
|
||||
// Also remove fixCleanup3_4_0 so the VaultDeposit clamp
|
||||
// introduced by that amendment does not short-circuit this
|
||||
// pre-fixCleanup3_2_0 scenario with tecPRECISION_LOSS.
|
||||
runScenario(
|
||||
testableAmendments() - fixCleanup3_2_0 - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runScenario(all_ - fixCleanup3_2_0 - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultDeposit below Vault precision canonicalized to zero "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments(), tecPRECISION_LOSS);
|
||||
runScenario(all_, tecPRECISION_LOSS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,7 +580,7 @@ private:
|
||||
// pattern that only makes sense on open-ended vaults. The gate
|
||||
// added by LP V1.1 is unrelated to the truncation bug asserted
|
||||
// here.
|
||||
auto const legacy = testableAmendments() - featureLendingProtocolV1_1;
|
||||
auto const legacy = all_ - featureLendingProtocolV1_1;
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultDeposit share truncation lets depositor debit "
|
||||
@@ -697,27 +696,25 @@ private:
|
||||
testcase(
|
||||
"bug: VaultWithdraw to third-party at IOU edge fires invariant "
|
||||
"(pre-fixCleanup3_2_0)");
|
||||
runScenario(
|
||||
testableAmendments() - fixCleanup3_2_0, DestKind::ThirdParty, tecINVARIANT_FAILED);
|
||||
runScenario(all_ - fixCleanup3_2_0, DestKind::ThirdParty, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw to third-party at IOU edge succeeds "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments(), DestKind::ThirdParty, tesSUCCESS);
|
||||
runScenario(all_, DestKind::ThirdParty, tesSUCCESS);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw to self at IOU edge fires invariant "
|
||||
"(pre-fixCleanup3_2_0)");
|
||||
runScenario(
|
||||
testableAmendments() - fixCleanup3_2_0, DestKind::Self, tecINVARIANT_FAILED);
|
||||
runScenario(all_ - fixCleanup3_2_0, DestKind::Self, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw to self at IOU edge succeeds "
|
||||
"(post-fixCleanup3_2_0)");
|
||||
runScenario(testableAmendments(), DestKind::Self, tesSUCCESS);
|
||||
runScenario(all_, DestKind::Self, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,14 +1004,14 @@ private:
|
||||
"IOU vault deposit exceeding depositor's balance but "
|
||||
"within counterparty's trust limit, pre-fixCleanup3_2_0 "
|
||||
"(tefINTERNAL)");
|
||||
runTest(test::jtx::testableAmendments() - fixCleanup3_2_0, tefINTERNAL);
|
||||
runTest(all_ - fixCleanup3_2_0, tefINTERNAL);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"IOU vault deposit exceeding depositor's balance but "
|
||||
"within counterparty's trust limit, post-fixCleanup3_2_0 "
|
||||
"(tesSUCCESS)");
|
||||
runTest(test::jtx::testableAmendments(), tesSUCCESS);
|
||||
runTest(all_, tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1026,7 +1023,7 @@ private:
|
||||
using namespace test::jtx;
|
||||
testcase("Bug6 - limit bypass with share-denominated withdrawal");
|
||||
|
||||
auto const allAmendments = testableAmendments() | featureSingleAssetVault;
|
||||
auto const allAmendments = all_ | featureSingleAssetVault;
|
||||
|
||||
for (auto const& features : {allAmendments, allAmendments - fixCleanup3_1_3})
|
||||
{
|
||||
@@ -1283,13 +1280,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultClawback round-trip overshoot lets issuer recover "
|
||||
"more than requested (pre-fixCleanup3_4_0)");
|
||||
runScenario(testableAmendments() - fixCleanup3_4_0, false);
|
||||
runScenario(all_ - fixCleanup3_4_0, false);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultClawback round-trip overshoot is clamped so "
|
||||
"assetsRecovered <= clawbackAmount (post-fixCleanup3_4_0)");
|
||||
runScenario(testableAmendments(), true);
|
||||
runScenario(all_, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1346,13 +1343,13 @@ private:
|
||||
testcase(
|
||||
"bug: VaultWithdraw round-trip overshoot delivers more than "
|
||||
"requested (pre-fixCleanup3_4_0)");
|
||||
runScenario(testableAmendments() - fixCleanup3_4_0, false);
|
||||
runScenario(all_ - fixCleanup3_4_0, false);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"bug: VaultWithdraw round-trip overshoot is clamped so "
|
||||
"assetsWithdrawn <= requested (post-fixCleanup3_4_0)");
|
||||
runScenario(testableAmendments(), true);
|
||||
runScenario(all_, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
419
src/test/app/vault/VaultFixedPrecision_test.cpp
Normal file
419
src/test/app/vault/VaultFixedPrecision_test.cpp
Normal file
@@ -0,0 +1,419 @@
|
||||
#include <test/app/vault/VaultTestBase.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class VaultFixedPrecision_test : public VaultTestBase
|
||||
{
|
||||
static FeatureBitset
|
||||
features()
|
||||
{
|
||||
return test::jtx::testableAmendments() | featureLendingProtocolV1_1 |
|
||||
featureLendingProtocolV1_2;
|
||||
}
|
||||
|
||||
void
|
||||
testCreate()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
PrettyAsset const asset{issuer["USD"]};
|
||||
|
||||
{
|
||||
testcase("VaultCreate writes FixedPrecision fields");
|
||||
Env env(*this, features());
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto const sle = env.le(keylet);
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(sle->at(sfLEVersion) == std::to_underlying(VaultVersion::FixedPrecision));
|
||||
BEAST_EXPECT(sle->at(sfScale) == kVaultDefaultIouScale);
|
||||
BEAST_EXPECT(sle->at(sfYieldUnrealized) == beast::kZero);
|
||||
}
|
||||
|
||||
for (std::uint8_t const scaleValue :
|
||||
{kVaultMaximumFixedIouScale,
|
||||
static_cast<std::uint8_t>(kVaultMaximumFixedIouScale + 1)})
|
||||
{
|
||||
testcase(
|
||||
scaleValue == kVaultMaximumFixedIouScale
|
||||
? "VaultCreate accepts fixed Scale maximum"
|
||||
: "VaultCreate rejects Scale above fixed maximum");
|
||||
Env env(*this, features());
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
tx[sfScale] = scaleValue;
|
||||
if (scaleValue == kVaultMaximumFixedIouScale)
|
||||
{
|
||||
env(tx);
|
||||
}
|
||||
else
|
||||
{
|
||||
env(tx, Ter(temMALFORMED));
|
||||
}
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
static_cast<bool>(env.le(keylet)) == (scaleValue == kVaultMaximumFixedIouScale));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("CashBasis Vault retains legacy Scale maximum");
|
||||
auto const legacyFeatures = features() - featureLendingProtocolV1_2;
|
||||
Env env(*this, legacyFeatures);
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
tx[sfScale] = kVaultMaximumIouScale;
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto const sle = env.le(keylet);
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(sle->at(sfLEVersion) == std::to_underlying(VaultVersion::CashBasis));
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfYieldUnrealized));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDepositAdmission()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
PrettyAsset const asset{issuer["USD"]};
|
||||
Number const open{9, 9};
|
||||
Number const baseUnit{1, -6};
|
||||
|
||||
Env env(*this, features());
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
env(trust(owner, asset(open + Number{1})));
|
||||
env.close();
|
||||
env(pay(issuer, owner, asset(open + Number{1})));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [create, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
create[sfScale] = 6;
|
||||
env(create);
|
||||
env.close();
|
||||
|
||||
testcase("VaultDeposit admits the Open boundary");
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(open)}));
|
||||
env.close();
|
||||
|
||||
auto const atOpen = env.le(keylet);
|
||||
BEAST_EXPECT(atOpen);
|
||||
BEAST_EXPECT(atOpen->at(sfAssetsTotal) == open);
|
||||
BEAST_EXPECT(atOpen->at(sfAssetsAvailable) == open);
|
||||
|
||||
testcase("VaultDeposit rejects one base unit above Open");
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(baseUnit)}),
|
||||
Ter(tecLIMIT_EXCEEDED));
|
||||
env.close();
|
||||
|
||||
auto const afterRejected = env.le(keylet);
|
||||
BEAST_EXPECT(afterRejected);
|
||||
BEAST_EXPECT(afterRejected->at(sfAssetsTotal) == open);
|
||||
BEAST_EXPECT(afterRejected->at(sfAssetsAvailable) == open);
|
||||
}
|
||||
|
||||
void
|
||||
testExistingCashBasisVault()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("V1.2 does not migrate an existing CashBasis Vault");
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
PrettyAsset const asset{issuer["USD"]};
|
||||
Number const deposit{9'999'999'999'999'999LL};
|
||||
|
||||
Env env(*this, features() - featureLendingProtocolV1_2);
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
env(trust(owner, STAmount{asset.raw(), 2, 16}));
|
||||
env.close();
|
||||
env(pay(issuer, owner, asset(deposit)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [create, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
create[sfScale] = 0;
|
||||
env(create);
|
||||
env.close();
|
||||
|
||||
auto const before = env.le(keylet);
|
||||
BEAST_EXPECT(before);
|
||||
BEAST_EXPECT(before->at(sfLEVersion) == std::to_underlying(VaultVersion::CashBasis));
|
||||
|
||||
env.enableFeature(featureLendingProtocolV1_2);
|
||||
env.close();
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(deposit)}));
|
||||
env.close();
|
||||
|
||||
auto const after = env.le(keylet);
|
||||
BEAST_EXPECT(after);
|
||||
BEAST_EXPECT(after->at(sfLEVersion) == std::to_underlying(VaultVersion::CashBasis));
|
||||
BEAST_EXPECT(!after->isFieldPresent(sfYieldUnrealized));
|
||||
BEAST_EXPECT(after->at(sfAssetsTotal) == deposit);
|
||||
}
|
||||
|
||||
void
|
||||
testPartialTowardZeroRounding()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
// On a fresh vault the share price is one base unit, so the share
|
||||
// round-trip already lands on the Scale-6 grid before
|
||||
// clampToAssetsTotalScale. These cases check that the deposit,
|
||||
// withdraw, and clawback paths still book that truncated amount.
|
||||
// A non-unit share price (loan yield) is needed to exercise the
|
||||
// clamp itself; that arrives with the lending PR.
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
PrettyAsset const asset{issuer["USD"]};
|
||||
Number const depositRequested{32'345'678, -7}; // 3.2345678
|
||||
Number const outflowRequested{10'000'005, -7}; // 1.0000005
|
||||
|
||||
Env env(*this, features());
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env(trust(owner, asset(4)));
|
||||
env.close();
|
||||
env(pay(issuer, owner, asset(4)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [create, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
create[sfScale] = 6;
|
||||
env(create);
|
||||
env.close();
|
||||
|
||||
testcase("VaultDeposit books the truncated amount on the base grid");
|
||||
env(vault.deposit(
|
||||
{.depositor = owner, .id = keylet.key, .amount = asset(depositRequested)}));
|
||||
env.close();
|
||||
|
||||
auto afterDeposit = env.le(keylet);
|
||||
BEAST_EXPECT(afterDeposit);
|
||||
BEAST_EXPECT(afterDeposit->at(sfAssetsTotal) == (Number{3'234'567, -6}));
|
||||
BEAST_EXPECT(afterDeposit->at(sfAssetsAvailable) == (Number{3'234'567, -6}));
|
||||
BEAST_EXPECT(env.balance(owner, asset) == asset(Number{765'433, -6}));
|
||||
|
||||
testcase("VaultWithdraw books the truncated amount on the base grid");
|
||||
env(vault.withdraw(
|
||||
{.depositor = owner, .id = keylet.key, .amount = asset(outflowRequested)}));
|
||||
env.close();
|
||||
|
||||
auto afterWithdraw = env.le(keylet);
|
||||
BEAST_EXPECT(afterWithdraw);
|
||||
BEAST_EXPECT(afterWithdraw->at(sfAssetsTotal) == (Number{2'234'567, -6}));
|
||||
BEAST_EXPECT(afterWithdraw->at(sfAssetsAvailable) == (Number{2'234'567, -6}));
|
||||
BEAST_EXPECT(env.balance(owner, asset) == asset(Number{1'765'433, -6}));
|
||||
|
||||
testcase("VaultClawback books the truncated amount on the base grid");
|
||||
env(vault.clawback(
|
||||
{.issuer = issuer,
|
||||
.id = keylet.key,
|
||||
.holder = owner,
|
||||
.amount = asset(outflowRequested).value()}));
|
||||
env.close();
|
||||
|
||||
auto const afterClawback = env.le(keylet);
|
||||
BEAST_EXPECT(afterClawback);
|
||||
BEAST_EXPECT(afterClawback->at(sfAssetsTotal) == (Number{1'234'567, -6}));
|
||||
BEAST_EXPECT(afterClawback->at(sfAssetsAvailable) == (Number{1'234'567, -6}));
|
||||
}
|
||||
|
||||
void
|
||||
testIntegralAssetCapacity()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("FixedPrecision MPT Vault enforces integral Open zone");
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
constexpr std::uint64_t open = 9'000'000'000'000'000;
|
||||
constexpr std::uint64_t maximum = open + 1;
|
||||
|
||||
Env env(*this, features());
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
|
||||
MPTTester mpt{env, issuer, kMptInitNoFund};
|
||||
mpt.create({.maxAmt = maximum, .flags = tfMPTCanTransfer});
|
||||
PrettyAsset const asset = mpt.issuanceID();
|
||||
mpt.authorize({.account = owner});
|
||||
env(pay(issuer, owner, asset(maximum)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [create, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(create);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(open)}));
|
||||
env.close();
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
|
||||
Ter(tecLIMIT_EXCEEDED));
|
||||
|
||||
auto const sle = env.le(keylet);
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(sle->at(sfAssetsTotal) == Number{open});
|
||||
}
|
||||
|
||||
void
|
||||
testDepositDust()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("VaultDeposit rejects sub-base-unit dust");
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
PrettyAsset const asset{issuer["USD"]};
|
||||
|
||||
Env env(*this, features());
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
env(trust(owner, asset(1)));
|
||||
env.close();
|
||||
env(pay(issuer, owner, asset(1)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [create, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
create[sfScale] = 6;
|
||||
env(create);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(Number{1, -7})}),
|
||||
Ter(tecPRECISION_LOSS));
|
||||
}
|
||||
|
||||
void
|
||||
testWithdrawDust()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("VaultWithdraw rejects sub-base-unit dust");
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
PrettyAsset const asset{issuer["USD"]};
|
||||
|
||||
Env env(*this, features());
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
env(trust(owner, asset(2)));
|
||||
env.close();
|
||||
env(pay(issuer, owner, asset(2)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [create, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
create[sfScale] = 6;
|
||||
env(create);
|
||||
env.close();
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
|
||||
env.close();
|
||||
|
||||
env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(Number{1, -7})}),
|
||||
Ter(tecPRECISION_LOSS));
|
||||
}
|
||||
|
||||
void
|
||||
testClawbackDust()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("VaultClawback rejects sub-base-unit dust");
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
Account const depositor{"depositor"};
|
||||
PrettyAsset const asset{issuer["USD"]};
|
||||
|
||||
Env env(*this, features());
|
||||
env.fund(XRP(1'000'000), issuer, owner, depositor);
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env(trust(depositor, asset(2)));
|
||||
env.close();
|
||||
env(pay(issuer, depositor, asset(2)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [create, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
create[sfScale] = 6;
|
||||
env(create);
|
||||
env.close();
|
||||
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
|
||||
env.close();
|
||||
|
||||
env(vault.clawback(
|
||||
{.issuer = issuer,
|
||||
.id = keylet.key,
|
||||
.holder = depositor,
|
||||
.amount = asset(Number{1, -7}).value()}),
|
||||
Ter(tecPRECISION_LOSS));
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testCreate();
|
||||
testDepositAdmission();
|
||||
testExistingCashBasisVault();
|
||||
testPartialTowardZeroRounding();
|
||||
testIntegralAssetCapacity();
|
||||
testDepositDust();
|
||||
testWithdrawDust();
|
||||
testClawbackDust();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(VaultFixedPrecision, app, xrpl);
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -27,10 +27,10 @@
|
||||
|
||||
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
|
||||
@@ -58,11 +58,8 @@ 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 sfLEVersion absent, preserving the
|
||||
// pre-V1.2 Legacy behavior exercised by this existing clamp table.
|
||||
static std::shared_ptr<SLE>
|
||||
makeVault(Asset const& asset, Number const& assetsTotal)
|
||||
{
|
||||
|
||||
@@ -77,7 +77,7 @@ private:
|
||||
// attaching a loan broker). featureLendingProtocolV1_1 adds a
|
||||
// closed-ended vault gate on LoanBrokerSet::preclaim and is
|
||||
// orthogonal to what this suite asserts, so strip it here.
|
||||
Env env{*this, testableAmendments() - featureLendingProtocolV1_1};
|
||||
Env env{*this, all_ - featureLendingProtocolV1_1};
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const depositor{"depositor"};
|
||||
@@ -1018,7 +1018,7 @@ private:
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Env env{*this, all_};
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
|
||||
|
||||
@@ -113,7 +113,9 @@ protected:
|
||||
return {.vault = vault, .keylet = keylet, .sub = sub, .red = red};
|
||||
}
|
||||
|
||||
FeatureBitset const all_{test::jtx::testableAmendments()};
|
||||
// Keep legacy Vault suites on their pre-V1.2 behavior. Tests for the
|
||||
// fixed-precision protocol enable featureLendingProtocolV1_2 explicitly.
|
||||
FeatureBitset const all_{test::jtx::testableAmendments() - featureLendingProtocolV1_2};
|
||||
std::string const iouCurrency_{"IOU"};
|
||||
};
|
||||
|
||||
|
||||
@@ -369,7 +369,8 @@ private:
|
||||
BEAST_EXPECT(sleVault);
|
||||
BEAST_EXPECT((*sleVault)[sfScale] == 6);
|
||||
}
|
||||
});
|
||||
},
|
||||
{.features = testableAmendments() - featureLendingProtocolV1_2});
|
||||
|
||||
testCase(
|
||||
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
|
||||
@@ -1003,8 +1004,9 @@ private:
|
||||
|
||||
{
|
||||
testcase("VaultCreate LEVersion: featureLendingProtocolV1_1 disabled, field absent");
|
||||
Env env{*this};
|
||||
env.disableFeature(featureLendingProtocolV1_1);
|
||||
Env env{
|
||||
*this,
|
||||
testableAmendments() - featureLendingProtocolV1_1 - featureLendingProtocolV1_2};
|
||||
env.fund(XRP(1'000'000), owner);
|
||||
env.close();
|
||||
|
||||
@@ -1022,7 +1024,7 @@ private:
|
||||
testcase(
|
||||
"VaultCreate LEVersion: featureLendingProtocolV1_1 enabled, LEVersion == "
|
||||
"VaultVersion::CashBasis");
|
||||
Env env{*this};
|
||||
Env env{*this, testableAmendments() - featureLendingProtocolV1_2};
|
||||
env.fund(XRP(1'000'000), owner);
|
||||
env.close();
|
||||
|
||||
|
||||
211
src/tests/libxrpl/protocol/VaultGridTests.cpp
Normal file
211
src/tests/libxrpl/protocol/VaultGridTests.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.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/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STIssue.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 <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
namespace {
|
||||
|
||||
std::shared_ptr<SLE>
|
||||
makeVault(
|
||||
Asset const& asset,
|
||||
Number const& assetsTotal,
|
||||
std::optional<VaultVersion> version,
|
||||
std::uint8_t scaleValue = kVaultDefaultIouScale)
|
||||
{
|
||||
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);
|
||||
associateAsset(*vault, asset);
|
||||
return vault;
|
||||
}
|
||||
|
||||
TEST(VaultGrid, BaseAndLiveScale)
|
||||
{
|
||||
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, PreV12BehaviorIsPreserved)
|
||||
{
|
||||
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 = getPosteriorVaultScale(legacy, onGrid);
|
||||
STAmount const expectedLiveRound = roundToVaultScale(legacy, onGrid, downward);
|
||||
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(getPosteriorVaultScale(vault, onGrid), expectedPosterior);
|
||||
EXPECT_EQ(roundToVaultScale(vault, onGrid, downward), expectedLiveRound);
|
||||
EXPECT_EQ(roundToPosteriorVaultScale(vault, dust, downward), expectedPosteriorRound);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(vault, overOpen), tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VaultGrid, PreV12CreditClampFloorsPosteriorTotal)
|
||||
{
|
||||
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, IntegralScaleIsZero)
|
||||
{
|
||||
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(getPosteriorVaultScale(vault, delta), 0);
|
||||
EXPECT_EQ(roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero), delta);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, PosteriorScaleRoundsDelta)
|
||||
{
|
||||
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(getPosteriorVaultScale(vault, delta), -5);
|
||||
EXPECT_EQ(roundToVaultScale(vault, delta, Number::RoundingMode::TowardsZero), delta);
|
||||
EXPECT_EQ(
|
||||
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero),
|
||||
STAmount(iou, Number{20, -6}));
|
||||
}
|
||||
|
||||
TEST(VaultGrid, PosteriorScaleRejectsDustAtCallSite)
|
||||
{
|
||||
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, PosteriorOutflowCanRefineScale)
|
||||
{
|
||||
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(getPosteriorVaultScale(vault, delta), -6);
|
||||
EXPECT_EQ(roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero), delta);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, OptionalInflowCapacityBoundaries)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
|
||||
auto 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 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 legacy = makeVault(iou, Number{10, 5}, VaultVersion::Legacy, 10);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(legacy, iouDust), tesSUCCESS);
|
||||
|
||||
auto 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, OptionalInflowIncludesYieldUnrealized)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto 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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl
|
||||
@@ -32,6 +32,7 @@ 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 shareMPTIDValue = canonical_UINT192();
|
||||
auto const withdrawalPolicyValue = canonical_UINT8();
|
||||
auto const scaleValue = canonical_UINT8();
|
||||
@@ -57,6 +58,7 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
builder.setAssetsAvailable(assetsAvailableValue);
|
||||
builder.setAssetsMaximum(assetsMaximumValue);
|
||||
builder.setLossUnrealized(lossUnrealizedValue);
|
||||
builder.setYieldUnrealized(yieldUnrealizedValue);
|
||||
builder.setScale(scaleValue);
|
||||
builder.setLEVersion(lEVersionValue);
|
||||
builder.setVaultKind(vaultKindValue);
|
||||
@@ -166,6 +168,14 @@ 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 = scaleValue;
|
||||
auto const actualOpt = entry.getScale();
|
||||
@@ -231,6 +241,7 @@ 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 shareMPTIDValue = canonical_UINT192();
|
||||
auto const withdrawalPolicyValue = canonical_UINT8();
|
||||
auto const scaleValue = canonical_UINT8();
|
||||
@@ -253,6 +264,7 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfAssetsAvailable) = assetsAvailableValue;
|
||||
sle->at(sfAssetsMaximum) = assetsMaximumValue;
|
||||
sle->at(sfLossUnrealized) = lossUnrealizedValue;
|
||||
sle->at(sfYieldUnrealized) = yieldUnrealizedValue;
|
||||
sle->at(sfShareMPTID) = shareMPTIDValue;
|
||||
sle->at(sfWithdrawalPolicy) = withdrawalPolicyValue;
|
||||
sle->at(sfScale) = scaleValue;
|
||||
@@ -425,6 +437,19 @@ 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 = scaleValue;
|
||||
|
||||
@@ -570,6 +595,8 @@ 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.hasScale());
|
||||
EXPECT_FALSE(entry.getScale().has_value());
|
||||
EXPECT_FALSE(entry.hasLEVersion());
|
||||
|
||||
Reference in New Issue
Block a user