Lending protocol implementation (XLS-0066)

- Add the LendingProtocol amendment
- Add Loan Broker and Loan ledger objects:
- Also add new SFields, Keylet functions, and an Invariant to verify no
  illegal field modification
- Update list of "constant" fields from spec
- Also add a general check for all object types for the type and index
  fields
- refactor: Check transaction flags in preflight0
- Adds a flagMask parameter to preflight1 so that it's impossible to
  forget to check flags.
- Also adds a short hash prefix to all Transactor log messages.
- refactor: Generalize Transactor preflight:
- Derived classes no longer need to explicitly check amendments, nor
  call into preflight1 or preflight2.
- implemeng LoanBrokerSet
- Transactions: LoanDelete, LoanManage, LoanDraw, LoanPay
- LoanBrokerSet creation mostly done. Need update.
- Also added a lookup table for pseudo account fields.
- Update changed field name.
- Modify modifiable fields in an update. Note there are only two.
- Add a node field to dirLink, defaulting sfOwnerNode, so other
  relationships can be updated.
- Create some helper classes for transaction fields
- Test that they work by converting some of the existing classes
- Finish creating helper classes for JTx fields
- Also change the pseudo account field lookup to a function that uses
  a switch
- Update tests, update pseudo-account checking
- Generalize some of the Invariant checks using macro files
  - Valid ledger entry type
  - Valid new account root and pseudo account check
- Enumerate transaction privileges for invariants
  - Allows them to be defined in transactions.macro instead of needing to
    scrutinize every existing Invariant class.
  - List is not necessarily comprehensive, but does cover every check
    where more than one transaction type is involved.
- Reserve a few values between Vault and Lending for future use
- Pseudo-account improvements
  - Define pseudo-account fields with an sfield flag
  - Pseudo-account invariant checks rules whenever a pseudo-account is
    created or modified.
- Move some helper functions.
- Check the regular key in the pseudo-transaction invariant check.
- Transactor::checkSign will always fail for a pseudo-account, so even
  if someone figures out how to get a good signature, it won't work.
- Fix account creation to check both amendments
- Add a validity range for sfDebtMaximum
- Change more "failed" messages. The goal here is to be able to search
  the log for "failed" and ONLY get test failures.
- NoModifiedUnmodifiableFields and ValidPseudoAccounts
- Move the Invariants_test class into the test namespace
- Clang wants an explicit ctor to emplace in a vector
- Refactor: Add a Transactor base function to make it easier to get the
  owner reserve increment as a fee.
- Refactor: Add an overload jtx::fee(increment) to pay an owner reserve.
- Initial implementation of LoanBrokerDelete
- Generalize the LoanBroker lifecycle test
- Refactor ApplyView::dirAdd to give access to low-level operations
  - Takes a page from #5362, which may turn out to be useful!
- Start writing Loan Broker invariants and tests
  - Specifically those mentioned for LoanBrokerDelete
- Move all detail namespaces to be under ripple
  - Avoids problems with namespace collisions / ambiguous symbol issues
    with unity builds, especially when adding or removing files.
- Add LoanBrokerCoverDeposit transaction
- Add LoanBrokerCoverWithdraw transaction
- Start writing tests for LoanBrokerCover*
- Add support for `Asset` and `MPTIssue` to some `jtx` helper classes
  and functions (`balance`, `expectLine`)
- Add support for pseudo-accounts to `jtx::Account` by allowing directly
  setting the AccountID without a matching key.
- Add Asset and MPTIssue support to more jtx objects / functions
  - Unfortunately, to work around some ambiguous symbol compilation
    errors, I had to change the implicit conversion from IOU to Asset to
    a conversion from IOU to PrettyAsset, and add a more explicit
    `asset()` function. This workaround only required changing two
    existing tests, so seems acceptable.
- Ensure that an account is not deleted with an XRP balance
  - Updates the AccountRootsDeletedClean invariant
- Finish up the Loan Broker tests
- Move inclusion of Transactor headers to transactions.macro
  - Only need to update in one place when adding a new transaction.
- Start implementing LoanSet transactor
  - Add some more values and functions to make it easier to work with
    basis point values / bips.
  - Fix several earlier mistakes.
- Generalize the check*Sign functions to support CounterParty
  - checkSign, checkSingleSign, and checkMultiSign in STTx and Transactor
- Start writing Loan tests
  - Required adding support for counterparty signature to jtx framework:
    arbitrary signature field destination, multiple signer callbacks
- Get Counterparty signing working
- Add more LoanSet unit tests, added LoanBroker LoanSequence field
  - LoanSequence will prevent loan key collisions
- Change Loan object indexing, fix several broken LoanSet unit tests
  - Loan objects will now only be indexed by LoanBrokerID and
    LoanSequence, which is a new field in LoanBroker. Also changes
    Loan.Sequence to Loan.LoanSequence to match up.
  - Several tests weren't working because of `PrettyAsset` scaling. Also,
    `PrettyAsset` calculations could overflow. Made that less likely by
    changing the type of `scale_`.
  - LoanSet will fail if an account tries to loan to itself.
- Ensure that an account is not deleted with a non-zero owner count
  - Updates the AccountRootsDeletedClean invariant
- Add unit tests to create a Loan successfully
  - Fix a few field initializations in LoanSet
- Refactor issuance validity check in VaultCreate
  - Utility function: canAddHolding
  - Call canAddHolding from any transactor that call addEmptyHolding
    (LoanBrokerSet, LoanSet)
- Start implementing LoanManage transaction
  - Also add a ValidLoan invariant
- Finish `LoanManage` functionality and tests, modulo LoanDraw/Pay
- Allow existing trust lines to loan brokers to be managed (by issuer)
- Implement LoanDelete, and fix a bunch of math errors in LoanManage
- Update to match latest spec: compute interest, LoanBroker reserves
- refactor: Define getFlagsMask in the base Transactor class
  - Returns tfUniversalMask for most transactors
  - Only transactors that use other flags need to override
- Implement LoanDraw, and made good progress on related tests
- Start implementing LoanPay transaction
- Implement LoanPay & most tests
- Also add an XRPL_ASSERT_PARTS, which splits the parts of the assert message
    so I don't have to remember the proper formatting.
Start writing LoanPay transaction tests
This commit is contained in:
Ed Hennis
2025-05-16 16:10:04 +01:00
committed by Bronek Kozicki
parent fb5d94bbef
commit 527e0c916f
60 changed files with 7684 additions and 149 deletions

View File

@@ -343,6 +343,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(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept;
inline Keylet
loan(uint256 const& vaultKey)
{
return {ltLOAN, vaultKey};
}
Keylet
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;

View File

@@ -194,6 +194,11 @@ enum LedgerSpecificFlags {
// ltVAULT
lsfVaultPrivate = 0x00010000,
// ltLOAN
lsfLoanDefault = 0x00010000,
lsfLoanImpaired = 0x00020000,
lsfLoanOverpayment = 0x00040000, // True, loan allows overpayments
};
//------------------------------------------------------------------------------

View File

@@ -84,6 +84,90 @@ std::size_t constexpr maxDeletableTokenOfferEntries = 500;
*/
std::uint16_t constexpr maxTransferFee = 50000;
/** There are 10,000 basis points (bips) in 100%.
*
* Basis points represent 0.01%.
*
* Given a value X, to find the amount for B bps,
* use X * B / bipsPerUnity
*
* Example: If a loan broker has 999 XRP of debt, and must maintain 1,000 bps of
* that debt as cover (10%), then the minimum cover amount is 999,000,000 drops
* * 1000 / bipsPerUnity = 99,900,00 drops or 99.9 XRP.
*
* Given a percentage P, to find the number of bps that percentage represents,
* use P * bipsPerUnity.
*
* Example: 50% is 0.50 * bipsPerUnity = 5,000 bps.
*/
Bips32 constexpr bipsPerUnity(100 * 100);
TenthBips32 constexpr tenthBipsPerUnity(bipsPerUnity.value() * 10);
constexpr Bips32
percentageToBips(std::uint32_t percentage)
{
return Bips32(percentage * bipsPerUnity.value() / 100);
}
constexpr TenthBips32
percentageToTenthBips(std::uint32_t percentage)
{
return TenthBips32(percentage * tenthBipsPerUnity.value() / 100);
}
template <typename T, class TBips>
constexpr T
bipsOfValue(T value, Bips<TBips> bips)
{
return value * bips.value() / bipsPerUnity.value();
}
template <typename T, class TBips>
constexpr T
tenthBipsOfValue(T value, TenthBips<TBips> bips)
{
return value * bips.value() / tenthBipsPerUnity.value();
}
/** The maximum management fee rate allowed by a loan broker in 1/10 bips.
Valid values are between 0 and 10% inclusive.
*/
TenthBips16 constexpr maxManagementFeeRate(
unsafe_cast<std::uint16_t>(percentageToTenthBips(10).value()));
static_assert(maxManagementFeeRate == TenthBips16(std::uint16_t(10'000u)));
/** The maximum coverage rate required of a loan broker in 1/10 bips.
Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxCoverRate = percentageToTenthBips(100);
static_assert(maxCoverRate == TenthBips32(100'000u));
/** The maximum overpayment fee on a loan in 1/10 bips.
*
Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxOverpaymentFee = percentageToTenthBips(100);
/** The maximum premium added to the interest rate for late payments on a loan
* in 1/10 bips.
*
* Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxLateInterestRate = percentageToTenthBips(100);
/** The maximum close interest rate charged for repaying a loan early in 1/10
* bips.
*
* Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxCloseInterestRate = percentageToTenthBips(100);
/** The maximum overpayment interest rate charged on loan overpayments in 1/10
* bips.
*
* Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxOverpaymentInterestRate = percentageToTenthBips(100);
/** The maximum length of a URI inside an NFT */
std::size_t constexpr maxTokenURILength = 256;

View File

@@ -137,8 +137,8 @@ field_code(int id, int index)
SFields are created at compile time.
Each SField, once constructed, lives until program termination, and there
is only one instance per fieldType/fieldValue pair which serves the entire
application.
is only one instance per fieldType/fieldValue pair which serves the
entire application.
*/
class SField
{

View File

@@ -695,6 +695,36 @@ divRoundStrict(
std::uint64_t
getRate(STAmount const& offerOut, STAmount const& offerIn);
STAmount
roundToReference(
STAmount const value,
STAmount referenceValue,
Number::rounding_mode rounding = Number::getround());
/** Round an arbitrary precision Number to the precision of a given Asset.
*
* @param asset The relevant asset
* @param value The value to be rounded
* @param referenceValue Only relevant to IOU assets. A reference value to
* establish the precision limit of `value`. Should be larger than
* `value`.
* @param rounding Optional Number rounding mode
*/
template <AssetType A>
Number
roundToAsset(
A const& asset,
Number const& value,
Number const& referenceValue,
Number::rounding_mode rounding = Number::getround())
{
NumberRoundModeGuard mg(rounding);
STAmount const ret{asset, value};
if (ret.asset().native() || !ret.asset().holds<Issue>())
return ret;
return roundToReference(ret, STAmount{asset, referenceValue});
}
//------------------------------------------------------------------------------
inline bool
@@ -703,10 +733,10 @@ isXRP(STAmount const& amount)
return amount.native();
}
// Since `canonicalize` does not have access to a ledger, this is needed to put
// the low-level routine stAmountCanonicalize on an amendment switch. Only
// transactions need to use this switchover. Outside of a transaction it's safe
// to unconditionally use the new behavior.
// Since `canonicalize` does not have access to a ledger, this is needed to
// put the low-level routine stAmountCanonicalize on an amendment switch.
// Only transactions need to use this switchover. Outside of a transaction
// it's safe to unconditionally use the new behavior.
bool
getSTAmountCanonicalizeSwitchover();

View File

@@ -242,6 +242,9 @@ public:
getFieldPathSet(SField const& field) const;
STVector256 const&
getFieldV256(SField const& field) const;
// If not found, returns an object constructed with the given field
STObject
getFieldObject(SField const& field) const;
STArray const&
getFieldArray(SField const& field) const;
STCurrency const&
@@ -515,7 +518,26 @@ protected:
// Constraint += and -= ValueProxy operators
// to value types that support arithmetic operations
template <typename U>
concept IsArithmetic = std::is_arithmetic_v<U> || std::is_same_v<U, STAmount>;
concept IsArithmeticNumber = std::is_arithmetic_v<U> ||
std::is_same_v<U, Number> || std::is_same_v<U, STAmount>;
template <
typename U,
typename Value = typename U::value_type,
typename Unit = typename U::unit_type>
concept IsArithmeticValueUnit =
std::is_same_v<U, unit::ValueUnit<Unit, Value>> &&
IsArithmeticNumber<Value> && std::is_class_v<Unit>;
template <typename U, typename Value = typename U::value_type>
concept IsArithmeticST = !IsArithmeticValueUnit<U> && IsArithmeticNumber<Value>;
template <typename U>
concept IsArithmetic =
IsArithmeticNumber<U> || IsArithmeticST<U> || IsArithmeticValueUnit<U>;
template <class T, class U>
concept Addable = requires(T t, U u) { t = t + u; };
template <typename T, typename U>
concept IsArithmeticCompatible =
IsArithmetic<typename T::value_type> && Addable<typename T::value_type, U>;
template <class T>
class STObject::ValueProxy : public Proxy<T>
@@ -535,10 +557,12 @@ public:
// Convenience operators for value types supporting
// arithmetic operations
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
ValueProxy&
operator+=(U const& u);
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
ValueProxy&
operator-=(U const& u);
@@ -774,6 +798,7 @@ STObject::ValueProxy<T>::operator=(U&& u)
template <typename T>
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
STObject::ValueProxy<T>&
STObject::ValueProxy<T>::operator+=(U const& u)
{
@@ -783,6 +808,7 @@ STObject::ValueProxy<T>::operator+=(U const& u)
template <class T>
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
STObject::ValueProxy<T>&
STObject::ValueProxy<T>::operator-=(U const& u)
{

View File

@@ -88,7 +88,13 @@ public:
// Outer transaction functions / signature functions.
Blob
getSignature() const;
getSignature(STObject const& sigObject) const;
Blob
getSignature() const
{
return getSignature(*this);
}
uint256
getSigningHash() const;
@@ -121,10 +127,28 @@ public:
void
sign(PublicKey const& publicKey, SecretKey const& secretKey);
enum class RequireFullyCanonicalSig : bool { no, yes };
/** Check the signature.
@param requireCanonicalSig If `true`, check that the signature is fully
canonical. If `false`, only check that the signature is valid.
@param rules The current ledger rules.
@param pSig Pointer to object that contains the signature fields, if not
using "this". Will most often be null
@return `true` if valid signature. If invalid, the error message string.
*/
Expected<void, std::string>
checkSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules,
STObject const* pSig) const;
/** Check the signature.
@param requireCanonicalSig If `true`, check that the signature is fully
canonical. If `false`, only check that the signature is valid.
@param rules The current ledger rules.
@return `true` if valid signature. If invalid, the error message string.
*/
enum class RequireFullyCanonicalSig : bool { no, yes };
Expected<void, std::string>
checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules)
const;
@@ -146,12 +170,15 @@ public:
private:
Expected<void, std::string>
checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const;
checkSingleSign(
RequireFullyCanonicalSig requireCanonicalSig,
STObject const* pSig) const;
Expected<void, std::string>
checkMultiSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const;
Rules const& rules,
STObject const* pSig) const;
STBase*
copy(std::size_t n, void* buf) const override;

View File

@@ -242,6 +242,18 @@ constexpr std::uint32_t const tfVaultPrivate = 0x00010000;
static_assert(tfVaultPrivate == lsfVaultPrivate);
constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000;
constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable);
// LoanSet flags:
// True, indicates the loan supports overpayments
constexpr std::uint32_t const tfLoanOverpayment = 0x00010000;
constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal | tfLoanOverpayment);
// LoanManage flags:
constexpr std::uint32_t const tfLoanDefault = 0x00010000;
constexpr std::uint32_t const tfLoanImpair = 0x00020000;
constexpr std::uint32_t const tfLoanUnimpair = 0x00040000;
constexpr std::uint32_t const tfLoanManageMask = ~(tfUniversal | tfLoanDefault | tfLoanImpair | tfLoanUnimpair);
// clang-format on
} // namespace ripple

View File

@@ -27,11 +27,12 @@
#error "undefined macro: XRPL_RETIRE"
#endif
// clang-format off
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
@@ -146,3 +147,5 @@ XRPL_RETIRE(fix1201)
XRPL_RETIRE(fix1512)
XRPL_RETIRE(fix1523)
XRPL_RETIRE(fix1528)
// clang-format on

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.
@@ -497,6 +498,66 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
// no PermissionedDomainID ever (use MPTIssuance.sfDomainID)
}))
/** Reserve 0x0084-0x0087 for future Vault-related objects. */
/** A ledger object representing a loan broker
\sa keylet::loanbroker
*/
LEDGER_ENTRY(ltLOAN_BROKER, 0x0088, LoanBroker, loan_broker, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfVaultNode, soeREQUIRED},
{sfVaultID, soeREQUIRED},
{sfAccount, soeREQUIRED},
{sfOwner, soeREQUIRED},
{sfLoanSequence, soeREQUIRED},
{sfData, soeDEFAULT},
{sfManagementFeeRate, soeDEFAULT},
{sfOwnerCount, soeDEFAULT},
{sfDebtTotal, soeDEFAULT},
{sfDebtMaximum, soeDEFAULT},
{sfCoverAvailable, soeDEFAULT},
{sfCoverRateMinimum, soeDEFAULT},
{sfCoverRateLiquidation, soeDEFAULT},
}))
/** A ledger object representing a loan between a Borrower and a Loan Broker
\sa keylet::loan
*/
LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfLoanBrokerNode, soeREQUIRED},
{sfLoanBrokerID, soeREQUIRED},
{sfLoanSequence, 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},
{sfAssetsAvailable, soeREQUIRED},
{sfPrincipalOutstanding, soeREQUIRED},
// Save the original request amount for rounding / scaling of
// other computations, particularly for IOUs
{sfPrincipalRequested, soeREQUIRED},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE

View File

