fix: Round deposit cover to LoanBroker scale

This commit is contained in:
Vito
2026-05-15 15:56:19 +02:00
parent a5db99fb96
commit e2ba7a728b
3 changed files with 47 additions and 10 deletions

View File

@@ -39,10 +39,15 @@ canApplyToBrokerCover(
beast::Journal j,
std::string_view logPrefix)
{
XRPL_ASSERT(
sleBroker && sleBroker->getType() == ltLOAN_BROKER,
"xrpl::canApplyToBrokerCover : valid LoanBroker sle");
XRPL_ASSERT(
vaultAsset.getIssuer() == amount.getIssuer() && amount > beast::kZERO,
"xrpl::canApplyToBrokerCover : valid LoanBroker sle");
if (!view.rules().enabled(fixCleanup3_2_0))
return tesSUCCESS;
if (amount == beast::kZERO)
return tesSUCCESS;
int const coverScale = scale(sleBroker->at(sfCoverAvailable), vaultAsset);
if (amount.isZeroAtScale(coverScale))

View File

@@ -73,11 +73,6 @@ LoanBrokerCoverDeposit::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, "LoanBrokerCoverDeposit"))
return ret;
auto const pseudoAccountID = sleBroker->at(sfAccount);
// Cannot transfer a non-transferable Asset
if (auto const ret = canTransfer(ctx.view, vaultAsset, account, pseudoAccountID))
@@ -111,8 +106,6 @@ LoanBrokerCoverDeposit::doApply()
auto const& tx = ctx_.tx;
auto const brokerID = tx[sfLoanBrokerID];
auto const amount = tx[sfAmount];
auto broker = view().peek(keylet::loanbroker(brokerID));
if (!broker)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -122,9 +115,25 @@ LoanBrokerCoverDeposit::doApply()
return tecINTERNAL; // LCOV_EXCL_LINE
auto const vaultAsset = vault->at(sfAsset);
auto const brokerPseudoID = broker->at(sfAccount);
bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0);
auto const amount = [&]() -> STAmount {
if (!fix320Enabled)
return tx[sfAmount];
return roundToScale(
tx[sfAmount],
scale(broker->at(sfCoverAvailable), vaultAsset),
Number::RoundingMode::Downward);
}();
if (fix320Enabled && amount == beast::kZERO)
{
JLOG(ctx_.journal.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
<< "is zero at loan broker scale";
return tecPRECISION_LOSS;
}
// Transfer assets from depositor to pseudo-account.
if (auto ter = accountSend(view(), account_, brokerPseudoID, amount, j_, WaiveTransferFee::Yes))
return ter;

View File

@@ -1896,6 +1896,29 @@ class LoanBroker_test : public beast::unit_test::Suite
}
}
{
testcase("Cover precision guard: Deposit");
// Both cases succeed; post-fix the amount is rounded DOWN to
// cover scale first, so the delta differs from pre-fix
// Input: 1.8e-14 IOU (sub-scale at cover scale -14)
// Pre-fix: 10 + 1.8e-14 → round-to-nearest →
// 10.00000000000002 → delta 2e-14
// Post-fix: roundToScale(1.8e-14, -14, Downward) = 1e-14;
// 10 + 1e-14 = 10.00000000000001 → delta 1e-14
Env env{*this, features};
auto const [brokerKeylet, iou] = setup(env);
PrettyAmount const subUlpAmt = iou(Number{18, -15});
auto const coverBefore = env.le(brokerKeylet)->at(sfCoverAvailable);
env(coverDeposit(alice, brokerKeylet.key, subUlpAmt), Ter(tesSUCCESS));
env.close();
auto const brokerAfter = env.le(brokerKeylet);
if (!BEAST_EXPECT(brokerAfter))
return;
Number const delta = features[fixCleanup3_2_0] ? Number{1, -14} : Number{2, -14};
BEAST_EXPECT(brokerAfter->at(sfCoverAvailable) - coverBefore == delta);
}
{
testcase("Cover precision guard: Withdraw");
Env env{*this, features};