Add Loan Broker and Loan ledger objects:

- Also add new SFields, Keylet functions, and an Invariant to verify no
  illegal field modification
This commit is contained in:
Ed Hennis
2025-03-12 19:30:01 -04:00
parent 8165f9d5b1
commit 60a888619b
7 changed files with 243 additions and 1 deletions

View File

@@ -339,6 +339,24 @@ vault(uint256 const& vaultKey)
return {ltVAULT, vaultKey};
}
Keylet
loanbroker(AccountID const& owner, std::uint32_t seq) noexcept;
inline Keylet
loanbroker(uint256 const& vaultKey)
{
return {ltLOAN_BROKER, vaultKey};
}
Keylet
loan(AccountID const& owner, uint256 loanBrokerID, std::uint32_t seq) noexcept;
inline Keylet
loan(uint256 const& vaultKey)
{
return {ltLOAN, vaultKey};
}
Keylet
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;

View File

@@ -36,6 +36,8 @@ class STLedgerEntry final : public STObject, public CountedObject<STLedgerEntry>
public:
using pointer = std::shared_ptr<STLedgerEntry>;
using ref = const std::shared_ptr<STLedgerEntry>&;
using const_pointer = std::shared_ptr<STLedgerEntry const>;
using const_ref = const std::shared_ptr<STLedgerEntry const>&;
/** Create an empty object with the given key and type. */
explicit STLedgerEntry(Keylet const& k);

View File

@@ -167,6 +167,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfFirstNFTokenSequence, soeOPTIONAL},
{sfAMMID, soeOPTIONAL}, // pseudo-account designator
{sfVaultID, soeOPTIONAL}, // pseudo-account designator
{sfLoanBrokerID, soeOPTIONAL}, // pseudo-account designator
}))
/** A ledger object which contains a list of object identifiers.
@@ -485,6 +486,60 @@ LEDGER_ENTRY(ltVAULT, 0x0083, Vault, vault, ({
// no PermissionedDomainID ever (use MPTIssuance.sfDomainID)
}))
/** A ledger object representing a loan broker
\sa keylet::loanbroker
*/
LEDGER_ENTRY(ltLOAN_BROKER, 0x0084, LoanBroker, loan_broker, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfVaultNode, soeREQUIRED},
{sfVaultID, soeREQUIRED},
{sfAccount, soeREQUIRED},
{sfOwner, soeREQUIRED},
{sfData, soeDEFAULT},
{sfManagementFeeRate, soeDEFAULT},
{sfOwnerCount, soeREQUIRED},
{sfDebtTotal, soeREQUIRED},
{sfDebtMaximum, soeREQUIRED},
{sfCoverAvailable, soeREQUIRED},
{sfCoverRateMinimum, soeREQUIRED},
{sfCoverRateLiquidation, soeREQUIRED},
}))
/** A ledger object representing a loan between a Borrower and a Loan Broker
\sa keylet::loan
*/
LEDGER_ENTRY(ltLOAN, 0x0085, Loan, loan, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfLoanBrokerNode, soeREQUIRED},
{sfLoanBrokerID, soeREQUIRED},
{sfBorrower, soeREQUIRED},
{sfLoanOriginationFee, soeREQUIRED},
{sfLoanServiceFee, soeREQUIRED},
{sfLatePaymentFee, soeREQUIRED},
{sfClosePaymentFee, soeREQUIRED},
{sfOverpaymentFee, soeREQUIRED},
{sfInterestRate, soeREQUIRED},
{sfLateInterestRate, soeREQUIRED},
{sfCloseInterestRate, soeREQUIRED},
{sfOverpaymentInterestRate, soeREQUIRED},
{sfStartDate, soeREQUIRED},
{sfPaymentInterval, soeREQUIRED},
{sfGracePeriod, soeREQUIRED},
{sfPreviousPaymentDate, soeREQUIRED},
{sfNextPaymentDueDate, soeREQUIRED},
{sfPaymentRemaining, soeREQUIRED},
{sfAssetAvailable, soeREQUIRED},
{sfPrincipalOutstanding, soeREQUIRED},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE

View File