@@ -24,6 +24,8 @@
#error "undefined macro: TYPED_SFIELD"
#endif
// clang-format off
// untyped
UNTYPED_SFIELD(sfLedgerEntry, LEDGERENTRY, 257)
UNTYPED_SFIELD(sfTransaction, TRANSACTION, 257)
@@ -59,6 +61,7 @@ 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) // 1/10 basis points (bips)
// 32-bit integers (common)
TYPED_SFIELD(sfNetworkID, UINT32, 1)
@@ -114,6 +117,21 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48)
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
TYPED_SFIELD(sfStartDate, UINT32, 53)
TYPED_SFIELD(sfPaymentInterval, UINT32, 54)
TYPED_SFIELD(sfGracePeriod, UINT32, 55)
TYPED_SFIELD(sfPreviousPaymentDate, UINT32, 56)
TYPED_SFIELD(sfNextPaymentDueDate, UINT32, 57)
TYPED_SFIELD(sfPaymentRemaining, UINT32, 58)
TYPED_SFIELD(sfPaymentTotal, UINT32, 59)
TYPED_SFIELD(sfLoanSequence, UINT32, 60)
TYPED_SFIELD(sfCoverRateMinimum, UINT32, 61) // 1/10 basis points (bips)
TYPED_SFIELD(sfCoverRateLiquidation, UINT32, 62) // 1/10 basis points (bips)
TYPED_SFIELD(sfOverpaymentFee, UINT32, 63) // 1/10 basis points (bips)
TYPED_SFIELD(sfInterestRate, UINT32, 64) // 1/10 basis points (bips)
TYPED_SFIELD(sfLateInterestRate, UINT32, 65) // 1/10 basis points (bips)
TYPED_SFIELD(sfCloseInterestRate, UINT32, 66) // 1/10 basis points (bips)
TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 67) // 1/10 basis points (bips)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -144,6 +162,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)
@@ -197,6 +217,9 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
TYPED_SFIELD(sfDomainID, UINT256, 34)
TYPED_SFIELD(sfVaultID, UINT256, 35,
SField::sMD_PseudoAccount | SField::sMD_Default)
TYPED_SFIELD(sfLoanBrokerID, UINT256, 36,
SField::sMD_PseudoAccount | SField::sMD_Default)
TYPED_SFIELD(sfLoanID, UINT256, 37)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
@@ -204,6 +227,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, 10)
TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11)
TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12)
TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13)
TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
// currency amount (common)
TYPED_SFIELD(sfAmount, AMOUNT, 1)
@@ -299,6 +331,8 @@ 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)
TYPED_SFIELD(sfCounterparty, ACCOUNT, 26)
// vector of 256-bit
TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never)
@@ -359,6 +393,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 34, SField::sMD_Default, SField::notSigning)
// array of objects (common)
// ARRAY/1 is reserved for end of array
@@ -390,3 +425,5 @@ UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
UNTYPED_SFIELD(sfPermissions, ARRAY, 29)
// clang-format on

View File

