fix: Use consistent scale for debtTotal (#7093)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jingchen
2026-05-24 21:44:29 +01:00
committed by GitHub
parent e34c2667d7
commit a911f9089e
6 changed files with 482 additions and 25 deletions

View File

@@ -203,6 +203,21 @@ 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));
}
TER
checkLoanGuards(
Asset const& vaultAsset,

View File

@@ -12,6 +12,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
@@ -160,15 +161,25 @@ Expected<STAmount, TER>
determineClawAmount(
SLE const& sleBroker,
Asset const& vaultAsset,
std::optional<STAmount> const& amount)
std::optional<STAmount> const& amount,
SLE::const_ref vaultSle,
Rules const& rules)
{
auto const maxClawAmount = [&]() {
// Always round the minimum required up
NumberRoundModeGuard const mg1(Number::RoundingMode::Upward);
auto const minRequiredCover =
tenthBipsOfValue(sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
auto const minRequiredCover = [&]() {
if (rules.enabled(fixCleanup3_2_0))
{
return minimumBrokerCover(
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]), vaultSle);
}
// Always round the minimum required up
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return tenthBipsOfValue(
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
}();
// The subtraction probably won't round, but round down if it does.
NumberRoundModeGuard const mg2(Number::RoundingMode::Downward);
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
return sleBroker[sfCoverAvailable] - minRequiredCover;
}();
if (maxClawAmount <= beast::kZero)
@@ -283,7 +294,8 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
}
}
auto const findClawAmount = determineClawAmount(*sleBroker, vaultAsset, amount);
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount, vault, ctx.view.rules());
if (!findClawAmount)
{
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
@@ -345,7 +357,8 @@ LoanBrokerCoverClawback::doApply()
auto const vaultAsset = vault->at(sfAsset);
auto const findClawAmount = determineClawAmount(*sleBroker, vaultAsset, amount);
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount, vault, view().rules());
if (!findClawAmount)
return tecINTERNAL; // LCOV_EXCL_LINE
STAmount const& clawAmount = *findClawAmount;

View File

@@ -142,6 +142,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
// Cover Rate is in 1/10 bips units
auto const currentDebtTotal = sleBroker->at(sfDebtTotal);
auto const minimumCover = [&]() {
if (ctx.view.rules().enabled(fixCleanup3_2_0))
{
return minimumBrokerCover(
currentDebtTotal, TenthBips32{sleBroker->at(sfCoverRateMinimum)}, vault);
}
// Always round the minimum required up.
// Applies to `tenthBipsOfValue` as well as `roundToAsset`.
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);

View File

@@ -311,6 +311,8 @@ LoanPay::doApply()
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
auto const vaultScale = getAssetsTotalScale(vaultSle);
// Send the broker fee to the owner if they have sufficient cover available,
// _and_ if the owner can receive funds
// _and_ if the broker is authorized to hold funds. If not, so as not to
@@ -320,14 +322,22 @@ LoanPay::doApply()
// Normally freeze status is checked in preclaim, but we do it here to
// avoid duplicating the check. It'll claim a fee either way.
bool const sendBrokerFeeToOwner = [&]() {
// Round the minimum required cover up to be conservative. This ensures
// CoverAvailable never drops below the theoretical minimum, protecting
// the broker's solvency.
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return coverAvailableProxy >=
roundToAsset(
asset, tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), loanScale) &&
!isDeepFrozen(view, brokerOwner, asset) &&
// In the fixCleanup3_2_0 path, vault-related values (for example,
// DebtTotal) use vaultScale. The legacy path below intentionally retains
// its pre-amendment loanScale behavior.
auto const minCover = [&]() {
if (view.rules().enabled(fixCleanup3_2_0))
{
return minimumBrokerCover(debtTotalProxy.value(), coverRateMinimum, vaultSle);
}
// Round the minimum required cover up to be conservative. This ensures
// CoverAvailable never drops below the theoretical minimum, protecting
// the broker's solvency.
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return roundToAsset(
asset, tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), loanScale);
}();
return coverAvailableProxy >= minCover && !isDeepFrozen(view, brokerOwner, asset) &&
!requireAuth(view, asset, brokerOwner, AuthType::StrongAuth);
}();
@@ -423,10 +433,6 @@ LoanPay::doApply()
auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable);
auto assetsTotalProxy = vaultSle->at(sfAssetsTotal);
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the payment by rounding some of the values to that scale.
auto const vaultScale = getAssetsTotalScale(vaultSle);
auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid;
auto const totalPaidToVaultRounded =
roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward);