@@ -59,6 +59,13 @@ TYPED_SFIELD(sfHookEmitCount, UINT16, 18)
TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
TYPED_SFIELD(sfLedgerFixType, UINT16, 21)
TYPED_SFIELD(sfManagementFeeRate, UINT16, 22)
TYPED_SFIELD(sfCoverRateMinimum, UINT16, 23)
TYPED_SFIELD(sfCoverRateLiquidation, UINT16, 24)
TYPED_SFIELD(sfInterestRate, UINT16, 25)
TYPED_SFIELD(sfLateInterestRate, UINT16, 26)
TYPED_SFIELD(sfCloseInterestRate, UINT16, 27)
TYPED_SFIELD(sfOverpaymentInterestRate, UINT16, 28)
// 32-bit integers (common)
TYPED_SFIELD(sfNetworkID, UINT32, 1)
@@ -113,6 +120,12 @@ TYPED_SFIELD(sfEmitGeneration, UINT32, 46)
TYPED_SFIELD(sfVoteWeight, UINT32, 48)
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
TYPED_SFIELD(sfStartDate, UINT32, 52)
TYPED_SFIELD(sfPaymentInterval, UINT32, 53)
TYPED_SFIELD(sfGracePeriod, UINT32, 54)
TYPED_SFIELD(sfPreviousPaymentDate, UINT32, 55)
TYPED_SFIELD(sfNextPaymentDueDate, UINT32, 56)
TYPED_SFIELD(sfPaymentRemaining, UINT32, 57)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -143,6 +156,8 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie
TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default)
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
TYPED_SFIELD(sfVaultNode, UINT64, 29)
TYPED_SFIELD(sfLoanBrokerNode, UINT64, 30)
// 128-bit
TYPED_SFIELD(sfEmailHash, UINT128, 1)
@@ -194,6 +209,7 @@ TYPED_SFIELD(sfHookNamespace, UINT256, 32)
TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
TYPED_SFIELD(sfDomainID, UINT256, 34)
TYPED_SFIELD(sfVaultID, UINT256, 35)
TYPED_SFIELD(sfLoanBrokerID, UINT256, 35)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
@@ -201,6 +217,15 @@ TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2)
TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3)
TYPED_SFIELD(sfAssetsTotal, NUMBER, 4)
TYPED_SFIELD(sfLossUnrealized, NUMBER, 5)
TYPED_SFIELD(sfDebtTotal, NUMBER, 6)
TYPED_SFIELD(sfDebtMaximum, NUMBER, 7)
TYPED_SFIELD(sfCoverAvailable, NUMBER, 8)
TYPED_SFIELD(sfLoanOriginationFee, NUMBER, 9)
TYPED_SFIELD(sfLoanServiceFee, NUMBER, 9)
TYPED_SFIELD(sfLatePaymentFee, NUMBER, 10)
TYPED_SFIELD(sfClosePaymentFee, NUMBER, 11)
TYPED_SFIELD(sfOverpaymentFee, NUMBER, 12)
TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13)
// currency amount (common)
TYPED_SFIELD(sfAmount, AMOUNT, 1)
@@ -295,6 +320,7 @@ TYPED_SFIELD(sfAttestationRewardAccount, ACCOUNT, 21)
TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22)
TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23)
TYPED_SFIELD(sfSubject, ACCOUNT, 24)
TYPED_SFIELD(sfBorrower, ACCOUNT, 25)
// vector of 256-bit
TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never)

View File