@@ -814,6 +814,125 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
{sfAmount, soeOPTIONAL, soeMPTSupported},
}))
/** Reserve 71-73 for future Vault-related transactions */
/** This transaction creates and updates a Loan Broker */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanBrokerSet.h>
#endif
TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet,
Delegation::delegatable,
createPseudoAcct | mayAuthorizeMPT, ({
{sfVaultID, soeREQUIRED},
{sfLoanBrokerID, soeOPTIONAL},
{sfData, soeOPTIONAL},
{sfManagementFeeRate, soeOPTIONAL},
{sfDebtMaximum, soeOPTIONAL},
{sfCoverRateMinimum, soeOPTIONAL},
{sfCoverRateLiquidation, soeOPTIONAL},
}))
/** This transaction deletes a Loan Broker */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanBrokerDelete.h>
#endif
TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete,
Delegation::delegatable,
mustDeleteAcct | mayAuthorizeMPT, ({
{sfLoanBrokerID, soeREQUIRED},
}))
/** This transaction deposits First Loss Capital into a Loan Broker */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanBrokerCoverDeposit.h>
#endif
TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit,
Delegation::delegatable,
noPriv, ({
{sfLoanBrokerID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This transaction withdraws First Loss Capital from a Loan Broker */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h>
#endif
TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw,
Delegation::delegatable,
noPriv, ({
{sfLoanBrokerID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This transaction creates a Loan */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanSet.h>
#endif
TRANSACTION(ttLOAN_SET, 78, LoanSet,
Delegation::delegatable,
noPriv, ({
{sfLoanBrokerID, soeREQUIRED},
{sfData, soeOPTIONAL},
{sfCounterparty, soeOPTIONAL},
{sfCounterpartySignature, soeREQUIRED},
{sfLoanOriginationFee, soeOPTIONAL},
{sfLoanServiceFee, soeOPTIONAL},
{sfLatePaymentFee, soeOPTIONAL},
{sfClosePaymentFee, soeOPTIONAL},
{sfOverpaymentFee, soeOPTIONAL},
{sfInterestRate, soeOPTIONAL},
{sfLateInterestRate, soeOPTIONAL},
{sfCloseInterestRate, soeOPTIONAL},
{sfOverpaymentInterestRate, soeOPTIONAL},
{sfPrincipalRequested, soeREQUIRED},
{sfStartDate, soeREQUIRED},
{sfPaymentTotal, soeOPTIONAL},
{sfPaymentInterval, soeOPTIONAL},
{sfGracePeriod, soeOPTIONAL},
}))
/** This transaction deletes an existing Loan */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanDelete.h>
#endif
TRANSACTION(ttLOAN_DELETE, 79, LoanDelete,
Delegation::delegatable,
noPriv, ({
{sfLoanID, soeREQUIRED},
}))
/** This transaction is used to change the delinquency status of an existing Loan */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanManage.h>
#endif
TRANSACTION(ttLOAN_MANAGE, 80, LoanManage,
Delegation::delegatable,
noPriv, ({
{sfLoanID, soeREQUIRED},
}))
/** The Borrower uses this transaction to draws funds from the Loan. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanDraw.h>
#endif
TRANSACTION(ttLOAN_DRAW, 81, LoanDraw,
Delegation::delegatable,
noPriv, ({
{sfLoanID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** The Borrower uses this transaction to make a Payment on the Loan. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanPay.h>
#endif
TRANSACTION(ttLOAN_PAY, 82, LoanPay,
Delegation::delegatable,
noPriv, ({
{sfLoanID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html

View File

@@ -20,6 +20,8 @@
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <boost/predef.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>

View File

@@ -96,6 +96,8 @@ enum class LedgerNameSpace : std::uint16_t {
PERMISSIONED_DOMAIN = 'm',
DELEGATE = 'E',
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.
@@ -559,6 +561,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(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept
{
return loan(indexHash(LedgerNameSpace::LOAN, loanBrokerID, loanSeq));
}
Keylet
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept
{

View File

@@ -158,6 +158,14 @@ InnerObjectFormats::InnerObjectFormats()
add(sfPermission.jsonName.c_str(),
sfPermission.getCode(),
{{sfPermissionValue, soeREQUIRED}});
add(sfCounterpartySignature.jsonName,
sfCounterpartySignature.getCode(),
{
{sfSigningPubKey, soeOPTIONAL},
{sfTxnSignature, soeOPTIONAL},
{sfSigners, soeOPTIONAL},
});
}
InnerObjectFormats const&

View File

@@ -1353,6 +1353,30 @@ canonicalizeRoundStrict(
}
}
STAmount
roundToReference(
STAmount const value,
STAmount referenceValue,
Number::rounding_mode rounding)
{
if (value.asset().native() || !value.asset().holds<Issue>())
return value;
NumberRoundModeGuard mg(rounding);
if (referenceValue.negative() != value.negative())
referenceValue.negate();
if (value.exponent() > referenceValue.exponent() &&
(value.exponent() == referenceValue.exponent() &&
value.mantissa() >= referenceValue.mantissa()))
return value;
// With an IOU, the total will be truncated to the precision of the
// larger value: referenceValue
STAmount const total = referenceValue + value;
STAmount const result = total - referenceValue;
return result;
}
namespace {
// We need a class that has an interface similar to NumberRoundModeGuard

View File

@@ -682,6 +682,16 @@ STObject::getFieldV256(SField const& field) const
return getFieldByConstRef<STVector256>(field, empty);
}
STObject
STObject::getFieldObject(SField const& field) const
{
STObject const empty{field};
auto ret = getFieldByConstRef<STObject>(field, empty);
if (ret != empty)
ret.applyTemplateFromSField(field);
return ret;
}
STArray const&
STObject::getFieldArray(SField const& field) const
{

View File

@@ -197,11 +197,11 @@ STTx::getSigningHash() const
}
Blob
STTx::getSignature() const
STTx::getSignature(STObject const& sigObject) const
{
try
{
return getFieldVL(sfTxnSignature);
return sigObject.getFieldVL(sfTxnSignature);
}
catch (std::exception const&)
{
@@ -244,17 +244,20 @@ STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
Expected<void, std::string>
STTx::checkSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const
Rules const& rules,
STObject const* pSig) const
{
try
{
// Determine whether we're single- or multi-signing by looking
// at the SigningPubKey. If it's empty we must be
// multi-signing. Otherwise we're single-signing.
Blob const& signingPubKey = getFieldVL(sfSigningPubKey);
STObject const& sigObject{pSig ? *pSig : *this};
Blob const& signingPubKey = sigObject.getFieldVL(sfSigningPubKey);
return signingPubKey.empty()
? checkMultiSign(requireCanonicalSig, rules)
: checkSingleSign(requireCanonicalSig);
? checkMultiSign(requireCanonicalSig, rules, pSig)
: checkSingleSign(requireCanonicalSig, pSig);
}
catch (std::exception const&)
{
@@ -262,6 +265,24 @@ STTx::checkSign(
return Unexpected("Internal signature check failure.");
}
Expected<void, std::string>
STTx::checkSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const
{
if (auto const ret = checkSign(requireCanonicalSig, rules, nullptr); !ret)
return ret;
if (isFieldPresent(sfCounterpartySignature))
{
auto const counterSig = getFieldObject(sfCounterpartySignature);
if (auto const ret = checkSign(requireCanonicalSig, rules, &counterSig);
!ret)
return Unexpected("Counterparty: " + ret.error());
}
return {};
}
Json::Value
STTx::getJson(JsonOptions options) const
{
@@ -342,12 +363,16 @@ STTx::getMetaSQL(
}
Expected<void, std::string>
STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
STTx::checkSingleSign(
RequireFullyCanonicalSig requireCanonicalSig,
STObject const* pSig) const
{
STObject const& sigObject{pSig ? *pSig : *this};
// We don't allow both a non-empty sfSigningPubKey and an sfSigners.
// That would allow the transaction to be signed two ways. So if both
// fields are present the signature is invalid.
if (isFieldPresent(sfSigners))
if (sigObject.isFieldPresent(sfSigners))
return Unexpected("Cannot both single- and multi-sign.");
bool validSig = false;
@@ -356,11 +381,11 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
auto const spk = getFieldVL(sfSigningPubKey);
auto const spk = sigObject.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
{
Blob const signature = getFieldVL(sfTxnSignature);
Blob const signature = sigObject.getFieldVL(sfTxnSignature);
Blob const data = getSigningData(*this);
validSig = verify(
@@ -384,19 +409,22 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
Expected<void, std::string>
STTx::checkMultiSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const
Rules const& rules,
STObject const* pSig) const
{
STObject const& sigObject{pSig ? *pSig : *this};
// Make sure the MultiSigners are present. Otherwise they are not
// attempting multi-signing and we just have a bad SigningPubKey.
if (!isFieldPresent(sfSigners))
if (!sigObject.isFieldPresent(sfSigners))
return Unexpected("Empty SigningPubKey.");
// We don't allow both an sfSigners and an sfTxnSignature. Both fields
// being present would indicate that the transaction is signed both ways.
if (isFieldPresent(sfTxnSignature))
if (sigObject.isFieldPresent(sfTxnSignature))
return Unexpected("Cannot both single- and multi-sign.");
STArray const& signers{getFieldArray(sfSigners)};
STArray const& signers{sigObject.getFieldArray(sfSigners)};
// There are well known bounds that the number of signers must be within.
if (signers.size() < minMultiSigners ||
@@ -422,8 +450,8 @@ STTx::checkMultiSign(
{
auto const accountID = signer.getAccountID(sfAccount);
// The account owner may not multisign for themselves.
if (accountID == txnAccountID)
// The account owner may not usually multisign for themselves.
if (!pSig && accountID == txnAccountID)
return Unexpected("Invalid multisigner.");
// No duplicate signers allowed.

View File

@@ -0,0 +1,594 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/mpt.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_forwards.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
class LoanBroker_test : public beast::unit_test::suite
{
// Ensure that all the features needed for Lending Protocol are included,
// even if they are set to unsupported.
FeatureBitset const all{
jtx::supported_amendments() | featureMPTokensV1 |
featureSingleAssetVault | featureLendingProtocol};
void
testDisabled()
{
testcase("Disabled");
// Lending Protocol depends on Single Asset Vault (SAV). Test
// combinations of the two amendments.
// Single Asset Vault depends on MPTokensV1, but don't test every combo
// of that.
using namespace jtx;
auto failAll = [this](FeatureBitset features, bool goodVault = false) {
Env env(*this, features);
Account const alice{"alice"};
env.fund(XRP(10000), alice);
// Try to create a vault
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault vault{env};
auto const [tx, keylet] =
vault.create({.owner = alice, .asset = asset});
env(tx, ter(goodVault ? ter(tesSUCCESS) : ter(temDISABLED)));
env.close();
BEAST_EXPECT(static_cast<bool>(env.le(keylet)) == goodVault);
using namespace loanBroker;
// Can't create a loan broker regardless of whether the vault exists
env(set(alice, keylet.key), ter(temDISABLED));
auto const brokerKeylet =
keylet::loanbroker(alice.id(), env.seq(alice));
// Other LoanBroker transactions are disabled, too.
// 1. LoanBrokerCoverDeposit
env(coverDeposit(alice, brokerKeylet.key, asset(1000)),
ter(temDISABLED));
// 2. LoanBrokerCoverWithdraw
env(coverWithdraw(alice, brokerKeylet.key, asset(1000)),
ter(temDISABLED));
// 3. LoanBrokerDelete
env(del(alice, brokerKeylet.key), ter(temDISABLED));
};
failAll(all - featureMPTokensV1);
failAll(all - featureSingleAssetVault - featureLendingProtocol);
failAll(all - featureSingleAssetVault);
failAll(all - featureLendingProtocol, true);
}
struct VaultInfo
{
jtx::PrettyAsset asset;
uint256 vaultID;
VaultInfo(jtx::PrettyAsset const& asset_, uint256 const& vaultID_)
: asset(asset_), vaultID(vaultID_)
{
}
};
void
lifecycle(
char const* label,
jtx::Env& env,
jtx::Account const& alice,
jtx::Account const& evan,
VaultInfo const& vault,
std::function<jtx::JTx(jtx::JTx const&)> modifyJTx,
std::function<void(SLE::const_ref)> checkBroker,
std::function<void(SLE::const_ref)> changeBroker,
std::function<void(SLE::const_ref)> checkChangedBroker)
{
auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice));
{
auto const& asset = vault.asset.raw();
testcase << "Lifecycle: "
<< (asset.native() ? "XRP "
: asset.holds<Issue>() ? "IOU "
: asset.holds<MPTIssue>() ? "MPT "
: "Unknown ")
<< label;
}
using namespace jtx;
using namespace loanBroker;
{
// Start with default values
auto jtx = env.jt(set(alice, vault.vaultID));
// Modify as desired
if (modifyJTx)
jtx = modifyJTx(jtx);
// Successfully create a Loan Broker
env(jtx);
}
env.close();
if (auto broker = env.le(keylet); BEAST_EXPECT(broker))
{
// log << "Broker after create: " << to_string(broker->getJson())
// << std::endl;
BEAST_EXPECT(broker->at(sfVaultID) == vault.vaultID);
BEAST_EXPECT(broker->at(sfAccount) != alice.id());
BEAST_EXPECT(broker->at(sfOwner) == alice.id());
BEAST_EXPECT(broker->at(sfFlags) == 0);
BEAST_EXPECT(broker->at(sfSequence) == env.seq(alice) - 1);
BEAST_EXPECT(broker->at(sfOwnerCount) == 0);
BEAST_EXPECT(broker->at(sfLoanSequence) == 1);
BEAST_EXPECT(broker->at(sfDebtTotal) == 0);
BEAST_EXPECT(broker->at(sfCoverAvailable) == 0);
if (checkBroker)
checkBroker(broker);
// if (auto const vaultSLE = env.le(keylet::vault(vault.vaultID)))
//{
// log << "Vault: " << to_string(vaultSLE->getJson()) <<
// std::endl;
// }
// Load the pseudo-account
Account const pseudoAccount{
"Broker pseudo-account", broker->at(sfAccount)};
auto const pseudoKeylet = keylet::account(pseudoAccount);
if (auto const pseudo = env.le(pseudoKeylet); BEAST_EXPECT(pseudo))
{
// log << "Pseudo-account after create: "
// << to_string(pseudo->getJson()) << std::endl
// << std::endl;
BEAST_EXPECT(
pseudo->at(sfFlags) ==
(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth));
BEAST_EXPECT(pseudo->at(sfSequence) == 0);
BEAST_EXPECT(pseudo->at(sfBalance) == beast::zero);
BEAST_EXPECT(
pseudo->at(sfOwnerCount) ==
(vault.asset.raw().native() ? 0 : 1));
BEAST_EXPECT(!pseudo->isFieldPresent(sfAccountTxnID));
BEAST_EXPECT(!pseudo->isFieldPresent(sfRegularKey));
BEAST_EXPECT(!pseudo->isFieldPresent(sfEmailHash));
BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletLocator));
BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletSize));
BEAST_EXPECT(!pseudo->isFieldPresent(sfMessageKey));
BEAST_EXPECT(!pseudo->isFieldPresent(sfTransferRate));
BEAST_EXPECT(!pseudo->isFieldPresent(sfDomain));
BEAST_EXPECT(!pseudo->isFieldPresent(sfTickSize));
BEAST_EXPECT(!pseudo->isFieldPresent(sfTicketCount));
BEAST_EXPECT(!pseudo->isFieldPresent(sfNFTokenMinter));
BEAST_EXPECT(!pseudo->isFieldPresent(sfMintedNFTokens));
BEAST_EXPECT(!pseudo->isFieldPresent(sfBurnedNFTokens));
BEAST_EXPECT(!pseudo->isFieldPresent(sfFirstNFTokenSequence));
BEAST_EXPECT(!pseudo->isFieldPresent(sfAMMID));
BEAST_EXPECT(!pseudo->isFieldPresent(sfVaultID));
BEAST_EXPECT(pseudo->at(sfLoanBrokerID) == keylet.key);
}
auto verifyCoverAmount =
[&env, &vault, &broker, &pseudoAccount, this](auto n) {
auto const amount = vault.asset(n);
BEAST_EXPECT(
broker->at(sfCoverAvailable) == amount.number());
env.require(balance(pseudoAccount, amount));
};
// Test Cover funding before allowing alterations
env(coverDeposit(alice, uint256(0), vault.asset(10)),
ter(temINVALID));
env(coverDeposit(evan, keylet.key, vault.asset(10)),
ter(tecNO_PERMISSION));
env(coverDeposit(evan, keylet.key, vault.asset(0)),
ter(temBAD_AMOUNT));
env(coverDeposit(evan, keylet.key, vault.asset(-10)),
ter(temBAD_AMOUNT));
env(coverDeposit(alice, vault.vaultID, vault.asset(10)),
ter(tecNO_ENTRY));
verifyCoverAmount(0);
// Fund the cover deposit
env(coverDeposit(alice, keylet.key, vault.asset(10)));
if (BEAST_EXPECT(broker = env.le(keylet)))
{
verifyCoverAmount(10);
}
// Test withdrawal failure cases
env(coverWithdraw(alice, uint256(0), vault.asset(10)),
ter(temINVALID));
env(coverWithdraw(evan, keylet.key, vault.asset(10)),
ter(tecNO_PERMISSION));
env(coverWithdraw(evan, keylet.key, vault.asset(0)),
ter(temBAD_AMOUNT));
env(coverWithdraw(evan, keylet.key, vault.asset(-10)),
ter(temBAD_AMOUNT));
env(coverWithdraw(alice, vault.vaultID, vault.asset(10)),
ter(tecNO_ENTRY));
env(coverWithdraw(alice, keylet.key, vault.asset(900)),
ter(tecINSUFFICIENT_FUNDS));
// Withdraw some of the cover amount
env(coverWithdraw(alice, keylet.key, vault.asset(7)));
if (BEAST_EXPECT(broker = env.le(keylet)))
{
verifyCoverAmount(3);
}
// Add some more cover
env(coverDeposit(alice, keylet.key, vault.asset(5)));
if (BEAST_EXPECT(broker = env.le(keylet)))
{
verifyCoverAmount(8);
}
// Withdraw some more
env(coverWithdraw(alice, keylet.key, vault.asset(2)));
if (BEAST_EXPECT(broker = env.le(keylet)))
{
verifyCoverAmount(6);
}
env.close();
// no-op
env(set(alice, vault.vaultID), loanBrokerID(keylet.key));
// Make modifications to the broker
if (changeBroker)
changeBroker(broker);
env.close();
// Check the results of modifications
if (BEAST_EXPECT(broker = env.le(keylet)) && checkChangedBroker)
checkChangedBroker(broker);
// Verify that fields get removed when set to default values
// Debt maximum: explicit 0
// Data: explicit empty
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
debtMaximum(Number(0)),
data(""));
// Check the updated fields
if (BEAST_EXPECT(broker = env.le(keylet)))
{
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
BEAST_EXPECT(!broker->isFieldPresent(sfData));
}
/////////////////////////////////////
// try to delete the wrong broker object
env(del(alice, vault.vaultID), ter(tecNO_ENTRY));
// evan tries to delete the broker
env(del(evan, keylet.key), ter(tecNO_PERMISSION));
// Note alice's balance of the asset and the broker account's cover
// funds
auto const aliceBalance = env.balance(alice, vault.asset);
auto const coverFunds = env.balance(pseudoAccount, vault.asset);
BEAST_EXPECT(coverFunds.number() == broker->at(sfCoverAvailable));
BEAST_EXPECT(coverFunds != beast::zero);
verifyCoverAmount(6);
// delete the broker
// log << "Broker before delete: " << to_string(broker->getJson())
// << std::endl;
// if (auto const pseudo = env.le(pseudoKeylet);
// BEAST_EXPECT(pseudo))
//{
// log << "Pseudo-account before delete: "
// << to_string(pseudo->getJson()) << std::endl
// << std::endl;
//}
env(del(alice, keylet.key));
env.close();
{
broker = env.le(keylet);
BEAST_EXPECT(!broker);
auto pseudo = env.le(pseudoKeylet);
BEAST_EXPECT(!pseudo);
}
auto const expectedBalance = aliceBalance + coverFunds -
(aliceBalance.value().native()
? STAmount(env.current()->fees().base.value())
: vault.asset(0));
env.require(balance(alice, expectedBalance));
env.require(balance(pseudoAccount, vault.asset(none)));
}
}
void
testLifecycle()
{
testcase("Lifecycle");
using namespace jtx;
// Create 3 loan brokers: one for XRP, one for an IOU, and one for an
// MPT. That'll require three corresponding SAVs.
Env env(*this, all);
Account issuer{"issuer"};
// For simplicity, alice will be the sole actor for the vault & brokers.
Account alice{"alice"};
// Evan will attempt to be naughty
Account evan{"evan"};
Vault vault{env};
// Fund the accounts and trust lines with the same amount so that tests
// can use the same values regardless of the asset.
env.fund(XRP(100'000), issuer, noripple(alice, evan));
env.close();
// Create assets
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
PrettyAsset const iouAsset = issuer["IOU"];
env(trust(alice, iouAsset(1'000'000)));
env(trust(evan, iouAsset(1'000'000)));
env(pay(issuer, evan, iouAsset(100'000)));
env(pay(issuer, alice, iouAsset(100'000)));
env.close();
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create(
{.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
PrettyAsset const mptAsset = mptt["MPT"];
mptt.authorize({.account = alice});
mptt.authorize({.account = evan});
env(pay(issuer, alice, mptAsset(100'000)));
env(pay(issuer, evan, mptAsset(100'000)));
env.close();
std::array const assets{xrpAsset, iouAsset, mptAsset};
// Create vaults
std::vector<VaultInfo> vaults;
for (auto const& asset : assets)
{
auto [tx, keylet] = vault.create({.owner = alice, .asset = asset});
env(tx);
env.close();
BEAST_EXPECT(env.le(keylet));
vaults.emplace_back(asset, keylet.key);
env(vault.deposit(
{.depositor = alice, .id = keylet.key, .amount = asset(50)}));
env.close();
}
auto const aliceOriginalCount = env.ownerCount(alice);
// Create and update Loan Brokers
for (auto const& vault : vaults)
{
using namespace loanBroker;
auto badKeylet = keylet::vault(alice.id(), env.seq(alice));
// Try some failure cases
// not the vault owner
env(set(evan, vault.vaultID), ter(tecNO_PERMISSION));
// not a vault
env(set(alice, badKeylet.key), ter(tecNO_ENTRY));
// flags are checked first
env(set(evan, vault.vaultID, ~tfUniversal), ter(temINVALID_FLAG));
// field length validation
// sfData: good length, bad account
env(set(evan, vault.vaultID),
data(std::string(maxDataPayloadLength, 'X')),
ter(tecNO_PERMISSION));
// sfData: too long
env(set(evan, vault.vaultID),
data(std::string(maxDataPayloadLength + 1, 'Y')),
ter(temINVALID));
// sfManagementFeeRate: good value, bad account
env(set(evan, vault.vaultID),
managementFeeRate(maxManagementFeeRate),
ter(tecNO_PERMISSION));
// sfManagementFeeRate: too big
env(set(evan, vault.vaultID),
managementFeeRate(maxManagementFeeRate + TenthBips16(10)),
ter(temINVALID));
// sfCoverRateMinimum: good value, bad account
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate),
ter(tecNO_PERMISSION));
// sfCoverRateMinimum: too big
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate + 1),
ter(temINVALID));
// sfCoverRateLiquidation: good value, bad account
env(set(evan, vault.vaultID),
coverRateLiquidation(maxCoverRate),
ter(tecNO_PERMISSION));
// sfCoverRateLiquidation: too big
env(set(evan, vault.vaultID),
coverRateLiquidation(maxCoverRate + 1),
ter(temINVALID));
// sfDebtMaximum: good value, bad account
env(set(evan, vault.vaultID),
debtMaximum(Number(0)),
ter(tecNO_PERMISSION));
// sfDebtMaximum: overflow
env(set(evan, vault.vaultID),
debtMaximum(Number(1, 100)),
ter(temINVALID));
// sfDebtMaximum: negative
env(set(evan, vault.vaultID),
debtMaximum(Number(-1)),
ter(temINVALID));
std::string testData;
lifecycle(
"default fields",
env,
alice,
evan,
vault,
// No modifications
{},
[&](SLE::const_ref broker) {
// Extra checks
BEAST_EXPECT(!broker->isFieldPresent(sfManagementFeeRate));
BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateMinimum));
BEAST_EXPECT(
!broker->isFieldPresent(sfCoverRateLiquidation));
BEAST_EXPECT(!broker->isFieldPresent(sfData));
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
BEAST_EXPECT(broker->at(sfDebtMaximum) == 0);
BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 0);
BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0);
BEAST_EXPECT(
env.ownerCount(alice) == aliceOriginalCount + 2);
},
[&](SLE::const_ref broker) {
// Modifications
// Update the fields
auto const nextKeylet =
keylet::loanbroker(alice.id(), env.seq(alice));
// fields that can't be changed
// LoanBrokerID
env(set(alice, vault.vaultID),
loanBrokerID(nextKeylet.key),
ter(tecNO_ENTRY));
// VaultID
env(set(alice, nextKeylet.key),
loanBrokerID(broker->key()),
ter(tecNO_PERMISSION));
// Owner
env(set(evan, vault.vaultID),
loanBrokerID(broker->key()),
ter(tecNO_PERMISSION));
// ManagementFeeRate
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
managementFeeRate(maxManagementFeeRate),
ter(temINVALID));
// CoverRateMinimum
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
coverRateMinimum(maxManagementFeeRate),
ter(temINVALID));
// CoverRateLiquidation
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
coverRateLiquidation(maxManagementFeeRate),
ter(temINVALID));
// fields that can be changed
testData = "Test Data 1234";
// Bad data: too long
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
data(std::string(maxDataPayloadLength + 1, 'W')),
ter(temINVALID));
// Bad debt maximum
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
debtMaximum(Number(-175, -1)),
ter(temINVALID));
// Data & Debt maximum
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
data(testData),
debtMaximum(Number(175, -1)));
},
[&](SLE::const_ref broker) {
// Check the updated fields
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(175, -1));
});
lifecycle(
"non-default fields",
env,
alice,
evan,
vault,
[&](jtx::JTx const& jv) {
testData = "spam spam spam spam";
// Finally, create another Loan Broker with none of the
// values at default
return env.jt(
jv,
data(testData),
managementFeeRate(TenthBips16(123)),
debtMaximum(Number(9)),
coverRateMinimum(TenthBips32(100)),
coverRateLiquidation(TenthBips32(200)));
},
[&](SLE::const_ref broker) {
// Extra checks
BEAST_EXPECT(broker->at(sfManagementFeeRate) == 123);
BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 100);
BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 200);
BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(9));
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
},
[&](SLE::const_ref broker) {
// Reset Data & Debt maximum to default values
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
data(""),
debtMaximum(Number(0)));
},
[&](SLE::const_ref broker) {
// Check the updated fields
BEAST_EXPECT(!broker->isFieldPresent(sfData));
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
});
}
BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount);
}
public:
void
run() override
{
testDisabled();
testLifecycle();
}
};
BEAST_DEFINE_TESTSUITE(LoanBroker, tx, ripple);
} // namespace test
} // namespace ripple

1702
src/test/app/Loan_test.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -54,7 +54,11 @@ struct JTx
bool fill_sig = true;
bool fill_netid = true;
std::shared_ptr<STTx const> stx;
std::function<void(Env&, JTx&)> signer;
// Functions that sign the transaction from the Account
std::vector<std::function<void(Env&, JTx&)>> mainSigners;
// Functions that sign something else after the mainSigners, such as
// sfCounterpartySignature
std::vector<std::function<void(Env&, JTx&)>> postSigners;
JTx() = default;
JTx(JTx const&) = default;

View File

@@ -656,6 +656,114 @@ create(
} // namespace check
/* LoanBroker */
/******************************************************************************/
namespace loanBroker {
Json::Value
set(AccountID const& account, uint256 const& vaultId, std::uint32_t flags = 0);
// Use "del" because "delete" is a reserved word in C++.
Json::Value
del(AccountID const& account,
uint256 const& loanBrokerID,
std::uint32_t flags = 0);
Json::Value
coverDeposit(
AccountID const& account,
uint256 const& loanBrokerID,
STAmount const& amount,
std::uint32_t flags = 0);
Json::Value
coverWithdraw(
AccountID const& account,
uint256 const& loanBrokerID,
STAmount const& amount,
std::uint32_t flags = 0);
auto const loanBrokerID = JTxFieldWrapper<uint256Field>(sfLoanBrokerID);
auto const managementFeeRate =
valueUnitWrapper<SF_UINT16, unit::TenthBipsTag>(sfManagementFeeRate);
auto const debtMaximum = simpleField<SF_NUMBER>(sfDebtMaximum);
auto const coverRateMinimum =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfCoverRateMinimum);
auto const coverRateLiquidation =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfCoverRateLiquidation);
} // namespace loanBroker
/* Loan */
/******************************************************************************/
namespace loan {
Json::Value
set(AccountID const& account,
uint256 const& loanBrokerID,
Number principalRequested,
NetClock::time_point const& startDate,
std::uint32_t flags = 0);
auto const counterparty = JTxFieldWrapper<accountIDField>(sfCounterparty);
// For `CounterPartySignature`, use `sig(sfCounterpartySignature, ...)`
auto const loanOriginationFee = simpleField<SF_NUMBER>(sfLoanOriginationFee);
auto const loanServiceFee = simpleField<SF_NUMBER>(sfLoanServiceFee);
auto const latePaymentFee = simpleField<SF_NUMBER>(sfLatePaymentFee);
auto const closePaymentFee = simpleField<SF_NUMBER>(sfClosePaymentFee);
auto const overpaymentFee =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfOverpaymentFee);
auto const interestRate =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfInterestRate);
auto const lateInterestRate =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfLateInterestRate);
auto const closeInterestRate =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfCloseInterestRate);
auto const overpaymentInterestRate =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfOverpaymentInterestRate);
auto const paymentTotal = simpleField<SF_UINT32>(sfPaymentTotal);
auto const paymentInterval = simpleField<SF_UINT32>(sfPaymentInterval);
auto const gracePeriod = simpleField<SF_UINT32>(sfGracePeriod);
Json::Value
manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags);
Json::Value
del(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0);
Json::Value
draw(
AccountID const& account,
uint256 const& loanID,
STAmount const& amount,
std::uint32_t flags = 0);
Json::Value
pay(AccountID const& account,
uint256 const& loanID,
STAmount const& amount,
std::uint32_t flags = 0);
} // namespace loan
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -35,6 +35,7 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/scope.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
@@ -481,8 +482,22 @@ void
Env::autofill_sig(JTx& jt)
{
auto& jv = jt.jv;
if (jt.signer)
return jt.signer(*this, jt);
scope_success success([&]() {
// Call all the post-signers after the main signers or autofill are done
for (auto const& signer : jt.postSigners)
signer(*this, jt);
});
// Call all the main signers
if (!jt.mainSigners.empty())
{
for (auto const& signer : jt.mainSigners)
signer(*this, jt);
return;
}
// If the sig is still needed, get it here.
if (!jt.fill_sig)
return;
auto const account = jv.isMember(sfDelegate.jsonName)

View File

@@ -409,6 +409,142 @@ allpe(AccountID const& a, Issue const& iss)
iss.account);
};
/* LoanBroker */
/******************************************************************************/
namespace loanBroker {
Json::Value
set(AccountID const& account, uint256 const& vaultId, uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanBrokerSet;
jv[sfAccount] = to_string(account);
jv[sfVaultID] = to_string(vaultId);
jv[sfFlags] = flags;
return jv;
}
Json::Value
del(AccountID const& account, uint256 const& loanBrokerID, uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanBrokerDelete;
jv[sfAccount] = to_string(account);
jv[sfLoanBrokerID] = to_string(loanBrokerID);
jv[sfFlags] = flags;
return jv;
}
Json::Value
coverDeposit(
AccountID const& account,
uint256 const& loanBrokerID,
STAmount const& amount,
uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanBrokerCoverDeposit;
jv[sfAccount] = to_string(account);
jv[sfLoanBrokerID] = to_string(loanBrokerID);
jv[sfAmount] = amount.getJson(JsonOptions::none);
jv[sfFlags] = flags;
return jv;
}
Json::Value
coverWithdraw(
AccountID const& account,
uint256 const& loanBrokerID,
STAmount const& amount,
uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanBrokerCoverWithdraw;
jv[sfAccount] = to_string(account);
jv[sfLoanBrokerID] = to_string(loanBrokerID);
jv[sfAmount] = amount.getJson(JsonOptions::none);
jv[sfFlags] = flags;
return jv;
}
} // namespace loanBroker
/* Loan */
/******************************************************************************/
namespace loan {
Json::Value
set(AccountID const& account,
uint256 const& loanBrokerID,
Number principalRequested,
NetClock::time_point const& startDate,
std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanSet;
jv[sfAccount] = to_string(account);
jv[sfLoanBrokerID] = to_string(loanBrokerID);
jv[sfPrincipalRequested] = to_string(principalRequested);
jv[sfFlags] = flags;
jv[sfStartDate] = startDate.time_since_epoch().count();
return jv;
}
Json::Value
manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanManage;
jv[sfAccount] = to_string(account);
jv[sfLoanID] = to_string(loanID);
jv[sfFlags] = flags;
return jv;
}
Json::Value
del(AccountID const& account, uint256 const& loanID, std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanDelete;
jv[sfAccount] = to_string(account);
jv[sfLoanID] = to_string(loanID);
jv[sfFlags] = flags;
return jv;
}
Json::Value
draw(
AccountID const& account,
uint256 const& loanID,
STAmount const& amount,
std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanDraw;
jv[sfAccount] = to_string(account);
jv[sfLoanID] = to_string(loanID);
jv[sfAmount] = amount.getJson();
jv[sfFlags] = flags;
return jv;
}
Json::Value
pay(AccountID const& account,
uint256 const& loanID,
STAmount const& amount,
std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanPay;
jv[sfAccount] = to_string(account);
jv[sfLoanID] = to_string(loanID);
jv[sfAmount] = amount.getJson();
jv[sfFlags] = flags;
return jv;
}
} // namespace loan
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -65,7 +65,8 @@ signers(Account const& account, none_t)
//------------------------------------------------------------------------------
msig::msig(std::vector<msig::Reg> signers_) : signers(std::move(signers_))
msig::msig(SField const* subField_, std::vector<msig::Reg> signers_)
: signers(std::move(signers_)), subField(subField_)
{
// Signatures must be applied in sorted order.
std::sort(
@@ -80,8 +81,15 @@ void
msig::operator()(Env& env, JTx& jt) const
{
auto const mySigners = signers;
jt.signer = [mySigners, &env](Env&, JTx& jtx) {
jtx[sfSigningPubKey.getJsonName()] = "";
auto callback = [subField = subField, mySigners, &env](Env&, JTx& jtx) {
// Where to put the signature. Supports sfCounterPartySignature.
auto& sigObject = subField ? jtx[*subField] : jtx.jv;
// The signing pub key is only required at the top level.
if (!subField)
sigObject[sfSigningPubKey] = "";
else if (sigObject.isNull())
sigObject = Json::Value(Json::objectValue);
std::optional<STObject> st;
try
{
@@ -92,7 +100,7 @@ msig::operator()(Env& env, JTx& jt) const
env.test.log << pretty(jtx.jv) << std::endl;
Rethrow();
}
auto& js = jtx[sfSigners.getJsonName()];
auto& js = sigObject[sfSigners];
for (std::size_t i = 0; i < mySigners.size(); ++i)
{
auto const& e = mySigners[i];
@@ -107,6 +115,10 @@ msig::operator()(Env& env, JTx& jt) const
strHex(Slice{sig.data(), sig.size()});
}
};
if (!subField)
jt.mainSigners.emplace_back(callback);
else
jt.postSigners.emplace_back(callback);
}
} // namespace jtx

View File

@@ -29,12 +29,22 @@ sig::operator()(Env&, JTx& jt) const
{
if (!manual_)
return;
if (!subField)
jt.fill_sig = false;
if (account_)
{
// VFALCO Inefficient pre-C++14
auto const account = *account_;
jt.signer = [account](Env&, JTx& jtx) { jtx::sign(jtx.jv, account); };
auto callback = [subField = subField, account](Env&, JTx& jtx) {
// Where to put the signature. Supports sfCounterPartySignature.
auto& sigObject = subField ? jtx[*subField] : jtx.jv;
jtx::sign(jtx.jv, account, sigObject);
};
if (!subField)
jt.mainSigners.emplace_back(callback);
else
jt.postSigners.emplace_back(callback);
}
}

View File

@@ -44,14 +44,20 @@ parse(Json::Value const& jv)
}
void
sign(Json::Value& jv, Account const& account)
sign(Json::Value& jv, Account const& account, Json::Value& sigObject)
{
jv[jss::SigningPubKey] = strHex(account.pk().slice());
sigObject[jss::SigningPubKey] = strHex(account.pk().slice());
Serializer ss;
ss.add32(HashPrefix::txSign);
parse(jv).addWithoutSigningFields(ss);
auto const sig = ripple::sign(account.pk(), account.sk(), ss.slice());
jv[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()});
sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()});
}
void
sign(Json::Value& jv, Account const& account)
{
sign(jv, account, jv);
}
void

