Add a LoanBroker invariant to compare CoverAvailable to balance

- Ensures that LoanBroker.CoverAvailable is never less than the
  pseudo-account balance for the Vault asset.
This commit is contained in:
Ed Hennis
2025-11-24 16:24:35 -05:00
parent 1ba010031d
commit a7af773dd8
4 changed files with 111 additions and 9 deletions

View File

@@ -564,7 +564,7 @@ STAmount::clear()
{
// The -100 is used to allow 0 to sort less than a small positive values
// which have a negative exponent.
mOffset = native() ? 0 : -100;
mOffset = integral() ? 0 : -100;
mValue = 0;
mIsNegative = false;
}

View File

@@ -830,7 +830,7 @@ STAmount::isDefault() const
void
STAmount::canonicalize()
{
if (native() || mAsset.holds<MPTIssue>())
if (integral())
{
// native and MPT currency amounts should always have an offset of zero
// log(2^64,10) ~ 19.2
@@ -859,8 +859,10 @@ STAmount::canonicalize()
};
if (native())
set(XRPAmount{num});
else
else if (mAsset.holds<MPTIssue>())
set(MPTAmount{num});
else
Throw<std::runtime_error>("Unknown integral asset type");
mOffset = 0;
}
else

View File

@@ -2342,9 +2342,30 @@ ValidLoanBroker::visitEntry(
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (after && after->getType() == ltLOAN_BROKER)
if (after)
{
brokers_.emplace_back(before, after);
if (after->getType() == ltLOAN_BROKER)
{
auto& broker = brokers_[after->key()];
broker.brokerBefore = before;
broker.brokerAfter = after;
}
else if (
after->getType() == ltACCOUNT_ROOT &&
after->isFieldPresent(sfLoanBrokerID))
{
auto const& loanBrokerID = after->at(sfLoanBrokerID);
// create an entry if one doesn't already exist
auto& broker = brokers_[loanBrokerID];
}
else if (after->getType() == ltRIPPLE_STATE)
{
lines_.emplace_back(after);
}
else if (after->getType() == ltMPTOKEN)
{
mpts_.emplace_back(after);
}
}
}
@@ -2403,8 +2424,51 @@ ValidLoanBroker::finalize(
// Loan Brokers will not exist on ledger if the Lending Protocol amendment
// is not enabled, so there's no need to check it.
for (auto const& [before, after] : brokers_)
for (auto const& line : lines_)
{
for (auto const& field : {&sfLowLimit, &sfHighLimit})
{
auto const account =
view.read(keylet::account(line->at(*field).getIssuer()));
// This Invariant doesn't know about the rules for Trust Lines, so
// if the account is missing, don't treat it as an error. This
// loop is only concerned with finding Broker pseudo-accounts
if (account && account->isFieldPresent(sfLoanBrokerID))
{
auto const& loanBrokerID = account->at(sfLoanBrokerID);
// create an entry if one doesn't already exist
auto& broker = brokers_[loanBrokerID];
}
}
}
for (auto const& mpt : mpts_)
{
auto const account = view.read(keylet::account(mpt->at(sfAccount)));
// This Invariant doesn't know about the rules for MPTokens, so
// if the account is missing, don't treat is as an error. This
// loop is only concerned with finding Broker pseudo-accounts
if (account && account->isFieldPresent(sfLoanBrokerID))
{
auto const& loanBrokerID = account->at(sfLoanBrokerID);
// create an entry if one doesn't already exist
auto& broker = brokers_[loanBrokerID];
}
}
for (auto const& [brokerID, broker] : brokers_)
{
auto const& after = broker.brokerAfter
? broker.brokerAfter
: view.read(keylet::loanbroker(brokerID));
if (!after)
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker missing";
return false;
}
auto const& before = broker.brokerBefore;
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants
// If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
// one node (the root), which will only hold entries for `RippleState`
@@ -2438,6 +2502,26 @@ ValidLoanBroker::finalize(
<< "Invariant failed: Loan Broker cover available is negative";
return false;
}
auto const vault = view.read(keylet::vault(after->at(sfVaultID)));
if (!vault)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker vault ID is invalid";
return false;
}
auto const& vaultAsset = vault->at(sfAsset);
if (after->at(sfCoverAvailable) < accountHolds(
view,
after->at(sfAccount),
vaultAsset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j))
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available "
"is less than pseudo-account asset balance";
return false;
}
}
return true;
}

View File

@@ -763,9 +763,25 @@ public:
*/
class ValidLoanBroker
{
// Pair is <before, after>. After is used for most of the checks, except
// Not all of these elements will necessarily be populated. Remaining items
// will be looked up as needed.
struct BrokerInfo
{
SLE::const_pointer brokerBefore = nullptr;
// After is used for most of the checks, except
// those that check changed values.
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> brokers_;
SLE::const_pointer brokerAfter = nullptr;
};
// Collect all the LoanBrokers found directly or indirectly through
// pseudo-accounts. Key is the brokerID / index. It will be used to find the
// LoanBroker object if brokerBefore and brokerAfter are nullptr
std::map<uint256, BrokerInfo> brokers_;
// Collect all the modified trust lines. Their high and low accounts will be
// loaded to look for LoanBroker pseudo-accounts.
std::vector<SLE::const_pointer> lines_;
// Collect all the modified MPTokens. Their accounts will be loaded to look
// for LoanBroker pseudo-accounts.
std::vector<SLE::const_pointer> mpts_;
bool
goodZeroDirectory(