mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-28 07:48:01 +00:00
feat: Apply FixedPrecision cover grid to LoanBroker first-loss capital
Cover deposit, withdraw, and clawback round at the posterior CoverAvailable exponent so optional inflows cannot coarsen past the Open zone, while outflows may re-fine. DebtMaximum and minimum cover use the vault base scale, and FixedPrecision withdraw/clawback skip the live-scale canApplyToBrokerCover guard that would reject a valid re-fine.
This commit is contained in:
@@ -56,6 +56,46 @@ canApplyToBrokerCover(
|
||||
beast::Journal j,
|
||||
std::string_view logPrefix);
|
||||
|
||||
/**
|
||||
* Return a LoanBroker's current live cover exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use the exponent of CoverAvailable.
|
||||
* FixedPrecision Vaults floor that exponent at the Vault's base exponent.
|
||||
*
|
||||
* Reserved for fee redirection into cover. Cover deposit, withdraw, and
|
||||
* clawback round at the posterior live exponent instead.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getBrokerCoverScale(SLE::const_ref vault, SLE::const_ref broker);
|
||||
|
||||
/**
|
||||
* Return a LoanBroker's posterior live cover exponent after applying an
|
||||
* unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorBrokerCoverScale(SLE::const_ref vault, SLE::const_ref broker, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Round a cover delta at the LoanBroker's posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorBrokerCoverScale(
|
||||
SLE::const_ref vault,
|
||||
SLE::const_ref broker,
|
||||
STAmount const& delta,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Check whether `amount` is an admissible optional cover inflow.
|
||||
*
|
||||
* Legacy and CashBasis Vaults always succeed. A LoanBroker attached to a
|
||||
* FixedPrecision Vault must remain at the Vault's base scale after applying
|
||||
* the rounded amount, and its posterior CoverAvailable must stay within the
|
||||
* Open zone.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkOptionalBrokerCoverInflow(SLE::const_ref vault, SLE::const_ref broker, STAmount const& amount);
|
||||
|
||||
// Lending protocol has dependencies, so capture them here.
|
||||
bool
|
||||
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
|
||||
@@ -262,20 +302,16 @@ getAssetsTotalScale(SLE::const_ref vaultSle)
|
||||
return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset));
|
||||
}
|
||||
|
||||
// Compute the minimum required broker cover, rounded consistently.
|
||||
// DebtTotal is a broker-level aggregate maintained at vault scale, so the
|
||||
// rounding must also use vault scale — never an individual loan's scale.
|
||||
inline Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::const_ref vaultSle)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
tenthBipsOfValue(debtTotal, coverRateMinimum),
|
||||
getAssetsTotalScale(vaultSle));
|
||||
}
|
||||
/**
|
||||
* Minimum required broker cover, rounded up.
|
||||
*
|
||||
* DebtTotal is a broker-level aggregate, never rounded at an individual
|
||||
* loan's scale. Legacy and CashBasis Vaults round at the live AssetsTotal
|
||||
* exponent. FixedPrecision Vaults round at the Vault's base exponent
|
||||
* (`-Scale`, or 0 for integral assets).
|
||||
*/
|
||||
Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::const_ref vaultSle);
|
||||
|
||||
TER
|
||||
checkLoanGuards(
|
||||
|
||||
@@ -35,6 +35,18 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale)
|
||||
{
|
||||
if (reference == beast::kZero)
|
||||
return baseScale;
|
||||
return std::max(baseScale, scale(reference, asset));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] TER
|
||||
canApplyToBrokerCover(
|
||||
ReadView const& view,
|
||||
@@ -66,6 +78,126 @@ canApplyToBrokerCover(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getBrokerCoverScale(SLE::const_ref vault, SLE::const_ref broker)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::getBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::getBrokerCoverScale : valid LoanBroker sle");
|
||||
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(broker->at(sfCoverAvailable), vault->at(sfAsset));
|
||||
case VaultVersion::FixedPrecision:
|
||||
return liveScale(
|
||||
broker->at(sfCoverAvailable), vault->at(sfAsset), getVaultBaseScale(vault));
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getBrokerCoverScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getPosteriorBrokerCoverScale(SLE::const_ref vault, SLE::const_ref broker, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::getPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::getPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::getPosteriorBrokerCoverScale : delta and Vault asset match");
|
||||
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return broker->at(sfCoverAvailable) + 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), getVaultBaseScale(vault));
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getPosteriorBrokerCoverScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorBrokerCoverScale(
|
||||
SLE::const_ref vault,
|
||||
SLE::const_ref broker,
|
||||
STAmount const& delta,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::roundToPosteriorBrokerCoverScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::roundToPosteriorBrokerCoverScale : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::roundToPosteriorBrokerCoverScale : delta and Vault asset match");
|
||||
if (delta.integral())
|
||||
return delta;
|
||||
return roundToScale(delta, getPosteriorBrokerCoverScale(vault, broker, delta), roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkOptionalBrokerCoverInflow(SLE::const_ref vault, SLE::const_ref broker, STAmount const& amount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::checkOptionalBrokerCoverInflow : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
broker && broker->getType() == ltLOAN_BROKER,
|
||||
"xrpl::checkOptionalBrokerCoverInflow : valid LoanBroker sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::checkOptionalBrokerCoverInflow : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalBrokerCoverInflow : non-negative amount");
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return tesSUCCESS;
|
||||
|
||||
STAmount const rounded =
|
||||
roundToPosteriorBrokerCoverScale(vault, broker, amount, Number::RoundingMode::TowardsZero);
|
||||
int const baseScale = getVaultBaseScale(vault);
|
||||
if (getPosteriorBrokerCoverScale(vault, broker, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return broker->at(sfCoverAvailable) + rounded;
|
||||
}();
|
||||
if (posterior > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
Number
|
||||
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::const_ref vaultSle)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
tenthBipsOfValue(debtTotal, coverRateMinimum),
|
||||
getVaultBaseScale(vaultSle));
|
||||
}
|
||||
|
||||
bool
|
||||
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
@@ -158,7 +159,7 @@ determineAsset(
|
||||
|
||||
std::expected<STAmount, TER>
|
||||
determineClawAmount(
|
||||
SLE const& sleBroker,
|
||||
SLE::const_ref sleBroker,
|
||||
Asset const& vaultAsset,
|
||||
std::optional<STAmount> const& amount,
|
||||
SLE::const_ref vaultSle,
|
||||
@@ -166,20 +167,23 @@ determineClawAmount(
|
||||
{
|
||||
auto const maxClawAmount = [&]() {
|
||||
auto const minRequiredCover = [&]() {
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
if (rules.enabled(fixCleanup3_2_0) ||
|
||||
getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
return minimumBrokerCover(
|
||||
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]), vaultSle);
|
||||
sleBroker->at(sfDebtTotal),
|
||||
TenthBips32(sleBroker->at(sfCoverRateMinimum)),
|
||||
vaultSle);
|
||||
}
|
||||
|
||||
// Always round the minimum required up
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return tenthBipsOfValue(
|
||||
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
|
||||
sleBroker->at(sfDebtTotal), TenthBips32(sleBroker->at(sfCoverRateMinimum)));
|
||||
}();
|
||||
// The subtraction probably won't round, but round down if it does.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
|
||||
return sleBroker[sfCoverAvailable] - minRequiredCover;
|
||||
return sleBroker->at(sfCoverAvailable) - minRequiredCover;
|
||||
}();
|
||||
if (maxClawAmount <= beast::kZero)
|
||||
return std::unexpected(tecINSUFFICIENT_FUNDS);
|
||||
@@ -187,12 +191,24 @@ determineClawAmount(
|
||||
// Use the vaultAsset here, because it will be the right type in all
|
||||
// circumstances. The amount may be an IOU indicating the pseudo-account's
|
||||
// asset, which is correct, but not what is needed here.
|
||||
if (!amount || *amount == beast::kZero)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
Number const magnitude{*amount};
|
||||
if (magnitude > maxClawAmount)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
return STAmount{vaultAsset, magnitude};
|
||||
STAmount const requested = [&] {
|
||||
if (!amount || *amount == beast::kZero)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
Number const magnitude{*amount};
|
||||
if (magnitude > maxClawAmount)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
return STAmount{vaultAsset, magnitude};
|
||||
}();
|
||||
|
||||
if (getVaultVersion(vaultSle) != VaultVersion::FixedPrecision)
|
||||
return requested;
|
||||
|
||||
// Negate so the posterior is CoverAvailable minus amount.
|
||||
STAmount const rounded = -roundToPosteriorBrokerCoverScale(
|
||||
vaultSle, sleBroker, -requested, Number::RoundingMode::TowardsZero);
|
||||
if (rounded == beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
return rounded;
|
||||
}
|
||||
|
||||
template <ValidIssueType T>
|
||||
@@ -294,7 +310,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
|
||||
auto const findClawAmount =
|
||||
determineClawAmount(*sleBroker, vaultAsset, amount, vault, ctx.view.rules());
|
||||
determineClawAmount(sleBroker, vaultAsset, amount, vault, ctx.view.rules());
|
||||
if (!findClawAmount)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
|
||||
@@ -302,9 +318,15 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
STAmount const& clawAmount = *findClawAmount;
|
||||
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
|
||||
return ret;
|
||||
// FixedPrecision outflows already rounded at the posterior exponent; the
|
||||
// live CoverAvailable scale used by canApplyToBrokerCover would reject a
|
||||
// re-fining clawback as sub-ULP.
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Explicitly check the balance of the trust line / MPT to make sure the
|
||||
// balance is actually there. It should always match `sfCoverAvailable`, so
|
||||
@@ -357,7 +379,7 @@ LoanBrokerCoverClawback::doApply()
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
|
||||
auto const findClawAmount =
|
||||
determineClawAmount(*sleBroker, vaultAsset, amount, vault, view().rules());
|
||||
determineClawAmount(sleBroker, vaultAsset, amount, vault, view().rules());
|
||||
if (!findClawAmount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
STAmount const& clawAmount = *findClawAmount;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -104,22 +105,30 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
|
||||
// here in preclaim lets us reject sub-cover-scale dust early with tecPRECISION_LOSS instead of
|
||||
// failing only in doApply.
|
||||
auto const roundedAmount = [&]() -> STAmount {
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
return roundToPosteriorBrokerCoverScale(
|
||||
vault, sleBroker, amount, Number::RoundingMode::TowardsZero);
|
||||
if (!fix320Enabled)
|
||||
return tx[sfAmount];
|
||||
return amount;
|
||||
|
||||
return roundToScale(
|
||||
tx[sfAmount],
|
||||
amount,
|
||||
scale(sleBroker->at(sfCoverAvailable), vaultAsset),
|
||||
Number::RoundingMode::Downward);
|
||||
}();
|
||||
|
||||
if (fix320Enabled && roundedAmount == beast::kZero)
|
||||
if ((fix320Enabled || getVaultVersion(vault) == VaultVersion::FixedPrecision) &&
|
||||
roundedAmount == beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << amount
|
||||
<< " is zero at loan broker scale";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
if (auto const ter = checkOptionalBrokerCoverInflow(vault, sleBroker, roundedAmount);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
account,
|
||||
@@ -155,6 +164,9 @@ LoanBrokerCoverDeposit::doApply()
|
||||
// see the rationale comment in preclaim.
|
||||
bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0);
|
||||
auto const amount = [&]() -> STAmount {
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
return roundToPosteriorBrokerCoverScale(
|
||||
vault, broker, tx[sfAmount], Number::RoundingMode::TowardsZero);
|
||||
if (!fix320Enabled)
|
||||
return tx[sfAmount];
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -103,10 +104,29 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
if (amount.asset() != vaultAsset)
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
// Helper handles both IOU and MPT correctly without explicit branching.
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, amount, ctx.j, "LoanBrokerCoverWithdraw"))
|
||||
return ret;
|
||||
auto const roundedAmount = [&] {
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return amount;
|
||||
// Negate so the posterior is CoverAvailable minus amount.
|
||||
return -roundToPosteriorBrokerCoverScale(
|
||||
vault, sleBroker, -amount, Number::RoundingMode::TowardsZero);
|
||||
}();
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision && roundedAmount == beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverWithdraw: withdraw amount: " << amount
|
||||
<< " is zero at loan broker scale";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
// FixedPrecision outflows already rounded at the posterior exponent; the
|
||||
// live CoverAvailable scale used by canApplyToBrokerCover would reject a
|
||||
// re-fining withdrawal as sub-ULP.
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, roundedAmount, ctx.j, "LoanBrokerCoverWithdraw"))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// The broker's pseudo-account is the source of funds.
|
||||
auto const pseudoAccountID = sleBroker->at(sfAccount);
|
||||
@@ -170,7 +190,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
// Cover Rate is in 1/10 bips units
|
||||
auto const currentDebtTotal = sleBroker->at(sfDebtTotal);
|
||||
auto const minimumCover = [&]() {
|
||||
if (fix320Enabled)
|
||||
if (fix320Enabled || getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
return minimumBrokerCover(
|
||||
currentDebtTotal, TenthBips32{sleBroker->at(sfCoverRateMinimum)}, vault);
|
||||
@@ -184,9 +204,9 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
|
||||
scale(currentDebtTotal, vaultAsset));
|
||||
}();
|
||||
if (coverAvail < amount)
|
||||
if (coverAvail < roundedAmount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
if ((coverAvail - amount) < minimumCover)
|
||||
if ((coverAvail - roundedAmount) < minimumCover)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
auto const freezeHandling = fix330Enabled && dstAcct == vaultAsset.getIssuer()
|
||||
@@ -199,7 +219,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
vaultAsset,
|
||||
freezeHandling,
|
||||
AuthHandling::ZeroIfUnauthorized,
|
||||
ctx.j) < amount)
|
||||
ctx.j) < roundedAmount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -211,7 +231,7 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
auto const brokerID = tx[sfLoanBrokerID];
|
||||
auto const amount = tx[sfAmount];
|
||||
auto const requestedAmount = tx[sfAmount];
|
||||
auto const dstAcct = tx[~sfDestination].value_or(accountID_);
|
||||
|
||||
auto broker = view().peek(keylet::loanBroker(brokerID));
|
||||
@@ -223,6 +243,11 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
auto const amount = getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
// Negate so the posterior is CoverAvailable minus amount.
|
||||
? -roundToPosteriorBrokerCoverScale(
|
||||
vault, broker, -requestedAmount, Number::RoundingMode::TowardsZero)
|
||||
: requestedAmount;
|
||||
|
||||
auto const brokerPseudoID = *broker->at(sfAccount);
|
||||
|
||||
|
||||
@@ -174,7 +174,12 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
|
||||
// type. This is mostly only relevant for integral (non-IOU) types
|
||||
for (auto const& field : getValueFields())
|
||||
{
|
||||
if (auto const value = tx[field]; value && STAmount{asset, *value} != *value)
|
||||
if (auto const value = tx[field]; value &&
|
||||
(STAmount{asset, *value} != *value ||
|
||||
(getVaultVersion(sleVault) == VaultVersion::FixedPrecision &&
|
||||
roundToAsset(
|
||||
asset, *value, getVaultBaseScale(sleVault), Number::RoundingMode::TowardsZero) !=
|
||||
*value)))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
|
||||
<< ") can not be represented as a(n) " << to_string(asset) << ".";
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -375,7 +376,8 @@ LoanPay::doApply()
|
||||
// DebtTotal) use vaultScale. The legacy path below intentionally retains
|
||||
// its pre-amendment loanScale behavior.
|
||||
auto const minCover = [&]() {
|
||||
if (view.rules().enabled(fixCleanup3_2_0))
|
||||
if (view.rules().enabled(fixCleanup3_2_0) ||
|
||||
getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
return minimumBrokerCover(debtTotalProxy.value(), coverRateMinimum, vaultSle);
|
||||
}
|
||||
|
||||
@@ -559,7 +559,8 @@ LoanSet::doApply()
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
{
|
||||
auto const minCover = [&]() {
|
||||
if (ctx_.view().rules().enabled(fixCleanup3_2_0))
|
||||
if (ctx_.view().rules().enabled(fixCleanup3_2_0) ||
|
||||
getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
return minimumBrokerCover(newDebtTotal, coverRateMinimum, vaultSle);
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
|
||||
#include <cstdint>
|
||||
@@ -1862,6 +1864,17 @@ public:
|
||||
.amount = STAmount{iou, Number{1, -13}},
|
||||
.expected = tesSUCCESS,
|
||||
},
|
||||
{
|
||||
// CoverAvailable 1e10 is exponent -5. 1e-6 is non-zero at
|
||||
// FixedPrecision P=6, but ToNearest at the live cover
|
||||
// exponent rounds it to zero. Withdraw/clawback skip this
|
||||
// helper for FixedPrecision so a re-fining outflow can
|
||||
// succeed.
|
||||
.name = "Coarsened live scale rejects re-fining amount",
|
||||
.coverAvailable = Number{1, 10},
|
||||
.amount = STAmount{iou, Number{1, -6}},
|
||||
.expected = tecPRECISION_LOSS,
|
||||
},
|
||||
};
|
||||
|
||||
Env const env{*this};
|
||||
@@ -1891,6 +1904,26 @@ public:
|
||||
envOff.journal,
|
||||
"test") == tesSUCCESS);
|
||||
}
|
||||
|
||||
testcase("minimumBrokerCover: FixedPrecision uses base scale");
|
||||
auto const makeVault = [&](VaultVersion version) {
|
||||
auto vault = std::make_shared<SLE>(ltVAULT, uint256{2u});
|
||||
vault->setFieldIssue(sfAsset, STIssue{sfAsset, iou});
|
||||
vault->at(sfAssetsTotal) = Number{1};
|
||||
vault->at(sfScale) = 6;
|
||||
vault->at(sfLEVersion) = std::to_underlying(version);
|
||||
associateAsset(*vault, iou);
|
||||
return vault;
|
||||
};
|
||||
|
||||
Number const debtTotal{15, -2};
|
||||
TenthBips32 const coverRate{1};
|
||||
BEAST_EXPECT(
|
||||
(minimumBrokerCover(debtTotal, coverRate, makeVault(VaultVersion::FixedPrecision)) ==
|
||||
Number{2, -6}));
|
||||
BEAST_EXPECT(
|
||||
(minimumBrokerCover(debtTotal, coverRate, makeVault(VaultVersion::CashBasis)) ==
|
||||
Number{15, -7}));
|
||||
}
|
||||
|
||||
// Targeted unit test for getLoanDefaultFreezeExemptAccounts(): builds a real
|
||||
|
||||
@@ -73,12 +73,12 @@ class LoanBroker_test : public beast::unit_test::Suite
|
||||
// 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 adds
|
||||
// the closed-ended vault gate on LoanBrokerSet::preclaim (see
|
||||
// LoanBrokerSet.cpp), but this suite exercises loan-broker mechanics on
|
||||
// plain open-ended vaults. Tests that specifically exercise the
|
||||
// amendment opt it back in explicitly and use closed-ended vaults.
|
||||
FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1};
|
||||
// V1.1 and V1.2 are excluded from the default set: they add the
|
||||
// closed-ended Vault gate and fixed-precision behavior, while this suite
|
||||
// primarily exercises legacy LoanBroker mechanics on open-ended Vaults.
|
||||
// Tests for the new behavior opt both amendments back in explicitly.
|
||||
FeatureBitset const all_{
|
||||
jtx::testableAmendments() - featureLendingProtocolV1_1 - featureLendingProtocolV1_2};
|
||||
|
||||
void
|
||||
testDisabled()
|
||||
@@ -2757,6 +2757,135 @@ class LoanBroker_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(!env.le(credKeylet));
|
||||
}
|
||||
|
||||
void
|
||||
testFixedPrecisionCover()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace loan_broker;
|
||||
|
||||
testcase("FixedPrecision LoanBroker cover");
|
||||
|
||||
FeatureBitset const v12{all_ | featureLendingProtocolV1_1 | featureLendingProtocolV1_2};
|
||||
Account const issuer{"issuer"};
|
||||
Account const alice{"alice"};
|
||||
Account const borrower{"borrower"};
|
||||
Env env{*this, v12};
|
||||
env.fund(XRP(100'000), issuer, alice, borrower);
|
||||
env.close();
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const iou = issuer["IOU"];
|
||||
env(trust(alice, iou(Number{10, 10})));
|
||||
env(trust(borrower, iou(Number{10, 10})));
|
||||
env(pay(issuer, alice, iou(Number{10, 9})));
|
||||
env(pay(issuer, borrower, iou(Number{10, 2})));
|
||||
|
||||
Vault const vault{env};
|
||||
[[maybe_unused]] auto [createTx, vaultKeylet, subscriptionDate] =
|
||||
vault.createClosedEnded({.owner = alice, .asset = iou});
|
||||
createTx[sfScale] = 6;
|
||||
env(createTx);
|
||||
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = iou(100)}));
|
||||
vault.closePastSubscription(subscriptionDate);
|
||||
|
||||
env(set(alice, vaultKeylet.key), kDebtMaximum(Number{15, -7}), Ter(tecPRECISION_LOSS));
|
||||
|
||||
auto const brokerKeylet =
|
||||
keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
|
||||
env(set(alice, vaultKeylet.key), kDebtMaximum(Number{1, -6}));
|
||||
|
||||
env(coverDeposit(alice, brokerKeylet.key, iou(Number{1, -7})), Ter(tecPRECISION_LOSS));
|
||||
|
||||
Number const openLimit{9, 9};
|
||||
env(coverDeposit(alice, brokerKeylet.key, iou(Number{18, -7})));
|
||||
{
|
||||
auto const broker = env.le(brokerKeylet);
|
||||
BEAST_EXPECT(broker);
|
||||
if (broker)
|
||||
BEAST_EXPECT((broker->at(sfCoverAvailable) == Number{1, -6}));
|
||||
}
|
||||
env(coverDeposit(alice, brokerKeylet.key, iou(openLimit - Number{1, -6})));
|
||||
env(coverDeposit(alice, brokerKeylet.key, iou(Number{1, -6})), Ter(tecLIMIT_EXCEEDED));
|
||||
|
||||
auto const coverAvailable = [&]() {
|
||||
auto const broker = env.le(brokerKeylet);
|
||||
BEAST_EXPECT(broker);
|
||||
return broker ? broker->at(sfCoverAvailable) : Number{0};
|
||||
};
|
||||
BEAST_EXPECT(coverAvailable() == openLimit);
|
||||
|
||||
env(coverWithdraw(alice, brokerKeylet.key, iou(Number{18, -7})));
|
||||
BEAST_EXPECT((coverAvailable() == openLimit - Number{1, -6}));
|
||||
|
||||
env(coverClawback(issuer), kLoanBrokerId(brokerKeylet.key), kAmount(iou(Number{18, -7})));
|
||||
BEAST_EXPECT((coverAvailable() == openLimit - Number{2, -6}));
|
||||
|
||||
env(coverWithdraw(alice, brokerKeylet.key, iou(Number{1, -7})), Ter(tecPRECISION_LOSS));
|
||||
env(coverClawback(issuer),
|
||||
kLoanBrokerId(brokerKeylet.key),
|
||||
kAmount(iou(Number{1, -7})),
|
||||
Ter(tecPRECISION_LOSS));
|
||||
|
||||
env(coverClawback(issuer), kLoanBrokerId(brokerKeylet.key));
|
||||
BEAST_EXPECT((coverAvailable() == Number{0}));
|
||||
|
||||
auto const minCoverBroker =
|
||||
keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
|
||||
env(set(alice, vaultKeylet.key),
|
||||
kDebtMaximum(Number{100}),
|
||||
kCoverRateMinimum(percentageToTenthBips(10)),
|
||||
kCoverRateLiquidation(percentageToTenthBips(25)));
|
||||
env(coverDeposit(alice, minCoverBroker.key, iou(Number{1, -1})));
|
||||
env(loan::set(borrower, minCoverBroker.key, Number{1}),
|
||||
Sig(sfCounterpartySignature, alice),
|
||||
Fee(env.current()->fees().base * 2));
|
||||
{
|
||||
auto const broker = env.le(minCoverBroker);
|
||||
BEAST_EXPECT(broker);
|
||||
if (broker)
|
||||
BEAST_EXPECT((broker->at(sfDebtTotal) == Number{1}));
|
||||
}
|
||||
env(coverWithdraw(alice, minCoverBroker.key, iou(Number{1, -6})),
|
||||
Ter(tecINSUFFICIENT_FUNDS));
|
||||
env(loan::set(borrower, minCoverBroker.key, Number{1}),
|
||||
Sig(sfCounterpartySignature, alice),
|
||||
Fee(env.current()->fees().base * 2),
|
||||
Ter(tecINSUFFICIENT_FUNDS));
|
||||
auto const loanKeylet = keylet::loan(minCoverBroker.key, SeqProxy::rawSequence(1));
|
||||
env(loan::pay(borrower, loanKeylet.key, iou(1).value()));
|
||||
|
||||
{
|
||||
testcase("FixedPrecision LoanBroker cover: XRP");
|
||||
[[maybe_unused]] auto [xrpTx, xrpVault, xrpSub] =
|
||||
vault.createClosedEnded({.owner = alice, .asset = xrpIssue()});
|
||||
env(xrpTx);
|
||||
auto const xrpBroker =
|
||||
keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
|
||||
env(set(alice, xrpVault.key));
|
||||
env(coverDeposit(alice, xrpBroker.key, XRP(10)));
|
||||
env(coverWithdraw(alice, xrpBroker.key, XRP(1)));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("FixedPrecision LoanBroker cover: MPT");
|
||||
MPTTester mptt{env, issuer, kMptInitNoFund};
|
||||
mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
|
||||
PrettyAsset const mpt = mptt["MPT"];
|
||||
mptt.authorize({.account = alice});
|
||||
env(pay(issuer, alice, mpt(100)));
|
||||
[[maybe_unused]] auto [mptTx, mptVault, mptSub] =
|
||||
vault.createClosedEnded({.owner = alice, .asset = mpt});
|
||||
env(mptTx);
|
||||
auto const mptBroker =
|
||||
keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
|
||||
env(set(alice, mptVault.key));
|
||||
env(coverDeposit(alice, mptBroker.key, mpt(10).value()));
|
||||
env(coverWithdraw(alice, mptBroker.key, mpt(1).value()));
|
||||
env(coverClawback(issuer), kLoanBrokerId(mptBroker.key), kAmount(mpt(1)));
|
||||
}
|
||||
}
|
||||
|
||||
// Exercises canApplyToBrokerCover (fixCleanup3_2_0): a deposit, withdraw,
|
||||
// or clawback whose amount rounds to zero at sfCoverAvailable's precision
|
||||
// scale must be rejected with tecPRECISION_LOSS once the amendment is on,
|
||||
@@ -2979,6 +3108,7 @@ public:
|
||||
testCoverWithdrawFreezes();
|
||||
testCoverWithdrawSelfWhileFrozen();
|
||||
|
||||
testFixedPrecisionCover();
|
||||
testCoverPrecisionGuard();
|
||||
|
||||
testLoanBrokerSetDebtMaximum();
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.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/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -44,6 +47,15 @@ makeVault(
|
||||
return vault;
|
||||
}
|
||||
|
||||
std::shared_ptr<SLE>
|
||||
makeBroker(Asset const& asset, Number const& coverAvailable)
|
||||
{
|
||||
auto broker = std::make_shared<SLE>(ltLOAN_BROKER, uint256{2u});
|
||||
broker->at(sfCoverAvailable) = coverAvailable;
|
||||
associateAsset(*broker, asset);
|
||||
return broker;
|
||||
}
|
||||
|
||||
TEST(VaultGrid, BaseAndLiveScale)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
@@ -207,5 +219,65 @@ TEST(VaultGrid, OptionalInflowIncludesYieldUnrealized)
|
||||
EXPECT_EQ(checkOptionalVaultInflow(vault, amount), tecLIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, BrokerCoverScaleAndRounding)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault = makeVault(iou, Number{0}, VaultVersion::FixedPrecision, 6);
|
||||
auto broker = makeBroker(iou, Number{9'999'999'999'999'999, -6});
|
||||
STAmount const inflow{iou, Number{21, -6}};
|
||||
|
||||
EXPECT_EQ(getBrokerCoverScale(vault, broker), -6);
|
||||
EXPECT_EQ(getPosteriorBrokerCoverScale(vault, broker, inflow), -5);
|
||||
EXPECT_EQ(
|
||||
roundToPosteriorBrokerCoverScale(vault, broker, inflow, Number::RoundingMode::TowardsZero),
|
||||
STAmount(iou, Number{20, -6}));
|
||||
|
||||
broker->at(sfCoverAvailable) = Number{1'000'000'000'000'001, -5};
|
||||
associateAsset(*broker, iou);
|
||||
STAmount const outflow{iou, -Number{11, -6}};
|
||||
EXPECT_EQ(getBrokerCoverScale(vault, broker), -5);
|
||||
EXPECT_EQ(getPosteriorBrokerCoverScale(vault, broker, outflow), -6);
|
||||
EXPECT_EQ(
|
||||
roundToPosteriorBrokerCoverScale(vault, broker, outflow, Number::RoundingMode::TowardsZero),
|
||||
outflow);
|
||||
|
||||
// CoverAvailable exactly 1e10 (exponent -5). Withdrawing 1e-6 re-fines
|
||||
// to -6; the posterior rounded amount is 1e-6, which isZeroAtScale(-5)
|
||||
// would treat as zero.
|
||||
broker->at(sfCoverAvailable) = Number{1, 10};
|
||||
associateAsset(*broker, iou);
|
||||
STAmount const refine{iou, -Number{1, -6}};
|
||||
EXPECT_EQ(getBrokerCoverScale(vault, broker), -5);
|
||||
EXPECT_EQ(getPosteriorBrokerCoverScale(vault, broker, refine), -6);
|
||||
EXPECT_EQ(
|
||||
roundToPosteriorBrokerCoverScale(vault, broker, refine, Number::RoundingMode::TowardsZero),
|
||||
refine);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, BrokerCoverOptionalInflowBoundaries)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
|
||||
auto fixedIou = makeVault(iou, Number{0}, VaultVersion::FixedPrecision, 10);
|
||||
auto iouBroker = makeBroker(iou, Number{9, 5});
|
||||
STAmount const iouUnit{iou, Number{1, -10}};
|
||||
EXPECT_EQ(checkOptionalBrokerCoverInflow(fixedIou, iouBroker, STAmount{iou}), tesSUCCESS);
|
||||
EXPECT_EQ(checkOptionalBrokerCoverInflow(fixedIou, iouBroker, iouUnit), tecLIMIT_EXCEEDED);
|
||||
|
||||
auto legacy = makeVault(iou, Number{0}, VaultVersion::CashBasis, 10);
|
||||
EXPECT_EQ(checkOptionalBrokerCoverInflow(legacy, iouBroker, iouUnit), tesSUCCESS);
|
||||
|
||||
for (Asset const asset : {Asset{xrpIssue()}, Asset{MPTIssue{makeMptID(1, issuer.id())}}})
|
||||
{
|
||||
auto vault = makeVault(asset, Number{0}, VaultVersion::FixedPrecision, 0);
|
||||
auto broker = makeBroker(asset, Number{9, 15});
|
||||
EXPECT_EQ(
|
||||
checkOptionalBrokerCoverInflow(vault, broker, STAmount{asset, std::uint64_t{1}}),
|
||||
tecLIMIT_EXCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl
|
||||
|
||||
Reference in New Issue
Block a user