View File

@@ -493,11 +493,19 @@ LoanSet::doApply()
}
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
{
// Round the minimum required cover up to be conservative. This ensures
// CoverAvailable never drops below the theoretical minimum, protecting
// the broker's solvency.
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
if (brokerSle->at(sfCoverAvailable) < tenthBipsOfValue(newDebtTotal, coverRateMinimum))
auto const minCover = [&]() {
if (ctx_.view().rules().enabled(fixCleanup3_2_0))
{
return minimumBrokerCover(newDebtTotal, coverRateMinimum, vaultSle);
}
// Round the minimum required cover up to be conservative. This ensures
// CoverAvailable never drops below the theoretical minimum, protecting
// the broker's solvency.
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return tenthBipsOfValue(newDebtTotal, coverRateMinimum);
}();
if (brokerSle->at(sfCoverAvailable) < minCover)
{
JLOG(j_.warn()) << "Insufficient first-loss capital to cover the loan.";
return tecINSUFFICIENT_FUNDS;

View File

@@ -7858,6 +7858,414 @@ protected:
to_string(tolerance));
}
// Verify that LoanPay, LoanBrokerCoverWithdraw, and LoanSet all use the
// same vault-scale minimum cover when fixCleanup3_2_0 is enabled.
// Before the amendment, each transactor computed its minimum cover at a
// different precision (loanScale, debtScale, or the raw unrounded
// tenthBipsOfValue), which could lead to inconsistent decisions for the
// same broker state. After the amendment all three use
// minimumBrokerCover at vaultScale.
void
testMinimumBrokerCoverConsistency(FeatureBitset features)
{
using namespace jtx;
using namespace loan;
using namespace loanBroker;
bool const withAmendment = features[fixCleanup3_2_0];
struct Ctx
{
jtx::Account issuer;
jtx::Account lender;
jtx::Account borrower;
jtx::PrettyAsset iou;
BrokerInfo broker;
BrokerParameters brokerParams;
};
// Shared setup, parametrized by vaultDeposit (the only varying setup
// field across the three scenarios). Each call runs in its own Env
// so multiple invocations within one scenario cannot interfere.
// The caller is responsible for invoking testcase(...) before the
// first runTest call of each scenario.
auto runTest = [&](Number vaultDeposit, auto&& body) {
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000'000), issuer, lender, borrower);
env.close();
// Enable clawback on the issuer *before* any trust lines exist
// (asfAllowTrustLineClawback requires an empty owner directory).
env(fset(issuer, asfAllowTrustLineClawback));
env.close();
PrettyAsset const iou = issuer[iouCurrency_];
env(trust(lender, iou(1'000'000'000)));
env(trust(borrower, iou(1'000'000'000)));
env.close();
env(pay(issuer, lender, iou(100'000'000)));
env(pay(issuer, borrower, iou(100'000'000)));
env.close();
// 13.37% — non-round rate produces a messier minimum.
BrokerParameters const brokerParams{
.vaultDeposit = vaultDeposit,
.debtMax = 0,
.coverRateMin = TenthBips32{13'370},
.coverDeposit = 5'000,
.managementFeeRate = TenthBips16{500}};
BrokerInfo const broker = createVaultAndBroker(env, iou, lender, brokerParams);
body(
env,
Ctx{.issuer = issuer,
.lender = lender,
.borrower = borrower,
.iou = iou,
.broker = broker,
.brokerParams = brokerParams});
};
// Scenario 1 — LoanPay
//
// Verify that LoanPay's minimum cover check uses vault scale (not
// loan scale). Before the amendment, different loans could produce
// different fee routing decisions for the same broker-level state.
// Small vault deposit => vaultScale = -12.
testcase("LoanPay minimum cover scale consistency");
{
struct LoanKeylets
{
Keylet tiny;
Keylet big;
};
// Create the tiny + big loans and reduce cover via clawback so
// that subsequent LoanPay calls hit the minimum-cover boundary.
// Used by the two pay-and-check sub-tests below so each can run
// in its own Env.
auto setupLoansAndClawback = [&](Env& env, Ctx const& c) -> std::optional<LoanKeylets> {
Asset const asset{c.iou};
// Create the TINY loan first (while vaultScale is still
// small). principal 0.01, 0% interest, 1 payment =>
// loanScale = vaultScale.
auto const brokerSle1 = env.le(keylet::loanbroker(c.broker.brokerID));
if (!BEAST_EXPECT(brokerSle1))
return std::nullopt;
auto const tinyLoanSeq = brokerSle1->at(sfLoanSequence);
auto const tinyLoanKeylet = keylet::loan(c.broker.brokerID, tinyLoanSeq);
env(set(c.borrower, c.broker.brokerID, Number{1, -2}),
Sig(sfCounterpartySignature, c.lender),
kInterestRate(TenthBips32{0}),
kPaymentTotal(1),
kPaymentInterval(86400 * 365),
Fee(XRP(10)));
env.close();
// Create the BIG loan second. 100% annual interest over 20
// payments pushes totalValueOutstanding high enough that
// loanScale > vaultScale.
auto const brokerSle2 = env.le(keylet::loanbroker(c.broker.brokerID));
if (!BEAST_EXPECT(brokerSle2))
return std::nullopt;
auto const bigLoanSeq = brokerSle2->at(sfLoanSequence);
auto const bigLoanKeylet = keylet::loan(c.broker.brokerID, bigLoanSeq);
env(set(c.borrower, c.broker.brokerID, Number{500}),
Sig(sfCounterpartySignature, c.lender),
kInterestRate(TenthBips32{100'000}),
kPaymentTotal(20),
kPaymentInterval(86400 * 365),
Fee(XRP(10)));
env.close();
// The tiny loan's scale is frozen at the vault's pre-big-loan
// scale, so it is strictly smaller than the big loan's.
// After the big loan is created the vault absorbs its value,
// pushing vaultScale up to match bigLoanScale.
auto const tinyLoanSle = env.le(tinyLoanKeylet);
auto const bigLoanSle = env.le(bigLoanKeylet);
auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
if (!BEAST_EXPECT(tinyLoanSle) || !BEAST_EXPECT(bigLoanSle) ||
!BEAST_EXPECT(vaultSle))
return std::nullopt;
if (!BEAST_EXPECT(tinyLoanSle->at(sfLoanScale) == -12) ||
!BEAST_EXPECT(bigLoanSle->at(sfLoanScale) == -11) ||
!BEAST_EXPECT(getAssetsTotalScale(vaultSle) == -11))
return std::nullopt;
// Use issuer clawback to reduce cover to the minimum the
// clawback transactor allows. Compute the amount as
// initialCover - expectedCoverAfter so we exercise the exact
// clawback rather than relying on the transactor to clip
// down.
//
// Before the amendment the clawback minimum is the
// *unrounded* tenthBipsOfValue — strictly less than the
// rounded-at-vaultScale minimum LoanPay uses for the big
// loan. After the amendment both clawback and LoanPay use
// the same rounded minimum (via minimumBrokerCover), so
// cover lands exactly at that threshold.
Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15}
: Number{1330651855688458000, -15};
Number const clawbackAmount =
Number{c.brokerParams.coverDeposit} - expectedCoverAfter;
env(coverClawback(c.issuer),
kLoanBrokerId(c.broker.brokerID),
kAmount(STAmount{asset, clawbackAmount}));
env.close();
auto const brokerSle = env.le(keylet::loanbroker(c.broker.brokerID));
if (!BEAST_EXPECT(brokerSle) ||
!BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCoverAfter))
return std::nullopt;
return LoanKeylets{.tiny = tinyLoanKeylet, .big = bigLoanKeylet};
};
// Pay one loan and report whether the fee went to the broker's
// pseudo account (the fallback when cover < minimum) rather
// than to the owner.
auto feeGoesToPseudo = [&](Env& env, Ctx const& c, Keylet const& loanKeylet) -> bool {
Asset const asset{c.iou};
auto const brokerSle = env.le(keylet::loanbroker(c.broker.brokerID));
if (!BEAST_EXPECT(brokerSle))
return false;
auto const pseudoAcct = Account("pseudo", brokerSle->at(sfAccount));
auto const pseudoBefore = env.balance(pseudoAcct, c.iou);
auto const payLoan = env.le(loanKeylet);
if (!BEAST_EXPECT(payLoan))
return false;
auto const periodicPayment = payLoan->at(sfPeriodicPayment);
auto const serviceFee = payLoan->at(sfLoanServiceFee);
std::int32_t const loanScale = payLoan->at(sfLoanScale);
auto const payment = roundPeriodicPayment(asset, periodicPayment, loanScale);
auto const payAmt = STAmount{asset, payment + serviceFee};
env(loan::pay(c.borrower, loanKeylet.key, payAmt), Fee(XRP(10)));
env.close();
auto const pseudoAfter = env.balance(pseudoAcct, c.iou);
return pseudoAfter.number() > pseudoBefore.number();
};
// Pay the BIG loan in its own Env so its outcome cannot affect
// the TINY-loan check. With the fix, LoanPay and clawback use
// the same vaultScale minimum (cover == minAtVaultScale =>
// fee to owner). Without the fix, LoanPay uses bigLoanScale=-11,
// rounds up to a larger minimum than what clawback used =>
// cover < min => fee to pseudo.
runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) {
auto const loans = setupLoansAndClawback(env, c);
if (!loans)
return;
BEAST_EXPECT(feeGoesToPseudo(env, c, loans->big) == !withAmendment);
});
// Pay the TINY loan in its own Env. Fee goes to the owner
// either way:
// - With the fix: LoanPay uses vaultScale=-11 (same as
// clawback) => owner.
// - Without the fix: LoanPay uses tinyLoanScale=-12, rounds
// up at -12 (a no-op) => min == cover => owner.
runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) {
auto const loans = setupLoansAndClawback(env, c);
if (!loans)
return;
BEAST_EXPECT(!feeGoesToPseudo(env, c, loans->tiny));
});
}
// Scenario 2 — LoanBrokerCoverWithdraw
//
// Verify that CoverWithdraw's minimum cover check uses vault scale
// (not scale(debtTotal, asset)). Before the amendment, CoverWithdraw
// used:
// roundToAsset(asset, tenthBipsOfValue(debt, rate), scale(debt, asset))
// which could disagree with LoanPay's minimum (which used loanScale).
//
// Use a large vault deposit so that vaultScale (from AssetsTotal) is
// strictly larger than debtScale (from DebtTotal). With
// vaultDeposit = 100,000: after the big loan
// AssetsTotal ≈ 109,500 → vaultScale = -10
// DebtTotal ≈ 10,000 → debtScale = -11
// The one-order-of-magnitude gap makes roundToAsset at -10 truncate
// more aggressively than at -11, exposing the bug.
testcase("CoverWithdraw minimum cover scale consistency");
runTest(
/*vaultDeposit=*/100'000, [&](Env& env, Ctx const& c) {
Asset const asset{c.iou};
// Create only the big loan to push DebtTotal up to ~10,000
// while AssetsTotal stays around 109,500 (dominated by the
// large vault deposit).
env(set(c.borrower, c.broker.brokerID, Number{500}),
Sig(sfCounterpartySignature, c.lender),
kInterestRate(TenthBips32{100'000}),
kPaymentTotal(20),
kPaymentInterval(86400 * 365),
Fee(XRP(10)));
env.close();
// Read broker state and compute both old and new minimums.
auto const brokerSle = env.le(keylet::loanbroker(c.broker.brokerID));
auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
if (!BEAST_EXPECT(brokerSle) || !BEAST_EXPECT(vaultSle))
return;
auto const coverAvail = brokerSle->at(sfCoverAvailable);
auto const debtTotal = brokerSle->at(sfDebtTotal);
auto const vaultScale = getAssetsTotalScale(vaultSle);
auto const debtScale = scale(debtTotal, asset);
// Sanity: debt scale differs from vault scale for this setup.
BEAST_EXPECT(debtScale < vaultScale);
auto const oldMin = [&]() {
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return roundToAsset(
asset,
tenthBipsOfValue(debtTotal, TenthBips32{c.brokerParams.coverRateMin}),
debtScale);
}();
auto const newMin = minimumBrokerCover(
debtTotal, TenthBips32{c.brokerParams.coverRateMin}, vaultSle);
// The new (vaultScale) minimum must be strictly larger than
// the old (debtScale) minimum — that is the gap the amendment
// closes.
Number const expectedNewMin{1330650518688500000, -15};
Number const expectedOldMin{1330650518688472000, -15};
BEAST_EXPECT(newMin == expectedNewMin);
BEAST_EXPECT(oldMin == expectedOldMin);
// Try to withdraw so that remaining cover lands between the
// two minimums: oldMin < target < newMin.
auto const target = oldMin + (newMin - oldMin) / 2;
auto const withdrawAmount = STAmount{asset, coverAvail - target};
if (withAmendment)
{
// CoverWithdraw now uses vaultScale: target < newMin
// => FAILS.
env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount),
Ter(tecINSUFFICIENT_FUNDS));
}
else
{
// Old CoverWithdraw uses debtScale: target > oldMin
// => SUCCEEDS.
env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount));
}
env.close();
});
// Scenario 3 — LoanSet
//
// Verify that LoanSet's minimum cover check uses vault scale (not the
// raw unrounded tenthBipsOfValue). Before the amendment, LoanSet
// used tenthBipsOfValue(newDebtTotal, coverRateMinimum) (no
// roundToAsset), while clawback/withdraw used different formulas.
// After the amendment all use minimumBrokerCover at vaultScale, and
// rounding at a coarser scale can absorb a tiny debt increase —
// allowing a loan that would otherwise be rejected.
testcase("LoanSet minimum cover scale consistency");
runTest(
/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) {
// Create the tiny loan (scale -12) AND the big loan (scale
// -11). Both loans are needed so that DebtTotal has a full
// 16-digit mantissa — a "messy" value where roundToAsset at
// vaultScale actually truncates digits and produces a
// different result from the raw tenthBipsOfValue. With only
// the big loan, DebtTotal has ~4 significant digits and
// rounding at scale -11 is a no-op, masking the amendment's
// effect.
env(set(c.borrower, c.broker.brokerID, Number{1, -2}),
Sig(sfCounterpartySignature, c.lender),
kInterestRate(TenthBips32{0}),
kPaymentTotal(1),
kPaymentInterval(86400 * 365),
Fee(XRP(10)));
env.close();
env(set(c.borrower, c.broker.brokerID, Number{500}),
Sig(sfCounterpartySignature, c.lender),
kInterestRate(TenthBips32{100'000}),
kPaymentTotal(20),
kPaymentInterval(86400 * 365),
Fee(XRP(10)));
env.close();
// Clawback to reduce cover to the clawback transactor's
// minimum. Pass the exact amount rather than relying on the
// transactor to clip down; the setup matches Scenario 1 so
// the same residual-cover values apply.
Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15}
: Number{1330651855688458000, -15};
Number const clawbackAmount =
Number{c.brokerParams.coverDeposit} - expectedCoverAfter;
env(coverClawback(c.issuer),
kLoanBrokerId(c.broker.brokerID),
kAmount(c.iou(clawbackAmount)));
env.close();
// Verify scales.
auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
if (!BEAST_EXPECT(vaultSle))
return;
auto const vaultScale = getAssetsTotalScale(vaultSle);
BEAST_EXPECT(vaultScale == -11);
// Now try to create a tiny additional loan. Principal is
// 1e-11 (the smallest value that survives the precision
// check at loanScale = vaultScale = -11), with 0% interest
// and 1 payment.
//
// The tiny debt increase adds ~1.337e-12 to the unrounded
// minimum.
// - Without the amendment: the old LoanSet formula rounds
// up during tenthBipsOfValue (16-digit Number
// normalisation), pushing the minimum past the cover left
// by clawback => tecINSUFFICIENT_FUNDS.
// - With the amendment: minimumBrokerCover rounds at
// vaultScale=-11, which absorbs the tiny increase — the
// rounded minimum stays the same => tesSUCCESS.
auto const tinyPrincipal = Number{1, -11};
if (withAmendment)
{
env(set(c.borrower, c.broker.brokerID, tinyPrincipal),
Sig(sfCounterpartySignature, c.lender),
kInterestRate(TenthBips32{0}),
kPaymentTotal(1),
kPaymentInterval(86400 * 365),
Fee(XRP(10)));
}
else
{
env(set(c.borrower, c.broker.brokerID, tinyPrincipal),
Sig(sfCounterpartySignature, c.lender),
kInterestRate(TenthBips32{0}),
kPaymentTotal(1),
kPaymentInterval(86400 * 365),
Fee(XRP(10)),
Ter(tecINSUFFICIENT_FUNDS));
}
env.close();
});
}
void
runAmendmentIndependent()
{
@@ -7919,6 +8327,7 @@ protected:
testOverpaymentManagementFee(features);
testIssuerIsBorrower(features);
testIntegerScalePrincipalSticks(features);
testMinimumBrokerCoverConsistency(features);
// RIPD regressions
testRIPD3831(features);