Fix PR comments

Signed-off-by: JCW <a1q123456@users.noreply.github.com>
This commit is contained in:
JCW
2026-03-26 23:56:54 +00:00
parent e74a24bced
commit 0bc4be2b9c
9 changed files with 544 additions and 82 deletions

View File

@@ -514,6 +514,7 @@ LEDGER_ENTRY(ltLOAN_BROKER, 0x0088, LoanBroker, loan_broker, ({
{sfDebtMaximum, soeDEFAULT},
{sfCoverAvailable, soeDEFAULT},
{sfCoverRateMinimum, soeDEFAULT},
// Deprecated by featureLendingProtocolV1_1
{sfCoverRateLiquidation, soeDEFAULT},
}))

View File

@@ -108,6 +108,7 @@ TYPED_SFIELD(sfPaymentRemaining, UINT32, 59)
TYPED_SFIELD(sfPaymentTotal, UINT32, 60)
TYPED_SFIELD(sfLoanSequence, UINT32, 61)
TYPED_SFIELD(sfCoverRateMinimum, UINT32, 62) // 1/10 basis points (bips)
// Deprecated by featureLendingProtocolV1_1
TYPED_SFIELD(sfCoverRateLiquidation, UINT32, 63) // 1/10 basis points (bips)
TYPED_SFIELD(sfOverpaymentFee, UINT32, 64) // 1/10 basis points (bips)
TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bips)

View File

@@ -960,6 +960,7 @@ TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet,
{sfManagementFeeRate, soeOPTIONAL},
{sfDebtMaximum, soeOPTIONAL},
{sfCoverRateMinimum, soeOPTIONAL},
// Deprecated by featureLendingProtocolV1_1
{sfCoverRateLiquidation, soeOPTIONAL},
}))

View File

