diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp index 3c5ddc3a0f..75cb893bf5 100644 --- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp @@ -191,9 +191,10 @@ LoanManage::defaultLoan( // sibling change in LoanPay for the same fix on the payment path. // // The Number-precision guard `T - A >= totalDefaultAmount` below is - // exactly sufficient: loanVaultExposure returns a difference of vault- - // associated STNumber fields (IOU-normalized via associateAsset), so - // its STAmount promotion round-trips losslessly with no writeOff slack. + // exactly sufficient: loanVaultExposure returns a difference of + // STNumber fields that are whole multiples of the same immutable + // `10^sfLoanScale` and never outgrow its 16 digits, so its STAmount + // promotion round-trips losslessly with no writeOff slack. bool const useUnifiedAssetArithmetic = view.rules().enabled(fixCleanup3_4_0); if (useUnifiedAssetArithmetic) diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index def96e66ce..85573ae084 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -445,10 +445,14 @@ LoanPay::doApply() // normalization is applied symmetrically and the two balances land on // the same STAmount value -- which is what makes the debug invariant // sfAssetsAvailable == pseudo-account balance hold. Pre-amendment, - // sfAssetsAvailable was fed the vaultScale-rounded value while the - // pseudo-account received the raw Number, creating the same - // cross-side asymmetry with `assetsTotalDelta` that - // `LoanManage::defaultLoan` addresses on the default path. + // sfAssetsAvailable and the pseudo-account both received the + // vaultScale-rounded value (accountSendMulti was called with + // `totalPaidToVault`, i.e. the rounded amount), but sfAssetsTotal + // was mutated with the unrounded `assetsTotalDelta` on the very + // next line. That vault-side asymmetry between sfAssetsAvailable + // (rounded) and sfAssetsTotal (unrounded) is the counterpart of the + // cross-side mismatch `LoanManage::defaultLoan` addresses on the + // default path. auto const totalPaidToVault = view.rules().enabled(fixCleanup3_4_0) ? totalPaidToVaultRaw : roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward); diff --git a/src/test/app/lending/LoanRounding_test.cpp b/src/test/app/lending/LoanRounding_test.cpp index 26b5bb7c19..ffca1b8905 100644 --- a/src/test/app/lending/LoanRounding_test.cpp +++ b/src/test/app/lending/LoanRounding_test.cpp @@ -257,9 +257,17 @@ private: env(pay(issuer, borrower, asset(5'000'000'000))); env.close(); - // Vault magnitude ~1e7 so vaultScale ~ -9; the loans below sit at - // loan scale ~ -13. That 4-order gap is where storage precision - // can differ from loan-tracked Number precision. + // Vault magnitude ~1e7 gives a coarse vaultScale; the loans + // below use principals sized so that + // `computeLoanProperties(..., minimumScale=vaultScale)` clamps + // `loanScale = std::max(vaultScale, amount.exponent())` down + // to `vaultScale`. That equal-scale regime is precisely the + // one where the pre-amendment paths still round vault-side + // mutations asymmetrically (rounded on sfAssetsAvailable and + // in the pseudo-account transfer, unrounded on sfAssetsTotal + // via assetsTotalDelta / defaultCovered), so the dust-snap + // and post-rounding invariant branches this test guards + // against get maximum exposure. BrokerParameters const brokerParams{ .vaultDeposit = Number{10'000'000}, .debtMax = Number{0},