View File

@@ -96,14 +96,51 @@ public:
};
std::vector<Reg> signers;
SField const* const subField = nullptr;
static constexpr SField* const topLevel = nullptr;
public:
msig(std::vector<Reg> signers_);
msig(SField const* subField_, std::vector<Reg> signers_);
msig(SField const& subField_, std::vector<Reg> signers_)
: msig{&subField_, signers_}
{
}
msig(std::vector<Reg> signers_) : msig(topLevel, signers_)
{
}
template <class AccountType, class... Accounts>
requires std::convertible_to<AccountType, Reg>
explicit msig(SField const* subField_, AccountType&& a0, Accounts&&... aN)
: msig{
subField_,
std::vector<Reg>{
std::forward<AccountType>(a0),
std::forward<Accounts>(aN)...}}
{
}
template <class AccountType, class... Accounts>
requires std::convertible_to<AccountType, Reg>
explicit msig(SField const& subField_, AccountType&& a0, Accounts&&... aN)
: msig{
&subField_,
std::vector<Reg>{
std::forward<AccountType>(a0),
std::forward<Accounts>(aN)...}}
{
}
template <class AccountType, class... Accounts>
requires(
std::convertible_to<AccountType, Reg> &&
!std::is_same_v<AccountType, SField*>)
explicit msig(AccountType&& a0, Accounts&&... aN)
: msig{std::vector<Reg>{
: msig{
topLevel,
std::vector<Reg>{
std::forward<AccountType>(a0),
std::forward<Accounts>(aN)...}}
{

View File

@@ -35,7 +35,10 @@ class sig
{
private:
bool manual_ = true;
/// subField only supported with explicit account
SField const* const subField = nullptr;
std::optional<Account> account_;
static constexpr SField* const topLevel = nullptr;
public:
explicit sig(autofill_t) : manual_(false)
@@ -46,7 +49,17 @@ public:
{
}
explicit sig(Account const& account) : account_(account)
explicit sig(SField const* subField_, Account const& account)
: subField(subField_), account_(account)
{
}
explicit sig(SField const& subField_, Account const& account)
: sig(&subField_, account)
{
}
explicit sig(Account const& account) : sig(topLevel, account)
{
}

View File

@@ -51,6 +51,12 @@ struct parse_error : std::logic_error
STObject
parse(Json::Value const& jv);
/** Sign automatically into a specific Json field of the jv object.
@note This only works on accounts with multi-signing off.
*/
void
sign(Json::Value& jv, Account const& account, Json::Value& sigObject);
/** Sign automatically.
@note This only works on accounts with multi-signing off.
*/

View File

@@ -159,9 +159,10 @@ class Invariants_test : public beast::unit_test::suite
{{"an account root was deleted"}},
[](Account const& A1, Account const&, ApplyContext& ac) {
// remove an account from the view
auto const sle = ac.view().peek(keylet::account(A1.id()));
auto sle = ac.view().peek(keylet::account(A1.id()));
if (!sle)
return false;
sle->at(sfBalance) = beast::zero;
ac.view().erase(sle);
return true;
});
@@ -185,10 +186,12 @@ class Invariants_test : public beast::unit_test::suite
{{"account deletion succeeded but deleted multiple accounts"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
// remove two accounts from the view
auto const sleA1 = ac.view().peek(keylet::account(A1.id()));
auto const sleA2 = ac.view().peek(keylet::account(A2.id()));
auto sleA1 = ac.view().peek(keylet::account(A1.id()));
auto sleA2 = ac.view().peek(keylet::account(A2.id()));
if (!sleA1 || !sleA2)
return false;
sleA1->at(sfBalance) = beast::zero;
sleA2->at(sfBalance) = beast::zero;
ac.view().erase(sleA1);
ac.view().erase(sleA2);
return true;
@@ -203,6 +206,43 @@ class Invariants_test : public beast::unit_test::suite
using namespace test::jtx;
testcase << "account root deletion left artifact";
doInvariantCheck(
{{"account deletion left behind a non-zero balance"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
// A1 has a balance. Delete A1
auto const a1 = A1.id();
auto const sleA1 = ac.view().peek(keylet::account(a1));
if (!sleA1)
return false;
if (!BEAST_EXPECT(*sleA1->at(sfBalance) != beast::zero))
return false;
ac.view().erase(sleA1);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
doInvariantCheck(
{{"account deletion left behind a non-zero owner count"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
// Increment A1's owner count, then delete A1
auto const a1 = A1.id();
auto const sleA1 = ac.view().peek(keylet::account(a1));
if (!sleA1)
return false;
sleA1->at(sfBalance) = beast::zero;
BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0);
adjustOwnerCount(ac.view(), sleA1, 1, ac.journal);
ac.view().erase(sleA1);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
for (auto const& keyletInfo : directAccountKeylets)
{
// TODO: Use structured binding once LLVM 16 is the minimum
@@ -223,29 +263,32 @@ class Invariants_test : public beast::unit_test::suite
// Add an object to the ledger for account A1, then delete
// A1
auto const a1 = A1.id();
auto const sleA1 = ac.view().peek(keylet::account(a1));
auto sleA1 = ac.view().peek(keylet::account(a1));
if (!sleA1)
return false;
auto const key = std::invoke(keyletfunc, a1);
auto const newSLE = std::make_shared<SLE>(key);
ac.view().insert(newSLE);
sleA1->at(sfBalance) = beast::zero;
ac.view().erase(sleA1);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
};
}
// NFT special case
doInvariantCheck(
{{"account deletion left behind a NFTokenPage object"}},
[&](Account const& A1, Account const&, ApplyContext& ac) {
// remove an account from the view
auto const sle = ac.view().peek(keylet::account(A1.id()));
auto sle = ac.view().peek(keylet::account(A1.id()));
if (!sle)
return false;
sle->at(sfBalance) = beast::zero;
sle->at(sfOwnerCount) = 0;
ac.view().erase(sle);
return true;
},
@@ -269,13 +312,15 @@ class Invariants_test : public beast::unit_test::suite
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
// Delete the AMM account without cleaning up the directory or
// deleting the AMM object
auto const sle = ac.view().peek(keylet::account(ammAcctID));
auto sle = ac.view().peek(keylet::account(ammAcctID));
if (!sle)
return false;
BEAST_EXPECT(sle->at(~sfAMMID));
BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
sle->at(sfBalance) = beast::zero;
sle->at(sfOwnerCount) = 0;
ac.view().erase(sle);
return true;
@@ -298,7 +343,7 @@ class Invariants_test : public beast::unit_test::suite
// Delete all the AMM's trust lines, remove the AMM from the AMM
// account's directory (this deletes the directory), and delete
// the AMM account. Do not delete the AMM object.
auto const sle = ac.view().peek(keylet::account(ammAcctID));
auto sle = ac.view().peek(keylet::account(ammAcctID));
if (!sle)
return false;
@@ -338,6 +383,8 @@ class Invariants_test : public beast::unit_test::suite
!ac.view().exists(ownerDirKeylet) ||
ac.view().emptyDirDelete(ownerDirKeylet));
sle->at(sfBalance) = beast::zero;
sle->at(sfOwnerCount) = 0;
ac.view().erase(sle);
return true;
@@ -1301,6 +1348,121 @@ class Invariants_test : public beast::unit_test::suite
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
}
Keylet
createLoanBroker(
jtx::Account const& a,
jtx::Env& env,
jtx::PrettyAsset const& asset)
{
using namespace jtx;
// Create vault
uint256 vaultID;
Vault vault{env};
auto [tx, vKeylet] = vault.create({.owner = a, .asset = asset});
env(tx);
BEAST_EXPECT(env.le(vKeylet));
vaultID = vKeylet.key;
// Create Loan Broker
using namespace loanBroker;
auto const loanBrokerKeylet = keylet::loanbroker(a.id(), env.seq(a));
// Create a Loan Broker with all default values.
env(set(a, vaultID), fee(increment));
return loanBrokerKeylet;
};
void
testNoModifiedUnmodifiableFields()
{
testcase("no modified unmodifiable fields");
using namespace jtx;
// Initialize with a placeholder value because there's no default ctor
Keylet loanBrokerKeylet = keylet::amendments();
Preclose createLoanBroker =
[&, this](Account const& a, Account const& b, Env& env) {
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset);
return BEAST_EXPECT(env.le(loanBrokerKeylet));
};
{
auto const mods =
std::to_array<std::function<void(SLE::pointer&)>>({
[](SLE::pointer& sle) { sle->at(sfSequence) += 1; },
[](SLE::pointer& sle) { sle->at(sfOwnerNode) += 1; },
[](SLE::pointer& sle) { sle->at(sfVaultNode) += 1; },
[](SLE::pointer& sle) { sle->at(sfVaultID) = uint256(1u); },
[](SLE::pointer& sle) {
sle->at(sfAccount) = sle->at(sfOwner);
},
[](SLE::pointer& sle) {
sle->at(sfOwner) = sle->at(sfAccount);
},
[](SLE::pointer& sle) {
sle->at(sfManagementFeeRate) += 1;
},
[](SLE::pointer& sle) { sle->at(sfCoverRateMinimum) += 1; },
[](SLE::pointer& sle) {
sle->at(sfCoverRateLiquidation) += 1;
},
[](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
[](SLE::pointer& sle) {
sle->at(sfLedgerIndex) = sle->at(sfVaultID).value();
},
});
for (auto const& mod : mods)
{
doInvariantCheck(
{{"changed an unchangable field"}},
[&](Account const& A1, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(loanBrokerKeylet);
if (!sle)
return false;
mod(sle);
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
}
}
// TODO: Loan Object
{
auto const mods =
std::to_array<std::function<void(SLE::pointer&)>>({
[](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
[](SLE::pointer& sle) {
sle->at(sfLedgerIndex) = uint256(1u);
},
});
for (auto const& mod : mods)
{
doInvariantCheck(
{{"changed an unchangable field"}},
[&](Account const& A1, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(keylet::account(A1.id()));
if (!sle)
return false;
mod(sle);
ac.view().update(sle);
return true;
});
}
}
}
void
testValidPseudoAccounts()
{
@@ -1354,7 +1516,6 @@ class Invariants_test : public beast::unit_test::suite
sle->at(~sfAMMID) = ~sle->at(~sfVaultID);
},
},
/*
{
"pseudo-account has 2 pseudo-account fields set",
[this](SLE::pointer& sle) {
@@ -1363,7 +1524,6 @@ class Invariants_test : public beast::unit_test::suite
sle->at(~sfLoanBrokerID) = ~sle->at(~sfVaultID);
},
},
*/
{
"pseudo-account sequence changed",
[](SLE::pointer& sle) { sle->at(sfSequence) = 12345; },
@@ -1413,6 +1573,245 @@ class Invariants_test : public beast::unit_test::suite
return true;
});
}
void
testValidLoanBroker()
{
testcase << "valid loan broker";
using namespace jtx;
enum class Asset { XRP, IOU, MPT };
auto const assetTypes =
std::to_array({Asset::XRP, Asset::IOU, Asset::MPT});
for (auto const assetType : assetTypes)
{
// Initialize with a placeholder value because there's no default
// ctor
Keylet loanBrokerKeylet = keylet::amendments();
Preclose createLoanBroker = [&, this](
Account const& alice,
Account const& issuer,
Env& env) {
PrettyAsset const asset = [&]() {
switch (assetType)
{
case Asset::IOU: {
PrettyAsset const iouAsset = issuer["IOU"];
env(trust(alice, iouAsset(1000)));
env(pay(issuer, alice, iouAsset(1000)));
env.close();
return iouAsset;
}
case Asset::MPT: {
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create(
{.flags = tfMPTCanClawback | tfMPTCanTransfer |
tfMPTCanLock});
PrettyAsset const mptAsset = mptt.issuanceID();
mptt.authorize({.account = alice});
env(pay(issuer, alice, mptAsset(1000)));
env.close();
return mptAsset;
}
case Asset::XRP:
default:
return PrettyAsset{xrpIssue(), 1'000'000};
}
}();
loanBrokerKeylet = this->createLoanBroker(alice, env, asset);
return BEAST_EXPECT(env.le(loanBrokerKeylet));
};
// Ensure the test scenarios are set up completely. The test cases
// will need to recompute any of these values it needs for itself
// rather than trying to return a bunch of items
auto setupTest =
[&, this](Account const& A1, Account const&, ApplyContext& ac)
-> std::optional<std::pair<SLE::pointer, SLE::pointer>> {
if (loanBrokerKeylet.type != ltLOAN_BROKER)
return {};
auto sleBroker = ac.view().peek(loanBrokerKeylet);
if (!sleBroker)
return {};
if (!BEAST_EXPECT(sleBroker->at(sfOwnerCount) == 0))
return {};
// Need to touch sleBroker so that it is included in the
// modified entries for the invariant to find
ac.view().update(sleBroker);
// The pseudo-account holds the directory, so get it
auto const pseudoAccountID = sleBroker->at(sfAccount);
auto const pseudoAccountKeylet =
keylet::account(pseudoAccountID);
// Strictly speaking, we don't need to load the
// ACCOUNT_ROOT, but check anyway
auto slePseudo = ac.view().peek(pseudoAccountKeylet);
if (!BEAST_EXPECT(slePseudo))
return {};
// Make sure the directory doesn't already exist
auto const dirKeylet = keylet::ownerDir(pseudoAccountID);
auto sleDir = ac.view().peek(dirKeylet);
auto const describe = describeOwnerDir(pseudoAccountID);
if (!sleDir)
{
// Create the directory
BEAST_EXPECT(
directory::createRoot(
ac.view(),
dirKeylet,
loanBrokerKeylet.key,
describe) == 0);
sleDir = ac.view().peek(dirKeylet);
}
return std::make_pair(slePseudo, sleDir);
};
doInvariantCheck(
{{"Loan Broker with zero OwnerCount has multiple directory "
"pages"}},
[&setupTest, this](
Account const& A1, Account const& A2, ApplyContext& ac) {
auto test = setupTest(A1, A2, ac);
if (!test || !test->first || !test->second)
return false;
auto slePseudo = test->first;
auto sleDir = test->second;
auto const describe =
describeOwnerDir(slePseudo->at(sfAccount));
BEAST_EXPECT(
directory::insertPage(
ac.view(),
0,
sleDir,
0,
sleDir,
slePseudo->key(),
keylet::page(sleDir->key(), 0),
describe) == 1);
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
doInvariantCheck(
{{"Loan Broker with zero OwnerCount has multiple indexes in "
"the Directory root"}},
[&setupTest](
Account const& A1, Account const& A2, ApplyContext& ac) {
auto test = setupTest(A1, A2, ac);
if (!test || !test->first || !test->second)
return false;
auto slePseudo = test->first;
auto sleDir = test->second;
auto indexes = sleDir->getFieldV256(sfIndexes);
// Put some extra garbage into the directory
for (auto const& key : {slePseudo->key(), sleDir->key()})
{
directory::insertKey(
ac.view(), sleDir, 0, false, indexes, key);
}
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
doInvariantCheck(
{{"Loan Broker directory corrupt"}},
[&setupTest](
Account const& A1, Account const& A2, ApplyContext& ac) {
auto test = setupTest(A1, A2, ac);
if (!test || !test->first || !test->second)
return false;
auto slePseudo = test->first;
auto sleDir = test->second;
auto const describe =
describeOwnerDir(slePseudo->at(sfAccount));
// Empty vector will overwrite the existing entry for the
// holding, if any, avoiding the "has multiple indexes"
// failure.
STVector256 indexes;
// Put one meaningless key into the directory
auto const key =
keylet::account(Account("random").id()).key;
directory::insertKey(
ac.view(), sleDir, 0, false, indexes, key);
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
doInvariantCheck(
{{"Loan Broker with zero OwnerCount has an unexpected entry in "
"the directory"}},
[&setupTest](
Account const& A1, Account const& A2, ApplyContext& ac) {
auto test = setupTest(A1, A2, ac);
if (!test || !test->first || !test->second)
return false;
auto slePseudo = test->first;
auto sleDir = test->second;
// Empty vector will overwrite the existing entry for the
// holding, if any, avoiding the "has multiple indexes"
// failure.
STVector256 indexes;
directory::insertKey(
ac.view(), sleDir, 0, false, indexes, slePseudo->key());
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
doInvariantCheck(
{{"Loan Broker sequence number decreased"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
if (loanBrokerKeylet.type != ltLOAN_BROKER)
return false;
auto sleBroker = ac.view().peek(loanBrokerKeylet);
if (!sleBroker)
return false;
if (!BEAST_EXPECT(sleBroker->at(sfLoanSequence) > 0))
return false;
// Need to touch sleBroker so that it is included in the
// modified entries for the invariant to find
ac.view().update(sleBroker);
sleBroker->at(sfLoanSequence) -= 1;
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
}
}
public:
void
run() override
@@ -1431,7 +1830,9 @@ public:
testValidNewAccountRoot();
testNFTokenPageInvariants();
testPermissionedDomainInvariants();
testNoModifiedUnmodifiableFields();
testValidPseudoAccounts();
testValidLoanBroker();
}
};

View File

@@ -0,0 +1,618 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
#define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Expected.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/st.h>
#include <algorithm>
namespace ripple {
struct PreflightContext;
// Lending protocol has dependencies, so capture them here.
bool
lendingProtocolEnabled(PreflightContext const& ctx);
namespace detail {
// These functions should rarely be used directly. More often, the ultimate
// result needs to be roundToAsset'd.
struct LoanPaymentParts
{
Number principalPaid;
Number interestPaid;
Number valueChange;
Number feePaid;
};
Number
loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
Number
loanPeriodicPayment(
Number principalOutstanding,
Number periodicRate,
std::uint32_t paymentsRemaining);
Number
loanPeriodicPayment(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining);
template <AssetType A>
Number
loanTotalValueOutstanding(
A asset,
Number const& originalPrincipal,
Number const& periodicPayment,
std::uint32_t paymentsRemaining)
{
return roundToAsset(
asset,
periodicPayment * paymentsRemaining,
originalPrincipal,
Number::upward);
}
template <AssetType A>
Number
loanTotalValueOutstanding(
A asset,
Number const& originalPrincipal,
Number const& principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
return loanTotalValueOutstanding(
asset,
originalPrincipal,
loanPeriodicPayment(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining),
paymentsRemaining);
}
inline Number
loanTotalInterestOutstanding(
Number principalOutstanding,
Number totalValueOutstanding)
{
return totalValueOutstanding - principalOutstanding;
}
template <AssetType A>
Number
loanTotalInterestOutstanding(
A asset,
Number const& originalPrincipal,
Number const& principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
return loanTotalInterestOutstanding(
principalOutstanding,
loanTotalValueOutstanding(
asset,
originalPrincipal,
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining));
}
Number
loanLatePaymentInterest(
Number principalOutstanding,
TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate);
Number
loanAccruedInterest(
Number principalOutstanding,
Number periodicRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate,
std::uint32_t paymentInterval);
struct PeriodicPayment
{
Number interest;
Number principal;
};
template <AssetType A>
PeriodicPayment
computePeriodicPaymentParts(
A const& asset,
Number const& originalPrincipal,
Number const& principalOutstanding,
Number const& periodicPaymentAmount,
Number const& periodicRate,
std::uint32_t paymentRemaining)
{
if (paymentRemaining == 1)
{
// If there's only one payment left, we need to pay off the principal.
Number const interest = roundToAsset(
asset,
periodicPaymentAmount - principalOutstanding,
originalPrincipal);
return {interest, principalOutstanding};
}
Number const interest = roundToAsset(
asset, principalOutstanding * periodicRate, originalPrincipal);
XRPL_ASSERT(
interest >= 0,
"ripple::detail::computePeriodicPayment : valid interest");
auto const roundedPayment =
roundToAsset(asset, periodicPaymentAmount, originalPrincipal);
Number const principal =
roundToAsset(asset, roundedPayment - interest, originalPrincipal);
XRPL_ASSERT(
principal > 0 && principal <= principalOutstanding,
"ripple::detail::computePeriodicPayment : valid principal");
return {interest, principal};
}
inline Number
minusManagementFee(Number value, TenthBips32 managementFeeRate)
{
return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate);
}
} // namespace detail
template <AssetType A>
Number
valueMinusManagementFee(
A const& asset,
Number const& value,
TenthBips32 managementFeeRate,
Number const& originalPrincipal)
{
return roundToAsset(
asset,
detail::minusManagementFee(value, managementFeeRate),
originalPrincipal);
}
template <AssetType A>
Number
loanInterestOutstandingMinusFee(
A const& asset,
Number const& originalPrincipal,
Number const& principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate)
{
return valueMinusManagementFee(
asset,
detail::loanTotalInterestOutstanding(
asset,
originalPrincipal,
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining),
managementFeeRate,
originalPrincipal);
}
template <AssetType A>
Number
loanPeriodicPayment(
A const& asset,
Number const& principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining,
Number const& originalPrincipal)
{
return roundToAsset(
asset,
detail::loanPeriodicPayment(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining),
originalPrincipal);
}
template <AssetType A>
Number
loanLatePaymentInterest(
A const& asset,
Number const& principalOutstanding,
TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate,
Number const& originalPrincipal)
{
return roundToAsset(
asset,
detail::loanLatePaymentInterest(
principalOutstanding,
lateInterestRate,
parentCloseTime,
startDate,
prevPaymentDate),
originalPrincipal);
}
struct LoanPaymentParts
{
Number principalPaid;
Number interestPaid;
Number valueChange;
Number feePaid;
};
template <AssetType A>
Expected<LoanPaymentParts, TER>
loanComputePaymentParts(
A const& asset,
ApplyView& view,
SLE::ref loan,
STAmount const& amount,
beast::Journal j)
{
Number const originalPrincipalRequested = loan->at(sfPrincipalRequested);
auto principalOutstandingField = loan->at(sfPrincipalOutstanding);
bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment);
TenthBips32 const interestRate{loan->at(sfInterestRate)};
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
TenthBips32 const overpaymentInterestRate{
loan->at(sfOverpaymentInterestRate)};
Number const serviceFee = loan->at(sfLoanServiceFee);
Number const latePaymentFee = loan->at(sfLatePaymentFee);
Number const closePaymentFee = roundToAsset(
asset, loan->at(sfClosePaymentFee), originalPrincipalRequested);
TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)};
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
auto paymentRemainingField = loan->at(sfPaymentRemaining);
auto prevPaymentDateField = loan->at(sfPreviousPaymentDate);
std::uint32_t const startDate = loan->at(sfStartDate);
auto nextDueDateField = loan->at(sfNextPaymentDueDate);
if (paymentRemainingField == 0 || principalOutstandingField == 0)
{
// Loan complete
JLOG(j.warn()) << "Loan is already paid off.";
return Unexpected(tecKILLED);
}
// Compute the normal periodic rate, payment, etc.
// We'll need it in the remaining calculations
Number const periodicRate =
detail::loanPeriodicRate(interestRate, paymentInterval);
XRPL_ASSERT(
periodicRate > 0, "ripple::loanComputePaymentParts : valid rate");
// Don't round the payment amount. Only round the final computations using
// it.
Number const periodicPaymentAmount = detail::loanPeriodicPayment(
principalOutstandingField, periodicRate, paymentRemainingField);
XRPL_ASSERT(
periodicPaymentAmount > 0,
"ripple::computePeriodicPayment : valid payment");
auto const periodic = detail::computePeriodicPaymentParts(
asset,
originalPrincipalRequested,
principalOutstandingField,
periodicPaymentAmount,
periodicRate,
paymentRemainingField);
Number const totalValueOutstanding = detail::loanTotalValueOutstanding(
asset,
originalPrincipalRequested,
periodicPaymentAmount,
paymentRemainingField);
XRPL_ASSERT(
totalValueOutstanding > 0,
"ripple::loanComputePaymentParts : valid total value");
Number const totalInterestOutstanding =
detail::loanTotalInterestOutstanding(
principalOutstandingField, totalValueOutstanding);
XRPL_ASSERT_PARTS(
totalInterestOutstanding >= 0,
"ripple::loanComputePaymentParts",
"valid total interest");
XRPL_ASSERT_PARTS(
totalValueOutstanding - totalInterestOutstanding ==
principalOutstandingField,
"ripple::loanComputePaymentParts",
"valid principal computation");
view.update(loan);
// -------------------------------------------------------------
// late payment handling
if (hasExpired(view, nextDueDateField))
{
// the payment is late
auto const latePaymentInterest = loanLatePaymentInterest(
asset,
principalOutstandingField,
lateInterestRate,
view.parentCloseTime(),
startDate,
prevPaymentDateField,
originalPrincipalRequested);
XRPL_ASSERT(
latePaymentInterest >= 0,
"ripple::loanComputePaymentParts : valid late interest");
auto const latePaymentAmount =
periodicPaymentAmount + latePaymentInterest + latePaymentFee;
if (amount < latePaymentAmount)
{
JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: "
<< latePaymentAmount << ", paid: " << amount;
return Unexpected(tecINSUFFICIENT_PAYMENT);
}
paymentRemainingField -= 1;
// A single payment always pays the same amount of principal. Only the
// interest and fees are extra for a late payment
principalOutstandingField -= periodic.principal;
// Make sure this does an assignment
prevPaymentDateField = nextDueDateField;
nextDueDateField += paymentInterval;
// A late payment increases the value of the loan by the difference
// between periodic and late payment interest
return LoanPaymentParts{
periodic.principal,
latePaymentInterest + periodic.interest,
latePaymentInterest,
latePaymentFee};
}
// -------------------------------------------------------------
// full payment handling
if (paymentRemainingField > 1)
{
// If there is more than one payment remaining, see if enough was paid
// for a full payment
auto const accruedInterest = roundToAsset(
asset,
detail::loanAccruedInterest(
principalOutstandingField,
periodicRate,
view.parentCloseTime(),
startDate,
prevPaymentDateField,
paymentInterval),
originalPrincipalRequested);
XRPL_ASSERT(
accruedInterest >= 0,
"ripple::loanComputePaymentParts : valid accrued interest");
auto const closePrepaymentInterest = roundToAsset(
asset,
tenthBipsOfValue(
principalOutstandingField.value(), closeInterestRate),
originalPrincipalRequested);
XRPL_ASSERT(
closePrepaymentInterest >= 0,
"ripple::loanComputePaymentParts : valid prepayment "
"interest");
auto const totalInterest = accruedInterest + closePrepaymentInterest;
auto const closeFullPayment =
principalOutstandingField + totalInterest + closePaymentFee;
// if the payment is equal or higher than full payment amount, make a
// full payment
if (amount >= closeFullPayment)
{
// A full payment decreases the value of the loan by the
// difference between the interest paid and the expected
// outstanding interest return
auto const valueChange = totalInterest - totalInterestOutstanding;
LoanPaymentParts const result{
principalOutstandingField,
totalInterest,
valueChange,
closePaymentFee};
paymentRemainingField = 0;
principalOutstandingField = 0;
return result;
}
}
// -------------------------------------------------------------
// normal payment handling
// if the payment is not late nor if it's a full payment, then it must be a
// periodic one, with possible overpayments
auto const totalDue = roundToAsset(
asset,
periodicPaymentAmount + serviceFee,
originalPrincipalRequested,
Number::upward);
std::optional<NumberRoundModeGuard> mg(Number::downward);
std::int64_t const fullPeriodicPayments = [&]() {
std::int64_t const full{amount / totalDue};
return full < paymentRemainingField ? full : paymentRemainingField;
}();
mg.reset();
// Temporary asserts
XRPL_ASSERT(
amount >= totalDue || fullPeriodicPayments == 0,
"temp full periodic rounding");
XRPL_ASSERT(
amount < totalDue || fullPeriodicPayments >= 1,
"temp full periodic rounding");
if (fullPeriodicPayments < 1)
{
JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: "
<< periodicPaymentAmount << ", paid: " << amount;
return Unexpected(tecINSUFFICIENT_PAYMENT);
}
nextDueDateField += paymentInterval * fullPeriodicPayments;
prevPaymentDateField = nextDueDateField - paymentInterval;
Number totalPrincipalPaid = 0;
Number totalInterestPaid = 0;
Number loanValueChange = 0;
std::optional<detail::PeriodicPayment> future = periodic;
for (int i = 0; i < fullPeriodicPayments; ++i)
{
// Only do the work if we need to
if (!future)
future = detail::computePeriodicPaymentParts(
asset,
originalPrincipalRequested,
principalOutstandingField,
periodicPaymentAmount,
periodicRate,
paymentRemainingField);
XRPL_ASSERT(
future->interest <= periodic.interest,
"ripple::loanComputePaymentParts : decreasing interest");
XRPL_ASSERT(
future->principal >= periodic.principal,
"ripple::loanComputePaymentParts : increasing principal");
totalPrincipalPaid += future->principal;
totalInterestPaid += future->interest;
paymentRemainingField -= 1;
principalOutstandingField -= future->principal;
future.reset();
}
Number totalFeePaid = serviceFee * fullPeriodicPayments;
Number const newInterest = detail::loanTotalInterestOutstanding(
asset,
originalPrincipalRequested,
principalOutstandingField,
interestRate,
paymentInterval,
paymentRemainingField) +
totalInterestPaid;
Number overpaymentInterestPortion = 0;
if (allowOverpayment)
{
Number const overpayment = std::min(
principalOutstandingField.value(),
amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid));
if (roundToAsset(asset, overpayment, originalPrincipalRequested) > 0)
{
Number const interestPortion = roundToAsset(
asset,
tenthBipsOfValue(overpayment, overpaymentInterestRate),
originalPrincipalRequested);
Number const feePortion = roundToAsset(
asset,
tenthBipsOfValue(overpayment, overpaymentFee),
originalPrincipalRequested);
Number const remainder = roundToAsset(
asset,
overpayment - interestPortion - feePortion,
originalPrincipalRequested);
// Don't process an overpayment if the whole amount (or more!) gets
// eaten by fees
if (remainder > 0)
{
overpaymentInterestPortion = interestPortion;
totalPrincipalPaid += remainder;
totalInterestPaid += interestPortion;
totalFeePaid += feePortion;
principalOutstandingField -= remainder;
}
}
}
loanValueChange =
(newInterest - totalInterestOutstanding) + overpaymentInterestPortion;
// Check the final results are rounded, to double-check that the
// intermediate steps were rounded.
XRPL_ASSERT(
roundToAsset(asset, totalPrincipalPaid, originalPrincipalRequested) ==
totalPrincipalPaid,
"ripple::loanComputePaymentParts : totalPrincipalPaid rounded");
XRPL_ASSERT(
roundToAsset(asset, totalInterestPaid, originalPrincipalRequested) ==
totalInterestPaid,
"ripple::loanComputePaymentParts : totalInterestPaid rounded");
XRPL_ASSERT(
roundToAsset(asset, loanValueChange, originalPrincipalRequested) ==
loanValueChange,
"ripple::loanComputePaymentParts : loanValueChange rounded");
XRPL_ASSERT(
roundToAsset(asset, totalFeePaid, originalPrincipalRequested) ==
totalFeePaid,
"ripple::loanComputePaymentParts : totalFeePaid rounded");
return LoanPaymentParts{
totalPrincipalPaid, totalInterestPaid, loanValueChange, totalFeePaid};
}
} // namespace ripple
#endif // RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED

View File

@@ -0,0 +1,111 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/misc/LendingHelpers.h>
//
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
namespace ripple {
bool
lendingProtocolEnabled(PreflightContext const& ctx)
{
return ctx.rules.enabled(featureLendingProtocol) &&
VaultCreate::isEnabled(ctx);
}
namespace detail {
Number
loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
{
// Need floating point math for this one, since we're dividing by some
// large numbers
return tenthBipsOfValue(Number(paymentInterval), interestRate) /
(365 * 24 * 60 * 60);
}
Number
loanPeriodicPayment(
Number principalOutstanding,
Number periodicRate,
std::uint32_t paymentsRemaining)
{
// TODO: Need a better name
Number const timeFactor = power(1 + periodicRate, paymentsRemaining);
return principalOutstanding * periodicRate * timeFactor / (timeFactor - 1);
}
Number
loanPeriodicPayment(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
if (principalOutstanding == 0 || paymentsRemaining == 0)
return 0;
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
return loanPeriodicPayment(
principalOutstanding, periodicRate, paymentsRemaining);
}
Number
loanLatePaymentInterest(
Number principalOutstanding,
TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate)
{
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
auto const secondsSinceLastPayment =
parentCloseTime.time_since_epoch().count() - lastPaymentDate;
auto const rate =
loanPeriodicRate(lateInterestRate, secondsSinceLastPayment);
return principalOutstanding * rate;
}
Number
loanAccruedInterest(
Number principalOutstanding,
Number periodicRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate,
std::uint32_t paymentInterval)
{
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
auto const secondsSinceLastPayment =
parentCloseTime.time_since_epoch().count() - lastPaymentDate;
return principalOutstanding * periodicRate * secondsSinceLastPayment /
paymentInterval;
}
} // namespace detail
} // namespace ripple

View File

@@ -38,7 +38,11 @@ NotTEC
Transactor::preflight<Change>(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
if (auto const ret = preflight0(ctx, 0))
// The check for tfChangeMask is gated by LendingProtocol because that
// feature introduced this parameter, and it's not worth adding another
// amendment just for this.
if (auto const ret = preflight0(
ctx, ctx.rules.enabled(featureLendingProtocol) ? tfChangeMask : 0))
return ret;
auto account = ctx.tx.getAccountID(sfAccount);

View File

@@ -415,10 +415,10 @@ void
AccountRootsDeletedClean::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const&)
std::shared_ptr<SLE const> const& after)
{
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
accountsDeleted_.emplace_back(before);
accountsDeleted_.emplace_back(before, after);
}
bool
@@ -434,7 +434,8 @@ AccountRootsDeletedClean::finalize(
// feature is enabled. Enabled, or not, though, a fatal-level message will
// be logged
[[maybe_unused]] bool const enforce =
view.rules().enabled(featureInvariantsV1_1);
view.rules().enabled(featureInvariantsV1_1) ||
view.rules().enabled(featureLendingProtocol);
auto const objectExists = [&view, enforce, &j](auto const& keylet) {
(void)enforce;
@@ -462,9 +463,33 @@ AccountRootsDeletedClean::finalize(
return false;
};
for (auto const& accountSLE : accountsDeleted_)
for (auto const& [before, after] : accountsDeleted_)
{
auto const accountID = accountSLE->getAccountID(sfAccount);
auto const accountID = before->getAccountID(sfAccount);
// An account should not be deleted with a balance
if (after->at(sfBalance) != beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: account deletion left "
"behind a non-zero balance";
XRPL_ASSERT(
enforce,
"ripple::AccountRootsDeletedClean::finalize : "
"deleted account has zero balance");
if (enforce)
return false;
}
// An account should not be deleted with a non-zero owner count
if (after->at(sfOwnerCount) != 0)
{
JLOG(j.fatal()) << "Invariant failed: account deletion left "
"behind a non-zero owner count";
XRPL_ASSERT(
enforce,
"ripple::AccountRootsDeletedClean::finalize : "
"deleted account has zero owner count");
if (enforce)
return false;
}
// Simple types
for (auto const& [keyletfunc, _, __] : directAccountKeylets)
{
@@ -490,9 +515,9 @@ AccountRootsDeletedClean::finalize(
// Keys directly stored in the AccountRoot object
for (auto const& field : getPseudoAccountFields())
{
if (accountSLE->isFieldPresent(*field))
if (before->isFieldPresent(*field))
{
auto const key = accountSLE->getFieldH256(*field);
auto const key = before->getFieldH256(*field);
if (objectExists(keylet::unchecked(key)) && enforce)
return false;
}
@@ -949,7 +974,9 @@ ValidNewAccountRoot::finalize(
result == tesSUCCESS)
{
bool const pseudoAccount =
(pseudoAccount_ && view.rules().enabled(featureSingleAssetVault));
(pseudoAccount_ &&
(view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureLendingProtocol)));
if (pseudoAccount && !checkMyPrivilege(tx, createPseudoAcct))
{
@@ -1433,6 +1460,12 @@ ValidMPTIssuance::finalize(
"succeeded but deleted issuances";
return false;
}
else if (mptokensCreated_ + mptokensDeleted_ > 1)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
"but created/deleted bad number mptokens";
return false;
}
else if (
submittedByIssuer &&
(mptokensCreated_ > 0 || mptokensDeleted_ > 0))
@@ -1609,6 +1642,122 @@ ValidPermissionedDomain::finalize(
//------------------------------------------------------------------------------
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;
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) {
bool const beforeField = before->isFieldPresent(field);
bool const afterField = after->isFieldPresent(field);
return beforeField != afterField ||
(afterField && 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, sfLedgerEntryType) ||
fieldChanged(before, after, sfLedgerIndex) ||
fieldChanged(before, after, sfSequence) ||
fieldChanged(before, after, sfOwnerNode) ||
fieldChanged(before, after, sfVaultNode) ||
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, sfLedgerEntryType) ||
fieldChanged(before, after, sfLedgerIndex) ||
fieldChanged(before, after, sfSequence) ||
fieldChanged(before, after, sfOwnerNode) ||
fieldChanged(before, after, sfLoanBrokerNode) ||
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, sfStartDate) ||
fieldChanged(before, after, sfPaymentInterval) ||
fieldChanged(before, after, sfGracePeriod) ||
fieldChanged(before, after, sfPrincipalRequested);
break;
default:
/*
* We check this invariant regardless of lending protocol
* amendment status, allowing for detection and logging of
* potential issues even when the amendment is disabled.
*
* We use the lending protocol as a gate, even though
* all transactions are affected because that's when it
* was added.
*/
enforce = view.rules().enabled(featureLendingProtocol);
bad = fieldChanged(before, after, sfLedgerEntryType) ||
fieldChanged(before, after, sfLedgerIndex);
}
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;
}
//------------------------------------------------------------------------------
void
ValidPseudoAccounts::visitEntry(
bool isDelete,
@@ -1701,4 +1850,170 @@ ValidPseudoAccounts::finalize(
return true;
}
//------------------------------------------------------------------------------
void
ValidLoanBroker::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (after && after->getType() == ltLOAN_BROKER)
{
brokers_.emplace_back(before, after);
}
}
bool
ValidLoanBroker::goodZeroDirectory(
ReadView const& view,
SLE::const_ref dir,
beast::Journal const& j) const
{
auto const next = dir->at(~sfIndexNext);
auto const prev = dir->at(~sfIndexPrevious);
if ((prev && *prev) || (next && *next))
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
"OwnerCount has multiple directory pages";
return false;
}
auto indexes = dir->getFieldV256(sfIndexes);
if (indexes.size() > 1)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker with zero "
"OwnerCount has multiple indexes in the Directory root";
return false;
}
if (indexes.size() == 1)
{
auto const index = indexes.value().front();
auto const sle = view.read(keylet::unchecked(index));
if (!sle)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker directory corrupt";
return false;
}
if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker with zero "
"OwnerCount has an unexpected entry in the directory";
return false;
}
}
return true;
}
bool
ValidLoanBroker::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// 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_)
{
// 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`
// or `MPToken` objects.
if (after->at(sfOwnerCount) == 0)
{
auto const dir = view.read(keylet::ownerDir(after->at(sfAccount)));
if (dir)
{
if (!goodZeroDirectory(view, dir, j))
{
return false;
}
}
}
if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence))
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number "
"decreased";
return false;
}
if (after->at(sfDebtTotal) < 0)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker debt total is negative";
return false;
}
if (after->at(sfCoverAvailable) < 0)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker cover available is negative";
return false;
}
}
return true;
}
//------------------------------------------------------------------------------
void
ValidLoan::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (after && after->getType() == ltLOAN)
{
loans_.emplace_back(before, after);
}
}
bool
ValidLoan::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// 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] : loans_)
{
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
// If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
if (after->at(sfPaymentRemaining) == 0 &&
after->at(sfPrincipalOutstanding) != 0)
{
return false;
}
if (before &&
(before->isFlag(lsfLoanOverpayment) !=
after->isFlag(lsfLoanOverpayment)))
{
JLOG(j.fatal())
<< "Invariant failed: Loan Overpayment flag changed";
return false;
}
if (after->at(sfAssetsAvailable) < 0)
{
JLOG(j.fatal())
<< "Invariant failed: Loan assets available is negative";
return false;
}
if (after->at(sfPrincipalOutstanding) < 0)
{
JLOG(j.fatal())
<< "Invariant failed: Loan principal outstanding is negative";
return false;
}
}
return true;
}
} // namespace ripple

View File

@@ -174,7 +174,14 @@ public:
*/
class AccountRootsDeletedClean
{
std::vector<std::shared_ptr<SLE const>> accountsDeleted_;
// Pair is <before, after>. Before is used for most of the checks, so that
// if, for example, and object ID field is cleared, but the object is not
// deleted, it can still be found. After is used specifically for any checks
// that are expected as part of the deletion, such as zeroing out the
// balance.
std::vector<
std::pair<std::shared_ptr<SLE const>, std::shared_ptr<SLE const>>>
accountsDeleted_;
public:
void
@@ -618,6 +625,34 @@ 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
{
// Pair is <before, after>.
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&);
};
/**
* @brief Invariants: Pseudo-accounts have valid and consisent properties
*
@@ -646,6 +681,70 @@ public:
beast::Journal const&);
};
/**
* @brief Invariants: Loan brokers are internally consistent
*
* 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one
* node (the root), which will only hold entries for `RippleState` or
* `MPToken` objects.
*
*/
class ValidLoanBroker
{
// Pair is <before, after>. 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_;
bool
goodZeroDirectory(
ReadView const& view,
SLE::const_ref dir,
beast::Journal const& j) const;
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&);
};
/**
* @brief Invariants: Loans are internally consistent
*
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
*
*/
class ValidLoan
{
// Pair is <before, after>. After is used for most of the checks, except
// those that check changed values.
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
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<
@@ -666,7 +765,10 @@ using InvariantChecks = std::tuple<
ValidClawback,
ValidMPTIssuance,
ValidPermissionedDomain,
ValidPseudoAccounts>;
NoModifiedUnmodifiableFields,
ValidPseudoAccounts,
ValidLoanBroker,
ValidLoan>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -0,0 +1,147 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/LoanBrokerCoverDeposit.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
NotTEC
LoanBrokerCoverDeposit::doPreflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanBrokerID] == beast::zero)
return temINVALID;
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const brokerID = tx[sfLoanBrokerID];
auto const amount = tx[sfAmount];
auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (account != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)));
auto const vaultAsset = vault->at(sfAsset);
if (amount.asset() != vaultAsset)
return tecWRONG_ASSET;
auto const pseudoAccountID = sleBroker->at(sfAccount);
// Cannot transfer a frozen Asset
if (isFrozen(ctx.view, account, vaultAsset))
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
if (vaultAsset.holds<Issue>())
{
auto const issue = vaultAsset.get<Issue>();
if (isDeepFrozen(
ctx.view, pseudoAccountID, issue.currency, issue.account))
return tecFROZEN;
}
if (accountHolds(
ctx.view,
account,
vaultAsset,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,
ctx.j) < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
TER
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
auto const brokerPseudoID = broker->at(sfAccount);
// Transfer assets from depositor to pseudo-account.
if (auto ter = accountSend(
view(),
account_,
brokerPseudoID,
amount,
j_,
WaiveTransferFee::Yes))
return ter;
// Increase the LoanBroker's CoverAvailable by Amount
broker->at(sfCoverAvailable) += amount;
view().update(broker);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -0,0 +1,53 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED
#define RIPPLE_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanBrokerCoverDeposit : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanBrokerCoverDeposit(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
isEnabled(PreflightContext const& ctx);
static NotTEC
doPreflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -0,0 +1,164 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
NotTEC
LoanBrokerCoverWithdraw::doPreflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanBrokerID] == beast::zero)
return temINVALID;
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const brokerID = tx[sfLoanBrokerID];
auto const amount = tx[sfAmount];
auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (account != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)));
auto const vaultAsset = vault->at(sfAsset);
if (amount.asset() != vaultAsset)
return tecWRONG_ASSET;
// Cannot transfer a frozen Asset
auto const pseudoAccountID = sleBroker->at(sfAccount);
// Cannot transfer a frozen Asset
/*
if (isFrozen(ctx.view, account, vaultAsset))
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
*/
if (isFrozen(ctx.view, pseudoAccountID, vaultAsset))
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
if (vaultAsset.holds<Issue>())
{
auto const issue = vaultAsset.get<Issue>();
if (isDeepFrozen(ctx.view, account, issue.currency, issue.account))
return tecFROZEN;
}
auto const coverAvail = sleBroker->at(sfCoverAvailable);
// Cover Rate is in 1/10 bips units
auto const currentDebtTotal = sleBroker->at(sfDebtTotal);
auto const minimumCover = roundToAsset(
vaultAsset,
tenthBipsOfValue(
currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
currentDebtTotal);
if (coverAvail < amount)
return tecINSUFFICIENT_FUNDS;
if ((coverAvail - amount) < minimumCover)
return tecINSUFFICIENT_FUNDS;
if (accountHolds(
ctx.view,
pseudoAccountID,
vaultAsset,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,
ctx.j) < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
TER
LoanBrokerCoverWithdraw::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 tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerPseudoID = broker->at(sfAccount);
// Transfer assets from pseudo-account to depositor.
if (auto ter = accountSend(
view(),
brokerPseudoID,
account_,
amount,
j_,
WaiveTransferFee::Yes))
return ter;
// Increase the LoanBroker's CoverAvailable by Amount
broker->at(sfCoverAvailable) -= amount;
view().update(broker);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -0,0 +1,53 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED
#define RIPPLE_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanBrokerCoverWithdraw : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanBrokerCoverWithdraw(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
isEnabled(PreflightContext const& ctx);
static NotTEC
doPreflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -0,0 +1,179 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/LoanBrokerDelete.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanBrokerDelete::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
NotTEC
LoanBrokerDelete::doPreflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
TER
LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const brokerID = tx[sfLoanBrokerID];
auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (account != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
if (auto const ownerCount = sleBroker->at(sfOwnerCount); ownerCount != 0)
{
JLOG(ctx.j.warn()) << "LoanBrokerDelete: Owner count is " << ownerCount;
return tecHAS_OBLIGATIONS;
}
return tesSUCCESS;
}
TER
LoanBrokerDelete::doApply()
{
auto const& tx = ctx_.tx;
auto const brokerID = tx[sfLoanBrokerID];
// Delete the loan broker
auto broker = view().peek(keylet::loanbroker(brokerID));
if (!broker)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultID = broker->at(sfVaultID);
auto const sleVault = view().read(keylet::vault(vaultID));
if (!sleVault)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudoID = sleVault->at(sfAccount);
auto const vaultAsset = sleVault->at(sfAsset);
auto const brokerPseudoID = broker->at(sfAccount);
if (!view().dirRemove(
keylet::ownerDir(account_),
broker->at(sfOwnerNode),
broker->key(),
false))
{
return tefBAD_LEDGER;
}
if (!view().dirRemove(
keylet::ownerDir(vaultPseudoID),
broker->at(sfVaultNode),
broker->key(),
false))
{
return tefBAD_LEDGER;
}
{
auto const coverAvailable =
STAmount{vaultAsset, broker->at(sfCoverAvailable)};
if (auto const ter = accountSend(
view(),
brokerPseudoID,
account_,
coverAvailable,
j_,
WaiveTransferFee::Yes))
return ter;
}
if (auto ter = removeEmptyHolding(view(), brokerPseudoID, vaultAsset, j_))
return ter;
auto brokerPseudoSLE = view().peek(keylet::account(brokerPseudoID));
if (!brokerPseudoSLE)
return tefBAD_LEDGER;
// Making the payment and removing the empty holding should have deleted any
// obligations associated with the broker or broker pseudo-account.
if (*brokerPseudoSLE->at(sfBalance))
{
JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a balance";
return tecHAS_OBLIGATIONS;
}
if (brokerPseudoSLE->at(sfOwnerCount) != 0)
{
JLOG(j_.warn())
<< "LoanBrokerDelete: Pseudo-account still owns objects";
return tecHAS_OBLIGATIONS;
}
if (auto const directory = keylet::ownerDir(brokerPseudoID);
view().read(directory))
{
JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a directory";
return tecHAS_OBLIGATIONS;
}
view().erase(brokerPseudoSLE);
view().erase(broker);
{
auto owner = view().peek(keylet::account(account_));
if (!owner)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
adjustOwnerCount(view(), owner, -2, j_);
}
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -0,0 +1,53 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_LOANBROKERDELETE_H_INCLUDED
#define RIPPLE_TX_LOANBROKERDELETE_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanBrokerDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanBrokerDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
isEnabled(PreflightContext const& ctx);
static NotTEC
doPreflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -0,0 +1,210 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/LoanBrokerSet.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/SignerEntries.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanBrokerSet::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
NotTEC
LoanBrokerSet::doPreflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
if (auto const data = tx[~sfData]; data && !data->empty() &&
!validDataLength(tx[~sfData], maxDataPayloadLength))
return temINVALID;
if (!validNumericRange(tx[~sfManagementFeeRate], maxManagementFeeRate))
return temINVALID;
if (!validNumericRange(tx[~sfCoverRateMinimum], maxCoverRate))
return temINVALID;
if (!validNumericRange(tx[~sfCoverRateLiquidation], maxCoverRate))
return temINVALID;
if (!validNumericRange(
tx[~sfDebtMaximum], Number(maxMPTokenAmount), Number(0)))
return temINVALID;
if (tx.isFieldPresent(sfLoanBrokerID))
{
// Fixed fields can not be specified if we're modifying an existing
// LoanBroker Object
if (tx.isFieldPresent(sfManagementFeeRate) ||
tx.isFieldPresent(sfCoverRateMinimum) ||
tx.isFieldPresent(sfCoverRateLiquidation))
return temINVALID;
}
return tesSUCCESS;
}
TER
LoanBrokerSet::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
if (auto const brokerID = tx[~sfLoanBrokerID])
{
auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (tx[sfVaultID] != sleBroker->at(sfVaultID))
{
JLOG(ctx.j.warn())
<< "Can not change VaultID on an existing LoanBroker.";
return tecNO_PERMISSION;
}
if (account != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
}
else
{
auto const vaultID = tx[sfVaultID];
auto const sleVault = ctx.view.read(keylet::vault(vaultID));
if (!sleVault)
{
JLOG(ctx.j.warn()) << "Vault does not exist.";
return tecNO_ENTRY;
}
if (account != sleVault->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the Vault.";
return tecNO_PERMISSION;
}
if (auto const ter = canAddHolding(ctx.view, sleVault->at(sfAsset)))
return ter;
}
return tesSUCCESS;
}
TER
LoanBrokerSet::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
if (auto const brokerID = tx[~sfLoanBrokerID])
{
// Modify an existing LoanBroker
auto broker = view.peek(keylet::loanbroker(*brokerID));
if (!broker)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
if (auto const data = tx[~sfData])
broker->at(sfData) = *data;
if (auto const debtMax = tx[~sfDebtMaximum])
broker->at(sfDebtMaximum) = *debtMax;
view.update(broker);
}
else
{
// Create a new LoanBroker pointing back to the given Vault
auto const vaultID = tx[sfVaultID];
auto const sleVault = view.read(keylet::vault(vaultID));
auto const vaultPseudoID = sleVault->at(sfAccount);
auto const sequence = tx.getSeqValue();
auto owner = view.peek(keylet::account(account_));
if (!owner)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto broker =
std::make_shared<SLE>(keylet::loanbroker(account_, sequence));
if (auto const ter = dirLink(view, account_, broker))
return ter;
if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode))
return ter;
adjustOwnerCount(view, owner, 2, j_);
auto const ownerCount = owner->at(sfOwnerCount);
if (mPriorBalance < view.fees().accountReserve(ownerCount))
return tecINSUFFICIENT_RESERVE;
auto maybePseudo =
createPseudoAccount(view, broker->key(), sfLoanBrokerID);
if (!maybePseudo)
return maybePseudo.error();
auto& pseudo = *maybePseudo;
auto pseudoId = pseudo->at(sfAccount);
if (auto ter = addEmptyHolding(
view, pseudoId, mPriorBalance, sleVault->at(sfAsset), j_))
return ter;
// Initialize data fields:
broker->at(sfSequence) = sequence;
broker->at(sfVaultID) = vaultID;
broker->at(sfOwner) = account_;
broker->at(sfAccount) = pseudoId;
broker->at(sfLoanSequence) = 1;
if (auto const data = tx[~sfData])
broker->at(sfData) = *data;
if (auto const rate = tx[~sfManagementFeeRate])
broker->at(sfManagementFeeRate) = *rate;
if (auto const debtMax = tx[~sfDebtMaximum])
broker->at(sfDebtMaximum) = *debtMax;
if (auto const coverMin = tx[~sfCoverRateMinimum])
broker->at(sfCoverRateMinimum) = *coverMin;
if (auto const coverLiq = tx[~sfCoverRateLiquidation])
broker->at(sfCoverRateLiquidation) = *coverLiq;
view.insert(broker);
}
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -0,0 +1,53 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_LOANBROKERSET_H_INCLUDED
#define RIPPLE_TX_LOANBROKERSET_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanBrokerSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanBrokerSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
isEnabled(PreflightContext const& ctx);
static NotTEC
doPreflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -0,0 +1,167 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/LoanDelete.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanDelete::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
NotTEC
LoanDelete::doPreflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanID] == beast::zero)
return temINVALID;
return tesSUCCESS;
}
TER
LoanDelete::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const loanID = tx[sfLoanID];
auto const loanSle = ctx.view.read(keylet::loan(loanID));
if (!loanSle)
{
JLOG(ctx.j.warn()) << "Loan does not exist.";
return tecNO_ENTRY;
}
if (loanSle->at(sfPaymentRemaining) > 0)
{
JLOG(ctx.j.warn()) << "Active loan can not be deleted.";
return tecHAS_OBLIGATIONS;
}
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
if (!loanBrokerSle)
{
// should be impossible
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (loanBrokerSle->at(sfOwner) != account)
{
JLOG(ctx.j.warn())
<< "LoanBroker for Loan does not belong to the account.";
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
LoanDelete::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
auto const loanID = tx[sfLoanID];
auto const loanSle = view.peek(keylet::loan(loanID));
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const borrower = loanSle->at(sfBorrower);
auto const borrowerSle = view.peek(keylet::account(borrower));
if (!borrowerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultAsset = vaultSle->at(sfAsset);
// transfer any remaining funds to the borrower
auto assetsAvailableProxy = loanSle->at(sfAssetsAvailable);
if (assetsAvailableProxy != 0)
{
if (auto const ter = accountSend(
view,
brokerPseudoAccount,
borrower,
STAmount{vaultAsset, assetsAvailableProxy},
j_,
WaiveTransferFee::Yes))
return ter;
}
// Remove LoanID from Directory of the LoanBroker pseudo-account.
if (!view.dirRemove(
keylet::ownerDir(brokerPseudoAccount),
loanSle->at(sfLoanBrokerNode),
loanID,
false))
return tefBAD_LEDGER;
// Remove LoanID from Directory of the Borrower.
if (!view.dirRemove(
keylet::ownerDir(borrower),
loanSle->at(sfOwnerNode),
loanID,
false))
return tefBAD_LEDGER;
// Delete the Loan object
view.erase(loanSle);
// Decrement the LoanBroker's owner count.
adjustOwnerCount(view, brokerSle, -1, j_);
// Decrement the borrower's owner count
adjustOwnerCount(view, borrowerSle, -1, j_);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -0,0 +1,53 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_LOANDELETE_H_INCLUDED
#define RIPPLE_TX_LOANDELETE_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
isEnabled(PreflightContext const& ctx);
static NotTEC
doPreflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -0,0 +1,194 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/LoanDraw.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanDraw::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
NotTEC
LoanDraw::doPreflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanID] == beast::zero)
return temINVALID;
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
LoanDraw::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const loanID = tx[sfLoanID];
auto const amount = tx[sfAmount];
auto const loanSle = ctx.view.read(keylet::loan(loanID));
if (!loanSle)
{
JLOG(ctx.j.warn()) << "Loan does not exist.";
return tecNO_ENTRY;
}
if (loanSle->at(sfBorrower) != account)
{
JLOG(ctx.j.warn()) << "Loan does not belong to the account.";
return tecNO_PERMISSION;
}
if (loanSle->isFlag(lsfLoanImpaired) || loanSle->isFlag(lsfLoanDefault))
{
JLOG(ctx.j.warn()) << "Loan is impaired or in default.";
return tecNO_PERMISSION;
}
if (!hasExpired(ctx.view, loanSle->at(sfStartDate)))
{
JLOG(ctx.j.warn()) << "Loan has not started yet.";
return tecTOO_SOON;
}
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
if (!loanBrokerSle)
{
// This should be impossible
// LCOV_EXCL_START
JLOG(ctx.j.fatal()) << "LoanBroker does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount);
auto const vaultID = loanBrokerSle->at(sfVaultID);
auto const vaultSle = ctx.view.read(keylet::vault(vaultID));
if (!vaultSle)
{
// This should be impossible
// LCOV_EXCL_START
JLOG(ctx.j.fatal()) << "Vault does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const asset = vaultSle->at(sfAsset);
if (amount.asset() != asset)
{
JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset.";
return tecWRONG_ASSET;
}
if (loanSle->at(sfAssetsAvailable) < amount)
{
JLOG(ctx.j.warn()) << "Loan does not have enough assets available.";
return tecINSUFFICIENT_FUNDS;
}
if (isFrozen(ctx.view, brokerPseudoAccount, asset))
{
JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen.";
return asset.holds<Issue>() ? tecFROZEN : tecLOCKED;
}
if (asset.holds<Issue>())
{
auto const issue = asset.get<Issue>();
if (isDeepFrozen(ctx.view, account, issue.currency, issue.account))
{
JLOG(ctx.j.warn()) << "Borrower account is frozen.";
return tecFROZEN;
}
}
if (hasExpired(ctx.view, loanSle->at(sfNextPaymentDueDate)))
{
JLOG(ctx.j.warn()) << "Loan payment is overdue.";
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
LoanDraw::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
auto const amount = tx[sfAmount];
auto const loanID = tx[sfLoanID];
auto const loanSle = view.peek(keylet::loan(loanID));
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
if (auto const ter = accountSend(
view,
brokerPseudoAccount,
account_,
amount,
j_,
WaiveTransferFee::Yes))
return ter;
loanSle->at(sfAssetsAvailable) -= amount;
view.update(loanSle);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -0,0 +1,53 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_LOANDRAW_H_INCLUDED
#define RIPPLE_TX_LOANDRAW_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanDraw : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanDraw(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
isEnabled(PreflightContext const& ctx);
static NotTEC
doPreflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -0,0 +1,442 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/LoanManage.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/LoanSet.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanManage::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
std::uint32_t
LoanManage::getFlagsMask(PreflightContext const& ctx)
{
return tfLoanManageMask;
}
NotTEC
LoanManage::doPreflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanID] == beast::zero)
return temINVALID;
// Flags are mutually exclusive
int numFlags = 0;
for (auto const flag : {
tfLoanDefault,
tfLoanImpair,
tfLoanUnimpair,
})
{
if (ctx.tx.isFlag(flag))
++numFlags;
}
if (numFlags > 1)
{
JLOG(ctx.j.warn())
<< "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or "
"tfLoanUnimpair can be set.";
return temINVALID_FLAG;
}
return tesSUCCESS;
}
TER
LoanManage::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const loanID = tx[sfLoanID];
auto const loanSle = ctx.view.read(keylet::loan(loanID));
if (!loanSle)
{
JLOG(ctx.j.warn()) << "Loan does not exist.";
return tecNO_ENTRY;
}
// Impairment only allows certain transitions.
// 1. Once it's in default, it can't be changed.
// 2. It can get worse: unimpaired -> impaired -> default
// or unimpaired -> default
// 3. It can get better: impaired -> unimpaired
// 4. If it's in a state, it can't be put in that state again.
if (loanSle->isFlag(lsfLoanDefault))
{
JLOG(ctx.j.warn())
<< "Loan is in default. A defaulted loan can not be modified.";
return tecNO_PERMISSION;
}
if (loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair))
{
JLOG(ctx.j.warn())
<< "Loan is impaired. A loan can not be impaired twice.";
return tecNO_PERMISSION;
}
if (!(loanSle->isFlag(lsfLoanImpaired) ||
loanSle->isFlag(lsfLoanDefault)) &&
(tx.isFlag(tfLoanUnimpair)))
{
JLOG(ctx.j.warn())
<< "Loan is unimpaired. Can not be unimpaired again.";
return tecNO_PERMISSION;
}
if (loanSle->at(sfPaymentRemaining) == 0)
{
JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified "
"after it is fully paid.";
return tecNO_PERMISSION;
}
if (tx.isFlag(tfLoanDefault) &&
!hasExpired(
ctx.view,
loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod)))
{
JLOG(ctx.j.warn())
<< "A loan can not be defaulted before the next payment due date.";
return tecTOO_SOON;
}
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
if (!loanBrokerSle)
{
// should be impossible
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (loanBrokerSle->at(sfOwner) != account)
{
JLOG(ctx.j.warn())
<< "LoanBroker for Loan does not belong to the account.";
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
defaultLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref brokerSle,
SLE::ref vaultSle,
Number const& principalOutstanding,
Number const& interestOutstanding,
std::uint32_t paymentInterval,
Asset const& vaultAsset,
beast::Journal j)
{
// Calculate the amount of the Default that First-Loss Capital covers:
Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested);
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
auto const totalDefaultAmount = principalOutstanding + interestOutstanding;
// The default Amount equals the outstanding principal and interest,
// excluding any funds unclaimed by the Borrower.
auto loanAssetsAvailableProxy = loanSle->at(sfAssetsAvailable);
auto const defaultAmount = totalDefaultAmount - loanAssetsAvailableProxy;
// Apply the First-Loss Capital to the Default Amount
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{
brokerSle->at(sfCoverRateLiquidation)};
auto const defaultCovered = roundToAsset(
vaultAsset,
std::min(
tenthBipsOfValue(
tenthBipsOfValue(
brokerDebtTotalProxy.value(), coverRateMinimum),
coverRateLiquidation),
defaultAmount),
originalPrincipalRequested);
auto const returnToVault = defaultCovered + loanAssetsAvailableProxy;
auto const vaultDefaultAmount = defaultAmount - defaultCovered;
// Update the Vault object:
{
// Decrease the Total Value of the Vault:
auto vaultAssetsTotalProxy = vaultSle->at(sfAssetsTotal);
if (vaultAssetsTotalProxy < vaultDefaultAmount)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "Vault total assets is less than the vault default amount";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
vaultAssetsTotalProxy -= vaultDefaultAmount;
// Increase the Asset Available of the Vault by liquidated First-Loss
// Capital and any unclaimed funds amount:
vaultSle->at(sfAssetsAvailable) += returnToVault;
// The loss has been realized
if (loanSle->isFlag(lsfLoanImpaired))
{
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
if (vaultLossUnrealizedProxy < totalDefaultAmount)
{
JLOG(j.warn())
<< "Vault unrealized loss is less than the default amount";
return tefBAD_LEDGER;
}
vaultLossUnrealizedProxy -= totalDefaultAmount;
}
view.update(vaultSle);
}
// Update the LoanBroker object:
{
// Decrease the Debt of the LoanBroker:
if (brokerDebtTotalProxy < totalDefaultAmount)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "LoanBroker debt total is less than the default amount";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
brokerDebtTotalProxy -= totalDefaultAmount;
// Decrease the First-Loss Capital Cover Available:
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
if (coverAvailableProxy < defaultCovered)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "LoanBroker cover available is less than amount covered";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
coverAvailableProxy -= defaultCovered;
view.update(brokerSle);
}
// Update the Loan object:
loanSle->setFlag(lsfLoanDefault);
loanSle->at(sfPaymentRemaining) = 0;
loanAssetsAvailableProxy = 0;
loanSle->at(sfPrincipalOutstanding) = 0;
view.update(loanSle);
// Return funds from the LoanBroker pseudo-account to the
// Vault pseudo-account:
return accountSend(
view,
brokerSle->at(sfAccount),
vaultSle->at(sfAccount),
STAmount{vaultAsset, returnToVault},
j,
WaiveTransferFee::Yes);
}
TER
impairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref brokerSle,
SLE::ref vaultSle,
Number const& principalOutstanding,
Number const& interestOutstanding,
std::uint32_t paymentInterval,
Asset const& vaultAsset,
beast::Journal j)
{
// Update the Vault object(set "paper loss")
vaultSle->at(sfLossUnrealized) +=
principalOutstanding + interestOutstanding;
view.update(vaultSle);
// Update the Loan object
loanSle->setFlag(lsfLoanImpaired);
auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
if (!hasExpired(view, loanNextDueProxy))
{
// loan payment is not yet late -
// move the next payment due date to now
loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
}
view.update(loanSle);
return tesSUCCESS;
}
TER
unimpairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref brokerSle,
SLE::ref vaultSle,
Number const& principalOutstanding,
Number const& interestOutstanding,
std::uint32_t paymentInterval,
Asset const& vaultAsset,
beast::Journal j)
{
// Update the Vault object(clear "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
auto const lossReversed = principalOutstanding + interestOutstanding;
if (vaultLossUnrealizedProxy < lossReversed)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "Vault unrealized loss is less than the amount to be cleared";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
vaultLossUnrealizedProxy -= lossReversed;
view.update(vaultSle);
// Update the Loan object
loanSle->clearFlag(lsfLoanImpaired);
auto const normalPaymentDueDate =
std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) +
paymentInterval;
if (!hasExpired(view, normalPaymentDueDate))
{
// loan was unimpaired within the payment interval
loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
}
else
{
// loan was unimpaired after the original payment due date
loanSle->at(sfNextPaymentDueDate) =
view.parentCloseTime().time_since_epoch().count() + paymentInterval;
}
view.update(loanSle);
return tesSUCCESS;
}
TER
LoanManage::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
auto const loanID = tx[sfLoanID];
auto const loanSle = view.peek(keylet::loan(loanID));
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultAsset = vaultSle->at(sfAsset);
TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested);
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const paymentsRemaining = loanSle->at(sfPaymentRemaining);
auto const interestOutstanding = loanInterestOutstandingMinusFee(
vaultAsset,
originalPrincipalRequested,
principalOutstanding.value(),
interestRate,
paymentInterval,
paymentsRemaining,
managementFeeRate);
// Valid flag combinations are checked in preflight. No flags is valid -
// just a noop.
if (tx.isFlag(tfLoanDefault))
{
if (auto const ter = defaultLoan(
view,
loanSle,
brokerSle,
vaultSle,
principalOutstanding,
interestOutstanding,
paymentInterval,
vaultAsset,
j_))
return ter;
}
if (tx.isFlag(tfLoanImpair))
{
if (auto const ter = impairLoan(
view,
loanSle,
brokerSle,
vaultSle,
principalOutstanding,
interestOutstanding,
paymentInterval,
vaultAsset,
j_))
return ter;
}
if (tx.isFlag(tfLoanUnimpair))
{
if (auto const ter = unimpairLoan(
view,
loanSle,
brokerSle,
vaultSle,
principalOutstanding,
interestOutstanding,
paymentInterval,
vaultAsset,
j_))
return ter;
}
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_LOANMANAGE_H_INCLUDED
#define RIPPLE_TX_LOANMANAGE_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanManage : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanManage(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
isEnabled(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
doPreflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -0,0 +1,297 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/LoanPay.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanPay::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
NotTEC
LoanPay::doPreflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanID] == beast::zero)
return temINVALID;
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
LoanPay::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const loanID = tx[sfLoanID];
auto const amount = tx[sfAmount];
auto const loanSle = ctx.view.read(keylet::loan(loanID));
if (!loanSle)
{
JLOG(ctx.j.warn()) << "Loan does not exist.";
return tecNO_ENTRY;
}
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
auto const paymentRemaining = loanSle->at(sfPaymentRemaining);
TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)};
auto const startDate = loanSle->at(sfStartDate);
if (loanSle->at(sfBorrower) != account)
{
JLOG(ctx.j.warn()) << "Loan does not belong to the account.";
return tecNO_PERMISSION;
}
if (!hasExpired(ctx.view, startDate))
{
JLOG(ctx.j.warn()) << "Loan has not started yet.";
return tecTOO_SOON;
}
if (paymentRemaining == 0 || principalOutstanding == 0)
{
JLOG(ctx.j.warn()) << "Loan is already paid off.";
return tecKILLED;
}
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
if (!loanBrokerSle)
{
// This should be impossible
// LCOV_EXCL_START
JLOG(ctx.j.fatal()) << "LoanBroker does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount);
auto const vaultID = loanBrokerSle->at(sfVaultID);
auto const vaultSle = ctx.view.read(keylet::vault(vaultID));
if (!vaultSle)
{
// This should be impossible
// LCOV_EXCL_START
JLOG(ctx.j.fatal()) << "Vault does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const asset = vaultSle->at(sfAsset);
if (amount.asset() != asset)
{
JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset.";
return tecWRONG_ASSET;
}
if (isFrozen(ctx.view, brokerPseudoAccount, asset))
{
JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen.";
return asset.holds<Issue>() ? tecFROZEN : tecLOCKED;
}
if (asset.holds<Issue>())
{
auto const issue = asset.get<Issue>();
if (isDeepFrozen(ctx.view, account, issue.currency, issue.account))
{
JLOG(ctx.j.warn()) << "Borrower account is frozen.";
return tecFROZEN;
}
}
return tesSUCCESS;
}
TER
LoanPay::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
auto const amount = tx[sfAmount];
auto const loanID = tx[sfLoanID];
auto const loanSle = view.peek(keylet::loan(loanID));
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerOwner = brokerSle->at(sfOwner);
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
auto const vaultID = brokerSle->at(sfVaultID);
auto const vaultSle = view.peek(keylet::vault(vaultID));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudoAccount = vaultSle->at(sfAccount);
auto const asset = vaultSle->at(sfAsset);
//------------------------------------------------------
// Loan object state changes
Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested);
view.update(loanSle);
Expected<LoanPaymentParts, TER> paymentParts =
loanComputePaymentParts(asset, view, loanSle, amount, j_);
if (!paymentParts)
return paymentParts.error();
// If the loan was impaired, it isn't anymore.
loanSle->clearFlag(lsfLoanImpaired);
XRPL_ASSERT_PARTS(
paymentParts->principalPaid > 0,
"ripple::LoanPay::doApply",
"valid principal paid");
XRPL_ASSERT_PARTS(
paymentParts->interestPaid >= 0,
"ripple::LoanPay::doApply",
"valid interest paid");
XRPL_ASSERT_PARTS(
paymentParts->feePaid >= 0,
"ripple::LoanPay::doApply",
"valid fee paid");
//------------------------------------------------------
// LoanBroker object state changes
view.update(brokerSle);
TenthBips32 managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto const managementFee = roundToAsset(
asset,
tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate),
originalPrincipalRequested);
auto const totalPaidToVault = paymentParts->principalPaid +
paymentParts->interestPaid - managementFee;
auto const totalPaidToBroker = paymentParts->feePaid + managementFee;
// If there is not enough first-loss capital
auto coverAvailableField = brokerSle->at(sfCoverAvailable);
auto debtTotalField = brokerSle->at(sfDebtTotal);
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
bool const sufficientCover = coverAvailableField >=
roundToAsset(asset,
tenthBipsOfValue(debtTotalField.value(), coverRateMinimum),
originalPrincipalRequested);
if (!sufficientCover)
{
// Add the fee to to First Loss Cover Pool
coverAvailableField += totalPaidToBroker;
}
// Decrease LoanBroker Debt by the amount paid, add the Loan value change,
// and subtract the change in the management fee
auto const vaultValueChange = valueMinusManagementFee(
asset,
paymentParts->valueChange,
managementFeeRate,
originalPrincipalRequested);
// debtDecrease may be negative, increasing the debt
auto const debtDecrease = totalPaidToVault - vaultValueChange;
XRPL_ASSERT_PARTS(
roundToAsset(asset, debtDecrease, originalPrincipalRequested) ==
debtDecrease,
"ripple::LoanPay::doApply",
"debtDecrease rounding good");
if (debtDecrease >= debtTotalField)
debtTotalField = 0;
else
debtTotalField -= debtDecrease;
//------------------------------------------------------
// Vault object state changes
view.update(vaultSle);
vaultSle->at(sfAssetsAvailable) += totalPaidToVault;
vaultSle->at(sfAssetsTotal) += vaultValueChange;
// Move funds
STAmount const paidToVault(asset, totalPaidToVault);
STAmount const paidToBroker(asset, totalPaidToBroker);
XRPL_ASSERT_PARTS(
paidToVault + paidToBroker <= amount,
"ripple::LoanPay::doApply",
"amount is sufficient");
XRPL_ASSERT_PARTS(
paidToVault + paidToBroker <= paymentParts->principalPaid +
paymentParts->interestPaid + paymentParts->feePaid,
"ripple::LoanPay::doApply",
"payment agreement");
if (auto const ter = accountSend(
view,
account_,
vaultPseudoAccount,
paidToVault,
j_,
WaiveTransferFee::Yes))
return ter;
if (auto const ter = accountSend(
view,
account_,
sufficientCover ? brokerOwner : brokerPseudoAccount,
paidToBroker,
j_,
WaiveTransferFee::Yes))
return ter;
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -0,0 +1,53 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_LOANPAY_H_INCLUDED
#define RIPPLE_TX_LOANPAY_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanPay : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanPay(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
isEnabled(PreflightContext const& ctx);
static NotTEC
doPreflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -0,0 +1,418 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/tx/detail/LoanSet.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/SignerEntries.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanSet::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
std::uint32_t
LoanSet::getFlagsMask(PreflightContext const& ctx)
{
return tfLoanSetMask;
}
NotTEC
LoanSet::doPreflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
auto const counterPartySig = ctx.tx.getFieldObject(sfCounterpartySignature);
if (auto const ret =
ripple::detail::preflightCheckSigningKey(counterPartySig, ctx.j))
return ret;
if (auto const data = tx[~sfData]; data && !data->empty() &&
!validDataLength(tx[~sfData], maxDataPayloadLength))
return temINVALID;
if (!validNumericRange(tx[~sfOverpaymentFee], maxOverpaymentFee))
return temINVALID;
if (!validNumericRange(tx[~sfLateInterestRate], maxLateInterestRate))
return temINVALID;
if (!validNumericRange(tx[~sfCloseInterestRate], maxCloseInterestRate))
return temINVALID;
if (!validNumericRange(
tx[~sfOverpaymentInterestRate], maxOverpaymentInterestRate))
return temINVALID;
if (auto const paymentTotal = tx[~sfPaymentTotal];
paymentTotal && *paymentTotal == 0)
return temINVALID;
if (auto const paymentInterval =
tx[~sfPaymentInterval].value_or(LoanSet::defaultPaymentInterval);
paymentInterval < LoanSet::minPaymentInterval)
return temINVALID;
else if (auto const gracePeriod =
tx[~sfGracePeriod].value_or(LoanSet::defaultGracePeriod);
gracePeriod > paymentInterval)
return temINVALID;
// Copied from preflight2
if (auto const ret = ripple::detail::preflightCheckSimulateKeys(
ctx.flags, counterPartySig, ctx.j))
return *ret;
return tesSUCCESS;
}
NotTEC
LoanSet::checkSign(PreclaimContext const& ctx)
{
if (auto ret = Transactor::checkSign(ctx))
return ret;
// Counter signer is optional. If it's not specified, it's assumed to be
// `LoanBroker.Owner`. Note that we have not checked whether the
// loanbroker exists at this point.
auto const counterSigner = [&]() -> std::optional<AccountID> {
if (auto const c = ctx.tx.at(~sfCounterparty))
return c;
if (auto const broker =
ctx.view.read(keylet::loanbroker(ctx.tx[sfLoanBrokerID])))
return broker->at(sfOwner);
return std::nullopt;
}();
if (!counterSigner)
return temBAD_SIGNER;
// Counterparty signature is required
auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature);
return Transactor::checkSign(ctx, *counterSigner, counterSig);
}
XRPAmount
LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx)
{
auto const normalCost = Transactor::calculateBaseFee(view, tx);
// Compute the additional cost of each signature in the
// CounterpartySignature, whether a single signature or a multisignature
XRPAmount const baseFee = view.fees().base;
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
// Each signer adds one more baseFee to the minimum required fee
// for the transaction. Note that unlike the base class, if there are no
// signers, 1 extra signature is still counted for the single signer.
std::size_t const signerCount =
tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 1;
return normalCost + (signerCount * baseFee);
}
TER
LoanSet::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
if (auto const startDate(tx[sfStartDate]); hasExpired(ctx.view, startDate))
{
JLOG(ctx.j.warn()) << "Start date is in the past.";
return tecEXPIRED;
}
auto const account = tx[sfAccount];
auto const brokerID = tx[sfLoanBrokerID];
auto const brokerSle = ctx.view.read(keylet::loanbroker(brokerID));
if (!brokerSle)
{
// This can only be hit if there's a counterparty specified, otherwise
// it'll fail in the signature check
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
auto const brokerOwner = brokerSle->at(sfOwner);
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
if (account != brokerOwner && counterparty != brokerOwner)
{
JLOG(ctx.j.warn()) << "Neither Account nor Counterparty are the owner "
"of the LoanBroker.";
return tecNO_PERMISSION;
}
if (account == counterparty)
{
JLOG(ctx.j.warn()) << "Account and Counterparty are the same. Can not "
"loan money to yourself.";
return tecNO_PERMISSION;
}
auto const borrower = counterparty == brokerOwner ? account : counterparty;
if (auto const borrowerSle = ctx.view.read(keylet::account(borrower));
!borrowerSle)
{
// It may not be possible to hit this case, because it'll fail the
// signature check with terNO_ACCOUNT.
JLOG(ctx.j.warn()) << "Borrower does not exist.";
return terNO_ACCOUNT;
}
auto const brokerPseudo = brokerSle->at(sfAccount);
auto const vault = ctx.view.read(keylet::vault(brokerSle->at(sfVaultID)));
if (!vault)
// Should be impossible
return tefBAD_LEDGER; // LCOV_EXCL_LINE
Asset const asset = vault->at(sfAsset);
if (auto const ter = canAddHolding(ctx.view, asset))
return ter;
if (isFrozen(ctx.view, brokerOwner, asset) ||
isFrozen(ctx.view, brokerPseudo, asset))
{
JLOG(ctx.j.warn()) << "One of the affected accounts is frozen.";
return asset.holds<Issue>() ? tecFROZEN : tecLOCKED;
}
if (asset.holds<Issue>())
{
auto const issue = asset.get<Issue>();
if (isDeepFrozen(ctx.view, borrower, issue.currency, issue.account))
return tecFROZEN;
if (isDeepFrozen(ctx.view, brokerPseudo, issue.currency, issue.account))
return tecFROZEN;
}
auto const principalRequested = tx[sfPrincipalRequested];
if (auto const assetsAvailable = vault->at(sfAssetsAvailable);
assetsAvailable < principalRequested)
{
JLOG(ctx.j.warn())
<< "Insufficient assets available in the Vault to fund the loan.";
return tecINSUFFICIENT_FUNDS;
}
auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested;
if (brokerSle->at(sfDebtMaximum) < newDebtTotal)
{
JLOG(ctx.j.warn())
<< "Loan would exceed the maximum debt limit of the LoanBroker.";
return tecLIMIT_EXCEEDED;
}
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
if (brokerSle->at(sfCoverAvailable) <
tenthBipsOfValue(newDebtTotal, coverRateMinimum))
{
JLOG(ctx.j.warn())
<< "Insufficient first-loss capital to cover the loan.";
return tecINSUFFICIENT_FUNDS;
}
return tesSUCCESS;
}
TER
LoanSet::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
auto const brokerID = tx[sfLoanBrokerID];
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerOwner = brokerSle->at(sfOwner);
auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner));
if (!brokerOwnerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudo = vaultSle->at(sfAccount);
Asset const vaultAsset = vaultSle->at(sfAsset);
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
auto const borrower = counterparty == brokerOwner ? account_ : counterparty;
auto const borrowerSle = view.peek(keylet::account(borrower));
if (!borrowerSle)
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
auto const brokerPseudo = brokerSle->at(sfAccount);
auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo));
if (!brokerPseudoSle)
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
auto const principalRequested = roundToAsset(
vaultAsset, tx[sfPrincipalRequested], tx[sfPrincipalRequested]);
TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
auto const originationFee = tx[~sfLoanOriginationFee];
auto const loanAssetsAvailable =
principalRequested - originationFee.value_or(Number{});
adjustOwnerCount(view, borrowerSle, 1, j_);
auto ownerCount = borrowerSle->at(sfOwnerCount);
if (mPriorBalance < view.fees().accountReserve(ownerCount))
return tecINSUFFICIENT_RESERVE;
// Create a holding for the borrower if one does not already exist.
// Account for the origination fee using two payments
//
// 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
// from vault pseudo-account to LoanBroker pseudo-account.
if (auto const ter = accountSend(
view,
vaultPseudo,
brokerPseudo,
STAmount{vaultAsset, loanAssetsAvailable},
j_,
WaiveTransferFee::Yes))
return ter;
// 2. Transfer originationFee, if any, from vault pseudo-account to
// LoanBroker owner.
if (originationFee)
{
// Create the holding if it doesn't already exist (necessary for MPTs).
// The owner may have deleted their MPT / line at some point.
if (auto const ter = addEmptyHolding(
view,
brokerOwner,
brokerOwnerSle->at(sfBalance).value().xrp(),
vaultAsset,
j_);
ter != tesSUCCESS && ter != tecDUPLICATE)
// ignore tecDUPLICATE. That means the holding already exists, and
// is fine here
return ter;
if (auto const ter = accountSend(
view,
vaultPseudo,
brokerOwner,
STAmount{vaultAsset, *originationFee},
j_,
WaiveTransferFee::Yes))
return ter;
}
auto const paymentInterval =
tx[~sfPaymentInterval].value_or(defaultPaymentInterval);
auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal);
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
// The portion of the loan interest that will go to the vault (total
// interest minus the management fee)
auto const loanInterestToVault = loanInterestOutstandingMinusFee(
vaultAsset,
principalRequested,
principalRequested,
interestRate,
paymentInterval,
paymentTotal,
managementFeeRate);
auto const startDate = tx[sfStartDate];
auto loanSequence = brokerSle->at(sfLoanSequence);
// Create the loan
auto loan = std::make_shared<SLE>(keylet::loan(brokerID, *loanSequence));
// Prevent copy/paste errors
auto setLoanField =
[&loan, &tx](auto const& field, std::uint32_t const defValue = 0) {
// at() is smart enough to unseat a default field set to the default
// value
loan->at(field) = tx[field].value_or(defValue);
};
// Set required tx fields and pre-computed fields
loan->at(sfPrincipalRequested) = principalRequested;
loan->at(sfPrincipalOutstanding) = principalRequested;
loan->at(sfStartDate) = startDate;
loan->at(sfPaymentInterval) = paymentInterval;
loan->at(sfLoanSequence) = loanSequence;
loan->at(sfLoanBrokerID) = brokerID;
loan->at(sfBorrower) = borrower;
// Set all other transaction fields directly from the transaction
if (tx.isFlag(tfLoanOverpayment))
loan->setFlag(lsfLoanOverpayment);
setLoanField(~sfLoanOriginationFee);
setLoanField(~sfLoanServiceFee);
setLoanField(~sfLatePaymentFee);
setLoanField(~sfClosePaymentFee);
setLoanField(~sfOverpaymentFee);
setLoanField(~sfInterestRate);
setLoanField(~sfLateInterestRate);
setLoanField(~sfCloseInterestRate);
setLoanField(~sfOverpaymentInterestRate);
setLoanField(~sfGracePeriod, defaultGracePeriod);
// Set dynamic fields to their initial values
loan->at(sfPreviousPaymentDate) = 0;
loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
loan->at(sfPaymentRemaining) = paymentTotal;
loan->at(sfAssetsAvailable) = loanAssetsAvailable;
view.insert(loan);
// Update the balances in the vault
vaultSle->at(sfAssetsAvailable) -= principalRequested;
vaultSle->at(sfAssetsTotal) += loanInterestToVault;
view.update(vaultSle);
// Update the balances in the loan broker
brokerSle->at(sfDebtTotal) += principalRequested + loanInterestToVault;
// The broker's owner count is solely for the number of outstanding loans,
// and is distinct from the broker's pseudo-account's owner count
adjustOwnerCount(view, brokerSle, 1, j_);
loanSequence += 1;
view.update(brokerSle);
// Put the loan into the pseudo-account's directory
if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode))
return ter;
// Borrower is the owner of the loan
if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode))
return ter;
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -0,0 +1,74 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_LOANSET_H_INCLUDED
#define RIPPLE_TX_LOANSET_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
isEnabled(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
doPreflight(PreflightContext const& ctx);
static NotTEC
checkSign(PreclaimContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
public:
static std::uint32_t constexpr minPaymentTotal = 1;
static std::uint32_t constexpr defaultPaymentTotal = 1;
static_assert(defaultPaymentTotal >= minPaymentTotal);
static std::uint32_t constexpr minPaymentInterval = 60;
static std::uint32_t constexpr defaultPaymentInterval = 60;
static_assert(defaultPaymentInterval >= minPaymentInterval);
static std::uint32_t constexpr defaultGracePeriod = 60;
static_assert(defaultGracePeriod >= minPaymentInterval);
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -295,7 +295,9 @@ SetTrust::preclaim(PreclaimContext const& ctx)
else
return tecINTERNAL; // LCOV_EXCL_LINE
}
else if (sleDst->isFieldPresent(sfVaultID))
else if (
sleDst->isFieldPresent(sfVaultID) ||
sleDst->isFieldPresent(sfLoanBrokerID))
{
if (!ctx.view.exists(keylet::line(id, uDstAccountID, currency)))
return tecNO_PERMISSION;

View File

@@ -627,6 +627,16 @@ Transactor::checkSign(
AccountID const& id,
STObject const& sigObject)
{
{
auto const sle = ctx.view.read(keylet::account(id));
if (ctx.view.rules().enabled(featureLendingProtocol) &&
isPseudoAccount(sle))
// Pseudo-accounts can't sign transactions. This check is gated on
// the Lending Protocol amendment because that's the project it was
// added under, and it doesn't justify another amendment
return tefBAD_AUTH;
}
if (ctx.flags & tapDRY_RUN)
{
// This code must be different for `simulate`

View File

@@ -383,6 +383,45 @@ public:
emptyDirDelete(Keylet const& directory);
};
namespace directory {
/** Helper functions for managing low-level directory operations.
These are not part of the ApplyView interface.
Don't use them unless you really, really know what you're doing.
Instead use dirAdd, dirInsert, etc.
*/
std::uint64_t
createRoot(
ApplyView& view,
Keylet const& directory,
uint256 const& key,
std::function<void(std::shared_ptr<SLE> const&)> const& describe);
auto
findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start);
std::uint64_t
insertKey(
ApplyView& view,
SLE::ref node,
std::uint64_t page,
bool preserveOrder,
STVector256& indexes,
uint256 const& key);
std::optional<std::uint64_t>
insertPage(
ApplyView& view,
std::uint64_t page,
SLE::pointer node,
std::uint64_t nextPage,
SLE::ref next,
uint256 const& key,
Keylet const& directory,
std::function<void(std::shared_ptr<SLE> const&)> const& describe);
} // namespace directory
} // namespace ripple
#endif

View File

@@ -504,7 +504,11 @@ dirNext(
describeOwnerDir(AccountID const& account);
[[nodiscard]] TER
dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr<SLE>& object);
dirLink(
ApplyView& view,
AccountID const& owner,
std::shared_ptr<SLE>& object,
SF_UINT64 const& node = sfOwnerNode);
AccountID
pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey);