@@ -95,6 +95,8 @@ enum class LedgerNameSpace : std::uint16_t {
CREDENTIAL = 'D',
PERMISSIONED_DOMAIN = 'm',
VAULT = 'V',
LOAN_BROKER = 'l', // lower-case L
LOAN = 'L',
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -550,6 +552,18 @@ vault(AccountID const& owner, std::uint32_t seq) noexcept
return vault(indexHash(LedgerNameSpace::VAULT, owner, seq));
}
Keylet
loanbroker(AccountID const& owner, std::uint32_t seq) noexcept
{
return loanbroker(indexHash(LedgerNameSpace::LOAN_BROKER, owner, seq));
}
Keylet
loan(AccountID const& owner, uint256 loanBrokerID, std::uint32_t seq) noexcept
{
return loan(indexHash(LedgerNameSpace::LOAN, loanBrokerID, seq));
}
Keylet
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept
{

View File

@@ -1578,4 +1578,101 @@ ValidPermissionedDomain::finalize(
(sleStatus_[1] ? check(*sleStatus_[1], j) : true);
}
//------------------------------------------------------------------------------
void
NoModifiedUnmodifiableFields::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete || !before)
// Creation and deletion are ignored
return;
auto const type = after->getType();
if (knownTypes_.contains(type))
{
changedEntries_.emplace(before, after);
}
}
bool
NoModifiedUnmodifiableFields::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
static auto const fieldChanged = [](auto const& before,
auto const& after,
auto const& field) {
return before->isFieldPresent(field) != after->isFieldPresent(field) ||
before->at(field) != after->at(field);
};
for (auto const& slePair : changedEntries_)
{
auto const& before = slePair.first;
auto const& after = slePair.second;
auto const type = after->getType();
bool bad = false;
[[maybe_unused]] bool enforce = false;
switch (type)
{
case ltLOAN_BROKER:
/*
* We check this invariant regardless of lending protocol
* amendment status, allowing for detection and logging of
* potential issues even when the amendment is disabled.
*/
enforce = view.rules().enabled(featureLendingProtocol);
bad = fieldChanged(before, after, sfVaultID) ||
fieldChanged(before, after, sfAccount) ||
fieldChanged(before, after, sfOwner) ||
fieldChanged(before, after, sfManagementFeeRate) ||
fieldChanged(before, after, sfCoverRateMinimum) ||
fieldChanged(before, after, sfCoverRateLiquidation);
break;
case ltLOAN:
/*
* We check this invariant regardless of lending protocol
* amendment status, allowing for detection and logging of
* potential issues even when the amendment is disabled.
*/
enforce = view.rules().enabled(featureLendingProtocol);
bad = fieldChanged(before, after, sfLoanBrokerID) ||
fieldChanged(before, after, sfBorrower) ||
fieldChanged(before, after, sfLoanOriginationFee) ||
fieldChanged(before, after, sfLoanServiceFee) ||
fieldChanged(before, after, sfLatePaymentFee) ||
fieldChanged(before, after, sfClosePaymentFee) ||
fieldChanged(before, after, sfOverpaymentFee) ||
fieldChanged(before, after, sfInterestRate) ||
fieldChanged(before, after, sfLateInterestRate) ||
fieldChanged(before, after, sfCloseInterestRate) ||
fieldChanged(before, after, sfOverpaymentInterestRate) ||
fieldChanged(before, after, sfPaymentInterval);
break;
default:
UNREACHABLE(
"ripple::NoModifiedUnmodifiableFields::finalize : unknown "
"SLE type");
}
XRPL_ASSERT(
!bad || enforce,
"ripple::NoModifiedUnmodifiableFields::finalize : no bad "
"changes or enforce invariant");
if (bad)
{
JLOG(j.fatal())
<< "Invariant failed: changed an unchangable field for "
<< tx.getTransactionID();
if (enforce)
return false;
}
}
return true;
}
} // namespace ripple

View File

@@ -618,6 +618,35 @@ public:
beast::Journal const&);
};
/**
* @brief Invariants: Some fields are unmodifiable
*
* Check that any fields specified as unmodifiable are not modified when the
* object is modified. Creation and deletion are ignored.
*
*/
class NoModifiedUnmodifiableFields
{
std::set<LedgerEntryType> const knownTypes_{ltLOAN_BROKER, ltLOAN};
std::set<std::pair<SLE::const_pointer, SLE::const_pointer>> changedEntries_;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
// additional invariant checks can be declared above and then added to this
// tuple
using InvariantChecks = std::tuple<
@@ -637,7 +666,8 @@ using InvariantChecks = std::tuple<
NFTokenCountTracking,
ValidClawback,
ValidMPTIssuance,
ValidPermissionedDomain>;
ValidPermissionedDomain,
NoModifiedUnmodifiableFields>;
/**
* @brief get a tuple of all invariant checks