@@ -219,6 +219,49 @@ computeFullPaymentInterest(
std::uint32_t startDate,
TenthBips32 closeInterestRate);
/** Whether to use the proportional (new) default cover formula.
*
* Returns true when featureLendingProtocolV1_1 is enabled AND the broker
* does not carry the deprecated sfCoverRateLiquidation field.
*/
inline bool
useProportionalDefaultCover(Rules const& rules, std::shared_ptr<SLE const> brokerSle)
{
return rules.enabled(featureLendingProtocolV1_1) &&
!brokerSle->isFieldPresent(sfCoverRateLiquidation);
}
/** Compute the amount of First-Loss Capital seized to cover a defaulted loan.
*
* Selects between the old (global) and new (proportional) formula based on
* whether featureLendingProtocolV1_1 is enabled and whether the broker still
* carries the deprecated sfCoverRateLiquidation value.
*
* @param useProportionalFormula true when featureLendingProtocolV1_1 is
* enabled AND the broker has no
* sfCoverRateLiquidation.
* @param coverRateLiquidation The broker's CoverRateLiquidation in 1/10
* bips. Only used by the old formula; ignored
* when \p useProportionalFormula is true.
* @param coverAvailable The broker's current CoverAvailable.
* @param vaultAsset The Vault's asset type (for rounding).
* @param totalDefaultAmount The loan's default amount (owed to the vault).
* @param brokerDebtTotal The broker's total debt before this default.
* @param coverRateMinimum The broker's CoverRateMinimum in 1/10 bips.
* @param loanScale The loan's rounding scale.
* @return The amount of cover seized, capped at \p coverAvailable.
*/
Number
computeDefaultCovered(
bool useProportionalFormula,
std::uint32_t coverRateLiquidation,
Number const& coverAvailable,
Asset const& vaultAsset,
Number const& totalDefaultAmount,
Number const& brokerDebtTotal,
TenthBips32 coverRateMinimum,
std::int32_t loanScale);
namespace detail {
// These classes and functions should only be accessed by LendingHelper
// functions and unit tests

View File

@@ -64,6 +64,50 @@ isRounded(Asset const& asset, Number const& value, std::int32_t scale)
roundToAsset(asset, value, scale, Number::upward);
}
Number
computeDefaultCovered(
bool useProportionalFormula,
std::uint32_t coverRateLiquidation,
Number const& coverAvailable,
Asset const& vaultAsset,
Number const& totalDefaultAmount,
Number const& brokerDebtTotal,
TenthBips32 coverRateMinimum,
std::int32_t loanScale)
{
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
Number covered;
if (useProportionalFormula)
{
// New formula: DefaultCovered = min(DefaultAmount × CoverRateMinimum,
// CoverAvailable)
covered = roundToAsset(
vaultAsset, tenthBipsOfValue(totalDefaultAmount, coverRateMinimum), loanScale);
}
else
{
// Old formula (deprecated by featureLendingProtocolV1_1):
// Kept for backwards compatibility with brokers that still carry
// sfCoverRateLiquidation.
auto const minimumCover = tenthBipsOfValue(brokerDebtTotal, coverRateMinimum);
covered = roundToAsset(
vaultAsset,
/*
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
* Changes), specifically "if the `tfLoanDefault` flag is set" /
* "Apply the First-Loss Capital to the Default Amount"
*/
std::min(
tenthBipsOfValue(minimumCover, TenthBips32{coverRateLiquidation}),
totalDefaultAmount),
loanScale);
}
return std::min(covered, coverAvailable);
}
namespace detail {
void

View File

@@ -49,6 +49,9 @@ LoanBrokerSet::preflight(PreflightContext const& ctx)
return temINVALID;
}
// sfCoverRateLiquidation is deprecated by featureLendingProtocolV1_1;
// only enforce consistency when the amendment is not enabled.
if (!ctx.rules.enabled(featureLendingProtocolV1_1))
{
auto const minimumZero = tx[~sfCoverRateMinimum].value_or(0) == 0;
auto const liquidationZero = tx[~sfCoverRateLiquidation].value_or(0) == 0;
@@ -249,7 +252,8 @@ LoanBrokerSet::doApply()
broker->at(sfDebtMaximum) = *debtMax;
if (auto const coverMin = tx[~sfCoverRateMinimum])
broker->at(sfCoverRateMinimum) = *coverMin;
if (auto const coverLiq = tx[~sfCoverRateLiquidation])
if (auto const coverLiq = tx[~sfCoverRateLiquidation];
coverLiq && !view.rules().enabled(featureLendingProtocolV1_1))
broker->at(sfCoverRateLiquidation) = *coverLiq;
view.insert(broker);

View File

@@ -141,41 +141,16 @@ LoanManage::defaultLoan(
// Apply the First-Loss Capital to the Default Amount
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
auto const coverRateLiquidation{brokerSle->at(~sfCoverRateLiquidation)};
auto const defaultCovered = [&]() {
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
Number covered;
if (view.rules().enabled(featureLendingProtocolV1_1) && !coverRateLiquidation)
{
// New formula: DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable)
// Round the liquidation amount up
covered = roundToAsset(
vaultAsset, tenthBipsOfValue(totalDefaultAmount, coverRateMinimum), loanScale);
}
else
{
auto const minimumCover =
tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
// Round the liquidation amount up
covered = roundToAsset(
vaultAsset,
/*
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
* Changes), specifically "if the `tfLoanDefault` flag is set" /
* "Apply the First-Loss Capital to the Default Amount"
*/
std::min(
tenthBipsOfValue(minimumCover, TenthBips32{coverRateLiquidation.value_or(0)}),
totalDefaultAmount),
loanScale);
}
auto const coverAvailable = *brokerSle->at(sfCoverAvailable);
return std::min(covered, coverAvailable);
}();
auto const defaultCovered = computeDefaultCovered(
useProportionalDefaultCover(view.rules(), brokerSle),
brokerSle->at(sfCoverRateLiquidation),
*brokerSle->at(sfCoverAvailable),
vaultAsset,
totalDefaultAmount,
brokerDebtTotalProxy.value(),
coverRateMinimum,
loanScale);
auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;

View File

@@ -1185,6 +1185,199 @@ class LendingHelpers_test : public beast::unit_test::suite
to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid));
}
void
testComputeDefaultCovered()
{
testcase("computeDefaultCovered");
using namespace jtx;
// ---- Common parameters ----
Asset const asset{xrpIssue()};
std::int32_t const loanScale = 1;
// coverRateLiquidation value used by old-formula tests (100%).
std::uint32_t const covRateLiq = 100'000;
// ---- Test 1: New formula basic ----
// DefaultCovered = min(DefaultAmount × CoverRateMinimum,
// CoverAvailable)
// 100,000 × 20% = 20,000; min(20,000, 50,000) = 20,000
{
auto result = computeDefaultCovered(
true, // useProportionalFormula
0, // coverRateLiquidation (unused)
Number{50'000}, // coverAvailable
asset,
Number{100'000}, // totalDefaultAmount
Number{200'000}, // brokerDebtTotal (unused)
TenthBips32{20'000}, // 20%
loanScale);
BEAST_EXPECT(result == Number{20'000});
}
// ---- Test 2: New formula capped by CoverAvailable ----
// 100,000 × 50% = 50,000; min(50,000, 10,000) = 10,000
{
auto result = computeDefaultCovered(
true,
0,
Number{10'000},
asset,
Number{100'000},
Number{200'000},
TenthBips32{50'000}, // 50%
loanScale);
BEAST_EXPECT(result == Number{10'000});
}
// ---- Test 3: Old formula basic ----
// min(CovRateLiq × (CovRateMin × BrokerDebtTotal), DefaultAmount)
// minimumCover = 200,000 × 20% = 40,000
// covered = min(100% × 40,000, 50,000) = 40,000
// min(40,000, 100,000) = 40,000
{
auto result = computeDefaultCovered(
false, // old formula
covRateLiq, // 100%
Number{100'000},
asset,
Number{50'000},
Number{200'000},
TenthBips32{20'000},
loanScale);
BEAST_EXPECT(result == Number{40'000});
}
// ---- Test 4: Old formula capped by DefaultAmount ----
// minimumCover = 200,000 × 50% = 100,000
// covered = min(100% × 100,000, 30,000) = 30,000
// min(30,000, 500,000) = 30,000
{
auto result = computeDefaultCovered(
false,
covRateLiq,
Number{500'000},
asset,
Number{30'000},
Number{200'000},
TenthBips32{50'000},
loanScale);
BEAST_EXPECT(result == Number{30'000});
}
// ---- Test 5: Old formula capped by CoverAvailable ----
// minimumCover = 200,000 × 20% = 40,000
// covered = min(100% × 40,000, 100,000) = 40,000
// min(40,000, 5,000) = 5,000
{
auto result = computeDefaultCovered(
false,
covRateLiq,
Number{5'000}, // small CoverAvailable
asset,
Number{100'000},
Number{200'000},
TenthBips32{20'000},
loanScale);
BEAST_EXPECT(result == Number{5'000});
}
// ---- Test 6: Backwards compatibility ----
// useProportionalFormula = false even though the amendment is
// enabled, because the broker was created before the amendment
// and still carries sfCoverRateLiquidation. The caller passes
// false in this case; we verify the old formula is used.
// minimumCover = 200,000 × 20% = 40,000
// covered = min(100% × 40,000, 50,000) = 40,000
// min(40,000, 100,000) = 40,000
{
auto result = computeDefaultCovered(
false, // old formula (backwards compat)
covRateLiq,
Number{100'000},
asset,
Number{50'000},
Number{200'000},
TenthBips32{20'000},
loanScale);
BEAST_EXPECT(result == Number{40'000});
}
// ---- Test 7: New vs old produce different results ----
// Same inputs, different formula selection → different outputs.
{
auto resultNew = computeDefaultCovered(
true,
0,
Number{100'000},
asset,
Number{50'000},
Number{200'000},
TenthBips32{20'000},
loanScale);
auto resultOld = computeDefaultCovered(
false,
covRateLiq,
Number{100'000},
asset,
Number{50'000},
Number{200'000},
TenthBips32{20'000},
loanScale);
// New: 50,000 × 20% = 10,000
BEAST_EXPECT(resultNew == Number{10'000});
// Old: min(100% × (20% × 200,000), 50,000)
// = min(40,000, 50,000) = 40,000
BEAST_EXPECT(resultOld == Number{40'000});
BEAST_EXPECT(resultNew != resultOld);
}
// ---- Test 8: Zero CoverAvailable ----
{
auto result = computeDefaultCovered(
true,
0,
Number{0},
asset,
Number{100'000},
Number{200'000},
TenthBips32{20'000},
loanScale);
BEAST_EXPECT(result == Number{0});
}
// ---- Test 9: Zero DefaultAmount ----
{
auto result = computeDefaultCovered(
true,
0,
Number{50'000},
asset,
Number{0},
Number{200'000},
TenthBips32{20'000},
loanScale);
BEAST_EXPECT(result == Number{0});
}
// ---- Test 10: Zero CoverRateMinimum (new formula) ----
// 100,000 × 0% = 0
{
auto result = computeDefaultCovered(
true,
0,
Number{50'000},
asset,
Number{100'000},
Number{200'000},
TenthBips32{0},
loanScale);
BEAST_EXPECT(result == Number{0});
}
}
public:
void
run() override
@@ -1205,6 +1398,7 @@ public:
testComputePaymentFactor();
testComputeOverpaymentComponents();
testComputeInterestAndFeeParts();
testComputeDefaultCovered();
}
};

View File

@@ -1945,31 +1945,15 @@ protected:
broker.vaultScale(env), state.principalOutstanding.exponent())));
NumberRoundModeGuard mg(Number::upward);
auto const totalDefaultAmount = state.totalValue - state.managementFeeOutstanding;
auto const defaultAmount = [&] {
if (env.enabled(featureLendingProtocolV1_1))
{
// DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable)
return roundToAsset(
broker.asset,
tenthBipsOfValue(totalDefaultAmount, broker.params.coverRateMin),
state.loanScale);
}
else
{
// From XLS-66 spec, section 3.2.3.2:
// DefaultCovered = min(DebtTotal × CoverRateMinimum × CoverRateLiquidation,
// DefaultAmount, CoverAvailable)
return roundToAsset(
broker.asset,
std::min(
tenthBipsOfValue(
tenthBipsOfValue(
brokerSle->at(sfDebtTotal), broker.params.coverRateMin),
broker.params.coverRateLiquidation),
state.totalValue - state.managementFeeOutstanding),
state.loanScale);
}
}();
auto const defaultAmount = computeDefaultCovered(
useProportionalDefaultCover(env.current()->rules(), brokerSle),
brokerSle->at(~sfCoverRateLiquidation).value_or(0),
brokerSle->at(sfCoverAvailable),
broker.asset,
totalDefaultAmount,
brokerSle->at(sfDebtTotal),
broker.params.coverRateMin,
state.loanScale);
return std::make_pair(defaultAmount, brokerSle->at(sfOwner));
}
return std::make_pair(Number{}, AccountID{});
@@ -6916,11 +6900,13 @@ protected:
});
auto const brokerKeylet = brokerInfo.brokerKeylet();
// Create two identical loans: each 50,000 XRP principal (scaled down to
// avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal
// + interest) Formula will calculate cover as: 100% × (20% × 100,000) =
// 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first
// default
// Create two identical loans: each 50,000 XRP principal.
// Total BrokerDebtTotal will be ~104,201 (principal + interest + fees).
// Old formula: seizure = min(100% × (20% × BrokerDebtTotal),
// DefaultAmount) ≈ 20,054 — nearly depleting 21,000 FLC on the
// first default.
// New formula: seizure = DefaultAmount × 20% ≈ 10,027 — splitting
// FLC equitably across both defaults.
auto const principalAmount = Number(50'000);
auto const loanPaymentInterval = 2592000; // 30 days
auto const loanGracePeriod = 604800; // 7 days
@@ -6979,13 +6965,16 @@ protected:
auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal);
auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable);
if (env.enabled(featureLendingProtocolV1_1))
if (useProportionalDefaultCover(env.current()->rules(), brokerSle))
{
// Proportional default cover
// Loan 1 Defaults: 20% of Loan A (50,134) = 10,027 seizure
// Proportional default cover (new formula):
// DefaultCovered = min(DefaultAmount × CoverRateMinimum,
// CoverAvailable)
// Loan A's DefaultAmount (~52,067) × 20% = 10,027 seizure
// Result: CoverAvailable = 21,000 - 10,027 = 10,973
// DebtTotal should have decreased by Loan A's debt (~52,067)
// DebtTotal should have decreased by Loan A's debt (~52,067),
// leaving only Loan B's debt (~50,134).
BEAST_EXPECT(afterFirstDebtTotal == 50'134);
// CoverAvailable should have decreased proportionally
@@ -6993,10 +6982,16 @@ protected:
}
else
{
// Loan 1 Defaults: 100% × (20% × 104,201) = 20,840 seizure
// Result: CoverAvailable = 21,000 - 20,840 = 160
// Global default cover (old formula):
// DefaultCovered = min(CoverRateLiquidation ×
// (CoverRateMinimum × BrokerDebtTotal), DefaultAmount)
// Pre-default BrokerDebtTotal (~104,201) × 20% = 20,840
// then 100% × 20,840 = 20,840
// but capped at DefaultAmount (~52,067), seizure = 20,054
// Result: CoverAvailable = 21,000 - 20,054 = 946
// DebtTotal should have decreased by Loan A's debt
// DebtTotal should have decreased by Loan A's debt (~52,067),
// leaving only Loan B's debt (~50,134).
BEAST_EXPECT(afterFirstDebtTotal == 50'134);
// CoverAvailable should have decreased significantly
@@ -7016,22 +7011,20 @@ protected:
// Both scenarios: DebtTotal should be 0 after both loans default
BEAST_EXPECT(afterSecondDebtTotal == 0);
if (env.enabled(featureLendingProtocolV1_1))
if (useProportionalDefaultCover(env.current()->rules(), brokerSle))
{
// Proportional default cover
// Loan 2 Defaults: 20% of Loan B (50,134) = 10,027 seizure
// Result: CoverAvailable = 10,973 - 10,027 = 946 (safety buffer remains)
// Both loans are covered equitably with a safety buffer remaining
// Proportional default cover (new formula):
// Loan B's DefaultAmount (~50,134) × 20% = 10,027 seizure
// Result: CoverAvailable = 10,973 - 10,027 = 946
//
// Both loans are covered equitably with a safety buffer remaining.
BEAST_EXPECT(afterSecondCoverAvailable == 946);
}
else
{
// Scenario A: Global Logic (Old Formula)
// Loan 2 Defaults: Only 160 remains to cover a 52,067 loss
// Global default cover (old formula):
// Only 946 remains to cover Loan B's DefaultAmount (~50,134)
// Result: CoverAvailable = 0 (fully depleted)
// CoverAvailable should be fully depleted
BEAST_EXPECT(afterSecondCoverAvailable == 0);
}
}
@@ -7166,6 +7159,210 @@ protected:
attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
}
void
testCoverRateLiquidationAmendmentGating()
{
testcase("CoverRateLiquidation amendment gating");
using namespace jtx;
using namespace loanBroker;
auto const coverRateLiqValue = percentageToTenthBips(25);
// When featureLendingProtocolV1_1 is NOT enabled,
// sfCoverRateLiquidation should be recorded on the broker.
{
Env env(*this, all - featureLendingProtocolV1_1);
Account const lender{"lender"};
env.fund(XRP(10'000'000), lender);
env.close();
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
BrokerParameters brokerParams{.coverRateLiquidation = coverRateLiqValue};
BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
if (BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(brokerSle->isFieldPresent(sfCoverRateLiquidation));
BEAST_EXPECT(brokerSle->at(sfCoverRateLiquidation) == coverRateLiqValue.value());
}
}
// When featureLendingProtocolV1_1 IS enabled,
// sfCoverRateLiquidation should NOT be recorded on the broker.
{
Env env(*this, all);
Account const lender{"lender"};
env.fund(XRP(10'000'000), lender);
env.close();
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
BrokerParameters brokerParams{.coverRateLiquidation = coverRateLiqValue};
BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
if (BEAST_EXPECT(brokerSle))
{
BEAST_EXPECT(!brokerSle->isFieldPresent(sfCoverRateLiquidation));
}
}
}
void
testCoverRateLiquidationBackwardsCompat()
{
testcase("CoverRateLiquidation backwards compatibility on default");
// Verify that the default cover formula honours whether
// sfCoverRateLiquidation is present on the broker SLE,
// regardless of the amendment state:
//
// * A broker created BEFORE the amendment has the field →
// old formula (global) is used even after the amendment
// is enabled.
//
// * A broker created AFTER the amendment lacks the field →
// new formula (proportional) is used.
using namespace jtx;
using namespace loan;
using namespace loanBroker;
// ---- helpers shared by both sub-tests ----
auto const coverRateMin = TenthBips32(20'000); // 20 %
auto const coverRateLiq = percentageToTenthBips(25); // 25 % (default)
auto const principalAmount = Number(50'000);
auto const loanPaymentInterval = 2'592'000; // 30 days
auto const loanGracePeriod = 604'800; // 7 days
// Lambda that creates one loan, advances past the grace period,
// defaults it, and returns the CoverAvailable after default.
auto defaultOneLoan = [&](Env& env,
BrokerInfo const& brokerInfo,
Account const& lender,
Account const& borrower) -> Number {
auto const brokerKeylet = brokerInfo.brokerKeylet();
auto loanTx = env.jt(
set(borrower, brokerKeylet.key, principalAmount),
sig(sfCounterpartySignature, lender),
interestRate(TenthBips32(500)), // 5 %
paymentTotal(12),
loan::paymentInterval(loanPaymentInterval),
loan::gracePeriod(loanGracePeriod),
fee(XRP(10)));
env(loanTx);
env.close();
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
auto loanSle = env.le(loanKeylet);
if (!BEAST_EXPECT(loanSle))
return Number{-1};
auto const nextDue = loanSle->at(sfNextPaymentDueDate);
auto const grace = loanSle->at(sfGracePeriod);
env.close(std::chrono::seconds{nextDue + grace + 60});
env(manage(lender, loanKeylet.key, tfLoanDefault), ter(tesSUCCESS));
env.close();
auto brokerSle = env.le(brokerKeylet);
if (!BEAST_EXPECT(brokerSle))
return Number{-1};
return brokerSle->at(sfCoverAvailable);
};
// ---- Sub-test A: pre-amendment broker (old formula) ----
Number coverAfterOld;
{
// Start WITHOUT the amendment so the broker stores the field.
Env env(*this, all - featureLendingProtocolV1_1);
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000), lender, borrower);
env.close();
PrettyAsset const asset = xrpIssue();
auto const brokerInfo = createVaultAndBroker(
env,
asset,
lender,
{
.vaultDeposit = Number(200'000),
.debtMax = 0,
.coverRateMin = coverRateMin,
.coverDeposit = 21'000,
.managementFeeRate = TenthBips16(100),
.coverRateLiquidation = coverRateLiq,
});
// Confirm the field was stored.
{
auto sle = env.le(brokerInfo.brokerKeylet());
BEAST_EXPECT(sle && sle->isFieldPresent(sfCoverRateLiquidation));
}
// Now enable the amendment the broker keeps its field.
env.enableFeature(featureLendingProtocolV1_1);
env.close();
BEAST_EXPECT(env.enabled(featureLendingProtocolV1_1));
coverAfterOld = defaultOneLoan(env, brokerInfo, lender, borrower);
}
// ---- Sub-test B: post-amendment broker (new formula) ----
Number coverAfterNew;
{
Env env(*this, all); // amendment already enabled
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000), lender, borrower);
env.close();
PrettyAsset const asset = xrpIssue();
// Pass coverRateLiquidation in BrokerParameters, but the
// amendment-gated code will NOT store it on the SLE.
auto const brokerInfo = createVaultAndBroker(
env,
asset,
lender,
{
.vaultDeposit = Number(200'000),
.debtMax = 0,
.coverRateMin = coverRateMin,
.coverDeposit = 21'000,
.managementFeeRate = TenthBips16(100),
.coverRateLiquidation = coverRateLiq,
});
// Confirm the field was NOT stored.
{
auto sle = env.le(brokerInfo.brokerKeylet());
BEAST_EXPECT(sle && !sle->isFieldPresent(sfCoverRateLiquidation));
}
coverAfterNew = defaultOneLoan(env, brokerInfo, lender, borrower);
}
// Old (global) formula with 25% CoverRateLiquidation:
// min(25% × (20% × 50,134), 50,134) = min(2,507, 50,134)
// = 2,507 seized → CoverAvailable = 21,000 - 2,507 = 18,493
BEAST_EXPECT(coverAfterOld == Number{18'493});
// New (proportional) formula:
// min(20% × 50,134, 21,000) = min(10,027, 21,000)
// = 10,027 seized → CoverAvailable = 21,000 - 10,027 = 10,973
BEAST_EXPECT(coverAfterNew == Number{10'973});
}
public:
void
run() override
@@ -7221,6 +7418,8 @@ public:
testLoanSetBrokerOwnerNoPermissionedDomainMPT();
testSequentialFLCDepletion(all - featureLendingProtocolV1_1);
testSequentialFLCDepletion(all);
testCoverRateLiquidationAmendmentGating();
testCoverRateLiquidationBackwardsCompat();
}
};