Fix invariant tests

This commit is contained in:
JCW
2026-07-14 18:27:22 +01:00
parent 210812e826
commit 31f1d5ff50
2 changed files with 153 additions and 8 deletions

View File

@@ -479,12 +479,9 @@ ValidVault::finalize(
// pending loan (Borrower present, CounterpartySignature absent).
if (afterVault.assetsReserved != beforeVault.assetsReserved)
{
bool const pendingLoanSet = txnType == ttLOAN_SET &&
tx.isFieldPresent(sfBorrower) &&
tx.isFieldPresent(sfStartDate) &&
!tx.isFieldPresent(sfCounterpartySignature) &&
bool const pendingLoanSet = txnType == ttLOAN_SET && tx.isFieldPresent(sfBorrower) &&
tx.isFieldPresent(sfStartDate) && !tx.isFieldPresent(sfCounterpartySignature) &&
!tx.isFieldPresent(sfCounterparty);
!tx.isFieldPresent(sfCounterpartySignature);
if (txnType != ttLOAN_DELETE && txnType != ttLOAN_ACCEPT && !pendingLoanSet)
{
JLOG(j.fatal()) << "Invariant failed: vault AssetsReserved changed "
@@ -1438,6 +1435,107 @@ ValidVault::finalize(
return result;
}
case ttLOAN_ACCEPT: {
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(),
"xrpl::ValidVault::finalize : loan accept updated a vault");
auto const& beforeVault = beforeVault_[0];
// Accepting a pending loan disburses the reserved principal
// from the vault pseudo-account to the borrower (and the
// origination fee, if any, to the broker owner) and releases it
// from the reserved bucket. The assets available and assets
// outstanding were already settled when the pending loan was
// created, so they must not move now.
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: loan accept must change vault balance";
return false; // That's all we can do
}
// A loan accept modifies exactly the loan being accepted; its
// principal outstanding is the amount disbursed and released.
if (afterLoan_.size() != 1 || beforeLoan_.size() != 1 ||
afterLoan_[0].key != beforeLoan_[0].key)
{
JLOG(j.fatal()) << //
"Invariant failed: loan accept must modify exactly one "
"loan";
return false; // That's all we can do
}
auto const& loan = beforeLoan_[0];
// Get the posterior scale to round calculations to
auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
auto const principalDelta =
roundToAsset(vaultAsset, -loan.principalOutstanding, minScale);
// The vault (pseudo-account) balance must fall by exactly the
// disbursed principal.
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultDeltaAssets != principalDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: loan accept must decrease vault "
"balance by the principal outstanding";
result = false;
}
// The reserved principal (held back when the pending loan was
// created) must be released by exactly the disbursed principal.
auto const assetsReservedDelta = roundToAsset(
vaultAsset, afterVault.assetsReserved - beforeVault.assetsReserved, minScale);
if (assetsReservedDelta != principalDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: loan accept must decrease assets "
"reserved by the principal outstanding";
result = false;
}
// Accepting a loan neither adds to nor removes from the pool
// tracked by assets available.
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
if (assetAvailableDelta != kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: loan accept must not change assets "
"available";
result = false;
}
// Likewise the interest booked at loan creation stands: assets
// outstanding must not move on accept.
auto const assetsTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
if (assetsTotalDelta != kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: loan accept must not change assets "
"outstanding";
result = false;
}
// A loan accept neither mints nor burns vault shares.
if (beforeShares && updatedShares &&
beforeShares->sharesTotal != updatedShares->sharesTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: loan accept must not change shares "
"outstanding";
result = false;
}
return result;
}
// LCOV_EXCL_START
default:
UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type");

View File

@@ -4004,15 +4004,37 @@ class Invariants_test : public beast::unit_test::Suite
Account const a1{"A1"};
Account const a2{"A2"};
env.fund(XRP(1000), a1, a2);
BEAST_EXPECT(precloseXrp(a1, a2, env));
// Create a real vault so a LoanAccept - which must modify the
// vault it lends from - can apply a self-consistent vault delta
// alongside the loan mutation under test.
Vault const vault{env};
auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = xrpIssue()});
env(createTx);
env(vault.deposit({.depositor = a1, .id = vaultKeylet.key, .amount = XRP(10)}));
env.close();
// The disbursed principal shared by the loan and the vault
// delta for every LoanAccept case below.
Number const principal{100};
OpenView ov{*env.current()};
auto const vaultKeylet = keylet::vault(a1.id(), ov.seq());
auto const loanKeylet = keylet::loan(vaultKeylet.key, 1);
// Seed the vault with reserved principal (as if a pending loan
// had already set it aside) so LoanAccept can release it.
AccountID vaultPseudo;
{
auto const sleVaultBase = ov.read(vaultKeylet);
if (!BEAST_EXPECT(sleVaultBase))
return;
vaultPseudo = sleVaultBase->getAccountID(sfAccount);
auto sleVault = std::make_shared<SLE>(*sleVaultBase);
sleVault->at(sfAssetsReserved) = principal;
ov.rawReplace(sleVault);
}
{
auto sleLoan = std::make_shared<SLE>(loanKeylet);
sleLoan->at(sfPrincipalOutstanding) = Number(100);
sleLoan->at(sfPrincipalOutstanding) = principal;
sleLoan->at(sfTotalValueOutstanding) = Number(150);
sleLoan->at(sfManagementFeeOutstanding) = Number(0);
sleLoan->at(sfPeriodicPayment) = Number(1);
@@ -4038,6 +4060,31 @@ class Invariants_test : public beast::unit_test::Suite
mutate(sleLoan);
ac.view().update(sleLoan);
// A LoanAccept must modify the vault it lends from: release the
// reserved principal and disburse it from the vault
// pseudo-account. The disbursed XRP is credited to a2 so the
// XRP-conservation invariant is satisfied.
if (tx.getTxnType() == ttLOAN_ACCEPT)
{
auto sleVault = ac.view().peek(vaultKeylet);
auto slePseudo = ac.view().peek(keylet::account(vaultPseudo));
auto sleDest = ac.view().peek(keylet::account(a2.id()));
if (!BEAST_EXPECT(sleVault && slePseudo && sleDest))
return;
auto reservedProxy = sleVault->at(sfAssetsReserved);
reservedProxy -= principal;
ac.view().update(sleVault);
STAmount const disbursed{100};
slePseudo->setFieldAmount(
sfBalance, slePseudo->getFieldAmount(sfBalance) - disbursed);
ac.view().update(slePseudo);
sleDest->setFieldAmount(
sfBalance, sleDest->getFieldAmount(sfBalance) + disbursed);
ac.view().update(sleDest);
}
auto transactor = makeTransactor(ac);
if (!BEAST_EXPECT(transactor))
return;