View File

@@ -25,46 +25,54 @@
namespace ripple {
std::optional<std::uint64_t>
ApplyView::dirAdd(
bool preserveOrder,
namespace directory {
std::uint64_t
createRoot(
ApplyView& view,
Keylet const& directory,
uint256 const& key,
std::function<void(std::shared_ptr<SLE> const&)> const& describe)
{
auto root = peek(directory);
if (!root)
{
// No root, make it.
root = std::make_shared<SLE>(directory);
root->setFieldH256(sfRootIndex, directory.key);
describe(root);
auto newRoot = std::make_shared<SLE>(directory);
newRoot->setFieldH256(sfRootIndex, directory.key);
describe(newRoot);
STVector256 v;
v.push_back(key);
root->setFieldV256(sfIndexes, v);
newRoot->setFieldV256(sfIndexes, v);
insert(root);
view.insert(newRoot);
return std::uint64_t{0};
}
}
std::uint64_t page = root->getFieldU64(sfIndexPrevious);
auto
findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start)
{
std::uint64_t page = start->getFieldU64(sfIndexPrevious);
auto node = root;
auto node = start;
if (page)
{
node = peek(keylet::page(directory, page));
node = view.peek(keylet::page(directory, page));
if (!node)
LogicError("Directory chain: root back-pointer broken.");
}
auto indexes = node->getFieldV256(sfIndexes);
return std::make_tuple(page, node, indexes);
}
// If there's space, we use it:
if (indexes.size() < dirNodeMaxEntries)
{
std::uint64_t
insertKey(
ApplyView& view,
SLE::ref node,
std::uint64_t page,
bool preserveOrder,
STVector256& indexes,
uint256 const& key)
{
if (preserveOrder)
{
if (std::find(indexes.begin(), indexes.end(), key) != indexes.end())
@@ -88,24 +96,37 @@ ApplyView::dirAdd(
}
node->setFieldV256(sfIndexes, indexes);
update(node);
view.update(node);
return page;
}
}
std::optional<std::uint64_t>
insertPage(
ApplyView& view,
std::uint64_t page,
SLE::pointer node,
std::uint64_t nextPage,
SLE::ref next,
uint256 const& key,
Keylet const& directory,
std::function<void(std::shared_ptr<SLE> const&)> const& describe)
{
// Check whether we're out of pages.
if (++page >= dirNodeMaxPages)
{
return std::nullopt;
}
// We are about to create a new node; we'll link it to
// the chain first:
node->setFieldU64(sfIndexNext, page);
update(node);
view.update(node);
root->setFieldU64(sfIndexPrevious, page);
update(root);
next->setFieldU64(sfIndexPrevious, page);
view.update(next);
// Insert the new key:
indexes.clear();
STVector256 indexes;
indexes.push_back(key);
node = std::make_shared<SLE>(keylet::page(directory, page));
@@ -116,12 +137,45 @@ ApplyView::dirAdd(
// it's the default.
if (page != 1)
node->setFieldU64(sfIndexPrevious, page - 1);
if (nextPage)
node->setFieldU64(sfIndexNext, nextPage);
describe(node);
insert(node);
view.insert(node);
return page;
}
} // namespace directory
std::optional<std::uint64_t>
ApplyView::dirAdd(
bool preserveOrder,
Keylet const& directory,
uint256 const& key,
std::function<void(std::shared_ptr<SLE> const&)> const& describe)
{
auto root = peek(directory);
if (!root)
{
// No root, make it.
return directory::createRoot(*this, directory, key, describe);
}
auto [page, node, indexes] =
directory::findPreviousPage(*this, directory, root);
// If there's space, we use it:
if (indexes.size() < dirNodeMaxEntries)
{
return directory::insertKey(
*this, node, page, preserveOrder, indexes, key);
}
return directory::insertPage(
*this, page, node, 0, root, key, directory, describe);
}
bool
ApplyView::emptyDirDelete(Keylet const& directory)
{

View File

@@ -1042,13 +1042,17 @@ describeOwnerDir(AccountID const& account)
}
TER
dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr<SLE>& object)
dirLink(
ApplyView& view,
AccountID const& owner,
std::shared_ptr<SLE>& object,
SF_UINT64 const& node)
{
auto const page = view.dirInsert(
keylet::ownerDir(owner), object->key(), describeOwnerDir(owner));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
object->setFieldU64(sfOwnerNode, *page);
object->setFieldU64(node, *page);
return tesSUCCESS;
}
@@ -1140,7 +1144,8 @@ createPseudoAccount(
// to 0 to make them easier to spot and verify, and add an extra level
// of protection.
std::uint32_t const seqno = //
view.rules().enabled(featureSingleAssetVault) //
view.rules().enabled(featureSingleAssetVault) || //
view.rules().enabled(featureLendingProtocol) //
? 0 //
: view.seq();
account->setFieldU32(sfSequence, seqno);