mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-21 19:45:53 +00:00
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:
committed by
Bronek Kozicki
parent
fb5d94bbef
commit
527e0c916f
@@ -343,6 +343,24 @@ vault(uint256 const& vaultKey)
|
|||||||
return {ltVAULT, 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
|
Keylet
|
||||||
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
|
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,11 @@ enum LedgerSpecificFlags {
|
|||||||
|
|
||||||
// ltVAULT
|
// ltVAULT
|
||||||
lsfVaultPrivate = 0x00010000,
|
lsfVaultPrivate = 0x00010000,
|
||||||
|
|
||||||
|
// ltLOAN
|
||||||
|
lsfLoanDefault = 0x00010000,
|
||||||
|
lsfLoanImpaired = 0x00020000,
|
||||||
|
lsfLoanOverpayment = 0x00040000, // True, loan allows overpayments
|
||||||
};
|
};
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -84,6 +84,90 @@ std::size_t constexpr maxDeletableTokenOfferEntries = 500;
|
|||||||
*/
|
*/
|
||||||
std::uint16_t constexpr maxTransferFee = 50000;
|
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 */
|
/** The maximum length of a URI inside an NFT */
|
||||||
std::size_t constexpr maxTokenURILength = 256;
|
std::size_t constexpr maxTokenURILength = 256;
|
||||||
|
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ field_code(int id, int index)
|
|||||||
SFields are created at compile time.
|
SFields are created at compile time.
|
||||||
|
|
||||||
Each SField, once constructed, lives until program termination, and there
|
Each SField, once constructed, lives until program termination, and there
|
||||||
is only one instance per fieldType/fieldValue pair which serves the entire
|
is only one instance per fieldType/fieldValue pair which serves the
|
||||||
application.
|
entire application.
|
||||||
*/
|
*/
|
||||||
class SField
|
class SField
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -695,6 +695,36 @@ divRoundStrict(
|
|||||||
std::uint64_t
|
std::uint64_t
|
||||||
getRate(STAmount const& offerOut, STAmount const& offerIn);
|
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
|
inline bool
|
||||||
@@ -703,10 +733,10 @@ isXRP(STAmount const& amount)
|
|||||||
return amount.native();
|
return amount.native();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since `canonicalize` does not have access to a ledger, this is needed to put
|
// Since `canonicalize` does not have access to a ledger, this is needed to
|
||||||
// the low-level routine stAmountCanonicalize on an amendment switch. Only
|
// put the low-level routine stAmountCanonicalize on an amendment switch.
|
||||||
// transactions need to use this switchover. Outside of a transaction it's safe
|
// Only transactions need to use this switchover. Outside of a transaction
|
||||||
// to unconditionally use the new behavior.
|
// it's safe to unconditionally use the new behavior.
|
||||||
|
|
||||||
bool
|
bool
|
||||||
getSTAmountCanonicalizeSwitchover();
|
getSTAmountCanonicalizeSwitchover();
|
||||||
|
|||||||
@@ -242,6 +242,9 @@ public:
|
|||||||
getFieldPathSet(SField const& field) const;
|
getFieldPathSet(SField const& field) const;
|
||||||
STVector256 const&
|
STVector256 const&
|
||||||
getFieldV256(SField const& field) 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&
|
STArray const&
|
||||||
getFieldArray(SField const& field) const;
|
getFieldArray(SField const& field) const;
|
||||||
STCurrency const&
|
STCurrency const&
|
||||||
@@ -515,7 +518,26 @@ protected:
|
|||||||
// Constraint += and -= ValueProxy operators
|
// Constraint += and -= ValueProxy operators
|
||||||
// to value types that support arithmetic operations
|
// to value types that support arithmetic operations
|
||||||
template <typename U>
|
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>
|
template <class T>
|
||||||
class STObject::ValueProxy : public Proxy<T>
|
class STObject::ValueProxy : public Proxy<T>
|
||||||
@@ -535,10 +557,12 @@ public:
|
|||||||
// Convenience operators for value types supporting
|
// Convenience operators for value types supporting
|
||||||
// arithmetic operations
|
// arithmetic operations
|
||||||
template <IsArithmetic U>
|
template <IsArithmetic U>
|
||||||
|
requires IsArithmeticCompatible<T, U>
|
||||||
ValueProxy&
|
ValueProxy&
|
||||||
operator+=(U const& u);
|
operator+=(U const& u);
|
||||||
|
|
||||||
template <IsArithmetic U>
|
template <IsArithmetic U>
|
||||||
|
requires IsArithmeticCompatible<T, U>
|
||||||
ValueProxy&
|
ValueProxy&
|
||||||
operator-=(U const& u);
|
operator-=(U const& u);
|
||||||
|
|
||||||
@@ -774,6 +798,7 @@ STObject::ValueProxy<T>::operator=(U&& u)
|
|||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
template <IsArithmetic U>
|
template <IsArithmetic U>
|
||||||
|
requires IsArithmeticCompatible<T, U>
|
||||||
STObject::ValueProxy<T>&
|
STObject::ValueProxy<T>&
|
||||||
STObject::ValueProxy<T>::operator+=(U const& u)
|
STObject::ValueProxy<T>::operator+=(U const& u)
|
||||||
{
|
{
|
||||||
@@ -783,6 +808,7 @@ STObject::ValueProxy<T>::operator+=(U const& u)
|
|||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
template <IsArithmetic U>
|
template <IsArithmetic U>
|
||||||
|
requires IsArithmeticCompatible<T, U>
|
||||||
STObject::ValueProxy<T>&
|
STObject::ValueProxy<T>&
|
||||||
STObject::ValueProxy<T>::operator-=(U const& u)
|
STObject::ValueProxy<T>::operator-=(U const& u)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,7 +88,13 @@ public:
|
|||||||
|
|
||||||
// Outer transaction functions / signature functions.
|
// Outer transaction functions / signature functions.
|
||||||
Blob
|
Blob
|
||||||
getSignature() const;
|
getSignature(STObject const& sigObject) const;
|
||||||
|
|
||||||
|
Blob
|
||||||
|
getSignature() const
|
||||||
|
{
|
||||||
|
return getSignature(*this);
|
||||||
|
}
|
||||||
|
|
||||||
uint256
|
uint256
|
||||||
getSigningHash() const;
|
getSigningHash() const;
|
||||||
@@ -121,10 +127,28 @@ public:
|
|||||||
void
|
void
|
||||||
sign(PublicKey const& publicKey, SecretKey const& secretKey);
|
sign(PublicKey const& publicKey, SecretKey const& secretKey);
|
||||||
|
|
||||||
|
enum class RequireFullyCanonicalSig : bool { no, yes };
|
||||||
|
|
||||||
/** Check the signature.
|
/** 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.
|
@return `true` if valid signature. If invalid, the error message string.
|
||||||
*/
|
*/
|
||||||
enum class RequireFullyCanonicalSig : bool { no, yes };
|
|
||||||
Expected<void, std::string>
|
Expected<void, std::string>
|
||||||
checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules)
|
checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules)
|
||||||
const;
|
const;
|
||||||
@@ -146,12 +170,15 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Expected<void, std::string>
|
Expected<void, std::string>
|
||||||
checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const;
|
checkSingleSign(
|
||||||
|
RequireFullyCanonicalSig requireCanonicalSig,
|
||||||
|
STObject const* pSig) const;
|
||||||
|
|
||||||
Expected<void, std::string>
|
Expected<void, std::string>
|
||||||
checkMultiSign(
|
checkMultiSign(
|
||||||
RequireFullyCanonicalSig requireCanonicalSig,
|
RequireFullyCanonicalSig requireCanonicalSig,
|
||||||
Rules const& rules) const;
|
Rules const& rules,
|
||||||
|
STObject const* pSig) const;
|
||||||
|
|
||||||
STBase*
|
STBase*
|
||||||
copy(std::size_t n, void* buf) const override;
|
copy(std::size_t n, void* buf) const override;
|
||||||
|
|||||||
@@ -242,6 +242,18 @@ constexpr std::uint32_t const tfVaultPrivate = 0x00010000;
|
|||||||
static_assert(tfVaultPrivate == lsfVaultPrivate);
|
static_assert(tfVaultPrivate == lsfVaultPrivate);
|
||||||
constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000;
|
constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000;
|
||||||
constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable);
|
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
|
// clang-format on
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -27,11 +27,12 @@
|
|||||||
#error "undefined macro: XRPL_RETIRE"
|
#error "undefined macro: XRPL_RETIRE"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
// Add new amendments to the top of this list.
|
// Add new amendments to the top of this list.
|
||||||
// Keep it sorted in reverse chronological order.
|
// 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(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo)
|
||||||
XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
@@ -146,3 +147,5 @@ XRPL_RETIRE(fix1201)
|
|||||||
XRPL_RETIRE(fix1512)
|
XRPL_RETIRE(fix1512)
|
||||||
XRPL_RETIRE(fix1523)
|
XRPL_RETIRE(fix1523)
|
||||||
XRPL_RETIRE(fix1528)
|
XRPL_RETIRE(fix1528)
|
||||||
|
|
||||||
|
// clang-format on
|
||||||
@@ -167,6 +167,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
|
|||||||
{sfFirstNFTokenSequence, soeOPTIONAL},
|
{sfFirstNFTokenSequence, soeOPTIONAL},
|
||||||
{sfAMMID, soeOPTIONAL}, // pseudo-account designator
|
{sfAMMID, soeOPTIONAL}, // pseudo-account designator
|
||||||
{sfVaultID, soeOPTIONAL}, // pseudo-account designator
|
{sfVaultID, soeOPTIONAL}, // pseudo-account designator
|
||||||
|
{sfLoanBrokerID, soeOPTIONAL}, // pseudo-account designator
|
||||||
}))
|
}))
|
||||||
|
|
||||||
/** A ledger object which contains a list of object identifiers.
|
/** 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)
|
// 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 EXPAND
|
||||||
#undef LEDGER_ENTRY_DUPLICATE
|
#undef LEDGER_ENTRY_DUPLICATE
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
#error "undefined macro: TYPED_SFIELD"
|
#error "undefined macro: TYPED_SFIELD"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
// untyped
|
// untyped
|
||||||
UNTYPED_SFIELD(sfLedgerEntry, LEDGERENTRY, 257)
|
UNTYPED_SFIELD(sfLedgerEntry, LEDGERENTRY, 257)
|
||||||
UNTYPED_SFIELD(sfTransaction, TRANSACTION, 257)
|
UNTYPED_SFIELD(sfTransaction, TRANSACTION, 257)
|
||||||
@@ -59,6 +61,7 @@ TYPED_SFIELD(sfHookEmitCount, UINT16, 18)
|
|||||||
TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
|
TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
|
||||||
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
|
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
|
||||||
TYPED_SFIELD(sfLedgerFixType, UINT16, 21)
|
TYPED_SFIELD(sfLedgerFixType, UINT16, 21)
|
||||||
|
TYPED_SFIELD(sfManagementFeeRate, UINT16, 22) // 1/10 basis points (bips)
|
||||||
|
|
||||||
// 32-bit integers (common)
|
// 32-bit integers (common)
|
||||||
TYPED_SFIELD(sfNetworkID, UINT32, 1)
|
TYPED_SFIELD(sfNetworkID, UINT32, 1)
|
||||||
@@ -114,6 +117,21 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48)
|
|||||||
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
|
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
|
||||||
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
||||||
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
|
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)
|
// 64-bit integers (common)
|
||||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
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(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default)
|
||||||
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
|
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
|
||||||
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
|
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
|
||||||
|
TYPED_SFIELD(sfVaultNode, UINT64, 29)
|
||||||
|
TYPED_SFIELD(sfLoanBrokerNode, UINT64, 30)
|
||||||
|
|
||||||
// 128-bit
|
// 128-bit
|
||||||
TYPED_SFIELD(sfEmailHash, UINT128, 1)
|
TYPED_SFIELD(sfEmailHash, UINT128, 1)
|
||||||
@@ -197,6 +217,9 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
|
|||||||
TYPED_SFIELD(sfDomainID, UINT256, 34)
|
TYPED_SFIELD(sfDomainID, UINT256, 34)
|
||||||
TYPED_SFIELD(sfVaultID, UINT256, 35,
|
TYPED_SFIELD(sfVaultID, UINT256, 35,
|
||||||
SField::sMD_PseudoAccount | SField::sMD_Default)
|
SField::sMD_PseudoAccount | SField::sMD_Default)
|
||||||
|
TYPED_SFIELD(sfLoanBrokerID, UINT256, 36,
|
||||||
|
SField::sMD_PseudoAccount | SField::sMD_Default)
|
||||||
|
TYPED_SFIELD(sfLoanID, UINT256, 37)
|
||||||
|
|
||||||
// number (common)
|
// number (common)
|
||||||
TYPED_SFIELD(sfNumber, NUMBER, 1)
|
TYPED_SFIELD(sfNumber, NUMBER, 1)
|
||||||
@@ -204,6 +227,15 @@ TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2)
|
|||||||
TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3)
|
TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3)
|
||||||
TYPED_SFIELD(sfAssetsTotal, NUMBER, 4)
|
TYPED_SFIELD(sfAssetsTotal, NUMBER, 4)
|
||||||
TYPED_SFIELD(sfLossUnrealized, NUMBER, 5)
|
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)
|
// currency amount (common)
|
||||||
TYPED_SFIELD(sfAmount, AMOUNT, 1)
|
TYPED_SFIELD(sfAmount, AMOUNT, 1)
|
||||||
@@ -299,6 +331,8 @@ TYPED_SFIELD(sfAttestationRewardAccount, ACCOUNT, 21)
|
|||||||
TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22)
|
TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22)
|
||||||
TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23)
|
TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23)
|
||||||
TYPED_SFIELD(sfSubject, ACCOUNT, 24)
|
TYPED_SFIELD(sfSubject, ACCOUNT, 24)
|
||||||
|
TYPED_SFIELD(sfBorrower, ACCOUNT, 25)
|
||||||
|
TYPED_SFIELD(sfCounterparty, ACCOUNT, 26)
|
||||||
|
|
||||||
// vector of 256-bit
|
// vector of 256-bit
|
||||||
TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never)
|
TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never)
|
||||||
@@ -359,6 +393,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
|
|||||||
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
|
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
|
||||||
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
|
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
|
||||||
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
|
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
|
||||||
|
UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 34, SField::sMD_Default, SField::notSigning)
|
||||||
|
|
||||||
// array of objects (common)
|
// array of objects (common)
|
||||||
// ARRAY/1 is reserved for end of array
|
// ARRAY/1 is reserved for end of array
|
||||||
@@ -390,3 +425,5 @@ UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
|
|||||||
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
|
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
|
||||||
UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
|
UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
|
||||||
UNTYPED_SFIELD(sfPermissions, ARRAY, 29)
|
UNTYPED_SFIELD(sfPermissions, ARRAY, 29)
|
||||||
|
|
||||||
|
// clang-format on
|
||||||
|
|||||||
@@ -814,6 +814,125 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
|
|||||||
{sfAmount, soeOPTIONAL, soeMPTSupported},
|
{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.
|
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||||
|
|
||||||
For details, see: https://xrpl.org/amendments.html
|
For details, see: https://xrpl.org/amendments.html
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
#include <xrpl/basics/Number.h>
|
#include <xrpl/basics/Number.h>
|
||||||
#include <xrpl/beast/utility/instrumentation.h>
|
#include <xrpl/beast/utility/instrumentation.h>
|
||||||
|
|
||||||
|
#include <boost/predef.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ enum class LedgerNameSpace : std::uint16_t {
|
|||||||
PERMISSIONED_DOMAIN = 'm',
|
PERMISSIONED_DOMAIN = 'm',
|
||||||
DELEGATE = 'E',
|
DELEGATE = 'E',
|
||||||
VAULT = 'V',
|
VAULT = 'V',
|
||||||
|
LOAN_BROKER = 'l', // lower-case L
|
||||||
|
LOAN = 'L',
|
||||||
|
|
||||||
// No longer used or supported. Left here to reserve the space
|
// No longer used or supported. Left here to reserve the space
|
||||||
// to avoid accidental reuse.
|
// to avoid accidental reuse.
|
||||||
@@ -559,6 +561,18 @@ vault(AccountID const& owner, std::uint32_t seq) noexcept
|
|||||||
return vault(indexHash(LedgerNameSpace::VAULT, owner, seq));
|
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
|
Keylet
|
||||||
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept
|
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -158,6 +158,14 @@ InnerObjectFormats::InnerObjectFormats()
|
|||||||
add(sfPermission.jsonName.c_str(),
|
add(sfPermission.jsonName.c_str(),
|
||||||
sfPermission.getCode(),
|
sfPermission.getCode(),
|
||||||
{{sfPermissionValue, soeREQUIRED}});
|
{{sfPermissionValue, soeREQUIRED}});
|
||||||
|
|
||||||
|
add(sfCounterpartySignature.jsonName,
|
||||||
|
sfCounterpartySignature.getCode(),
|
||||||
|
{
|
||||||
|
{sfSigningPubKey, soeOPTIONAL},
|
||||||
|
{sfTxnSignature, soeOPTIONAL},
|
||||||
|
{sfSigners, soeOPTIONAL},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerObjectFormats const&
|
InnerObjectFormats const&
|
||||||
|
|||||||
@@ -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 {
|
namespace {
|
||||||
|
|
||||||
// We need a class that has an interface similar to NumberRoundModeGuard
|
// We need a class that has an interface similar to NumberRoundModeGuard
|
||||||
|
|||||||
@@ -682,6 +682,16 @@ STObject::getFieldV256(SField const& field) const
|
|||||||
return getFieldByConstRef<STVector256>(field, empty);
|
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&
|
STArray const&
|
||||||
STObject::getFieldArray(SField const& field) const
|
STObject::getFieldArray(SField const& field) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -197,11 +197,11 @@ STTx::getSigningHash() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
Blob
|
Blob
|
||||||
STTx::getSignature() const
|
STTx::getSignature(STObject const& sigObject) const
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return getFieldVL(sfTxnSignature);
|
return sigObject.getFieldVL(sfTxnSignature);
|
||||||
}
|
}
|
||||||
catch (std::exception const&)
|
catch (std::exception const&)
|
||||||
{
|
{
|
||||||
@@ -244,17 +244,20 @@ STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
|
|||||||
Expected<void, std::string>
|
Expected<void, std::string>
|
||||||
STTx::checkSign(
|
STTx::checkSign(
|
||||||
RequireFullyCanonicalSig requireCanonicalSig,
|
RequireFullyCanonicalSig requireCanonicalSig,
|
||||||
Rules const& rules) const
|
Rules const& rules,
|
||||||
|
STObject const* pSig) const
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Determine whether we're single- or multi-signing by looking
|
// Determine whether we're single- or multi-signing by looking
|
||||||
// at the SigningPubKey. If it's empty we must be
|
// at the SigningPubKey. If it's empty we must be
|
||||||
// multi-signing. Otherwise we're single-signing.
|
// 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()
|
return signingPubKey.empty()
|
||||||
? checkMultiSign(requireCanonicalSig, rules)
|
? checkMultiSign(requireCanonicalSig, rules, pSig)
|
||||||
: checkSingleSign(requireCanonicalSig);
|
: checkSingleSign(requireCanonicalSig, pSig);
|
||||||
}
|
}
|
||||||
catch (std::exception const&)
|
catch (std::exception const&)
|
||||||
{
|
{
|
||||||
@@ -262,6 +265,24 @@ STTx::checkSign(
|
|||||||
return Unexpected("Internal signature check failure.");
|
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
|
Json::Value
|
||||||
STTx::getJson(JsonOptions options) const
|
STTx::getJson(JsonOptions options) const
|
||||||
{
|
{
|
||||||
@@ -342,12 +363,16 @@ STTx::getMetaSQL(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Expected<void, std::string>
|
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.
|
// 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
|
// That would allow the transaction to be signed two ways. So if both
|
||||||
// fields are present the signature is invalid.
|
// fields are present the signature is invalid.
|
||||||
if (isFieldPresent(sfSigners))
|
if (sigObject.isFieldPresent(sfSigners))
|
||||||
return Unexpected("Cannot both single- and multi-sign.");
|
return Unexpected("Cannot both single- and multi-sign.");
|
||||||
|
|
||||||
bool validSig = false;
|
bool validSig = false;
|
||||||
@@ -356,11 +381,11 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
|
|||||||
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
|
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
|
||||||
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
|
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
|
||||||
|
|
||||||
auto const spk = getFieldVL(sfSigningPubKey);
|
auto const spk = sigObject.getFieldVL(sfSigningPubKey);
|
||||||
|
|
||||||
if (publicKeyType(makeSlice(spk)))
|
if (publicKeyType(makeSlice(spk)))
|
||||||
{
|
{
|
||||||
Blob const signature = getFieldVL(sfTxnSignature);
|
Blob const signature = sigObject.getFieldVL(sfTxnSignature);
|
||||||
Blob const data = getSigningData(*this);
|
Blob const data = getSigningData(*this);
|
||||||
|
|
||||||
validSig = verify(
|
validSig = verify(
|
||||||
@@ -384,19 +409,22 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
|
|||||||
Expected<void, std::string>
|
Expected<void, std::string>
|
||||||
STTx::checkMultiSign(
|
STTx::checkMultiSign(
|
||||||
RequireFullyCanonicalSig requireCanonicalSig,
|
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
|
// Make sure the MultiSigners are present. Otherwise they are not
|
||||||
// attempting multi-signing and we just have a bad SigningPubKey.
|
// attempting multi-signing and we just have a bad SigningPubKey.
|
||||||
if (!isFieldPresent(sfSigners))
|
if (!sigObject.isFieldPresent(sfSigners))
|
||||||
return Unexpected("Empty SigningPubKey.");
|
return Unexpected("Empty SigningPubKey.");
|
||||||
|
|
||||||
// We don't allow both an sfSigners and an sfTxnSignature. Both fields
|
// We don't allow both an sfSigners and an sfTxnSignature. Both fields
|
||||||
// being present would indicate that the transaction is signed both ways.
|
// 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.");
|
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.
|
// There are well known bounds that the number of signers must be within.
|
||||||
if (signers.size() < minMultiSigners ||
|
if (signers.size() < minMultiSigners ||
|
||||||
@@ -422,8 +450,8 @@ STTx::checkMultiSign(
|
|||||||
{
|
{
|
||||||
auto const accountID = signer.getAccountID(sfAccount);
|
auto const accountID = signer.getAccountID(sfAccount);
|
||||||
|
|
||||||
// The account owner may not multisign for themselves.
|
// The account owner may not usually multisign for themselves.
|
||||||
if (accountID == txnAccountID)
|
if (!pSig && accountID == txnAccountID)
|
||||||
return Unexpected("Invalid multisigner.");
|
return Unexpected("Invalid multisigner.");
|
||||||
|
|
||||||
// No duplicate signers allowed.
|
// No duplicate signers allowed.
|
||||||
|
|||||||
594
src/test/app/LoanBroker_test.cpp
Normal file
594
src/test/app/LoanBroker_test.cpp
Normal 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
1702
src/test/app/Loan_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,11 @@ struct JTx
|
|||||||
bool fill_sig = true;
|
bool fill_sig = true;
|
||||||
bool fill_netid = true;
|
bool fill_netid = true;
|
||||||
std::shared_ptr<STTx const> stx;
|
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() = default;
|
||||||
JTx(JTx const&) = default;
|
JTx(JTx const&) = default;
|
||||||
|
|||||||
@@ -656,6 +656,114 @@ create(
|
|||||||
|
|
||||||
} // namespace check
|
} // 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 jtx
|
||||||
} // namespace test
|
} // namespace test
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
#include <xrpl/basics/Slice.h>
|
#include <xrpl/basics/Slice.h>
|
||||||
#include <xrpl/basics/contract.h>
|
#include <xrpl/basics/contract.h>
|
||||||
|
#include <xrpl/basics/scope.h>
|
||||||
#include <xrpl/json/to_string.h>
|
#include <xrpl/json/to_string.h>
|
||||||
#include <xrpl/protocol/ErrorCodes.h>
|
#include <xrpl/protocol/ErrorCodes.h>
|
||||||
#include <xrpl/protocol/Indexes.h>
|
#include <xrpl/protocol/Indexes.h>
|
||||||
@@ -481,8 +482,22 @@ void
|
|||||||
Env::autofill_sig(JTx& jt)
|
Env::autofill_sig(JTx& jt)
|
||||||
{
|
{
|
||||||
auto& jv = jt.jv;
|
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)
|
if (!jt.fill_sig)
|
||||||
return;
|
return;
|
||||||
auto const account = jv.isMember(sfDelegate.jsonName)
|
auto const account = jv.isMember(sfDelegate.jsonName)
|
||||||
|
|||||||
@@ -409,6 +409,142 @@ allpe(AccountID const& a, Issue const& iss)
|
|||||||
iss.account);
|
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 jtx
|
||||||
} // namespace test
|
} // namespace test
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -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.
|
// Signatures must be applied in sorted order.
|
||||||
std::sort(
|
std::sort(
|
||||||
@@ -80,8 +81,15 @@ void
|
|||||||
msig::operator()(Env& env, JTx& jt) const
|
msig::operator()(Env& env, JTx& jt) const
|
||||||
{
|
{
|
||||||
auto const mySigners = signers;
|
auto const mySigners = signers;
|
||||||
jt.signer = [mySigners, &env](Env&, JTx& jtx) {
|
auto callback = [subField = subField, mySigners, &env](Env&, JTx& jtx) {
|
||||||
jtx[sfSigningPubKey.getJsonName()] = "";
|
// 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;
|
std::optional<STObject> st;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -92,7 +100,7 @@ msig::operator()(Env& env, JTx& jt) const
|
|||||||
env.test.log << pretty(jtx.jv) << std::endl;
|
env.test.log << pretty(jtx.jv) << std::endl;
|
||||||
Rethrow();
|
Rethrow();
|
||||||
}
|
}
|
||||||
auto& js = jtx[sfSigners.getJsonName()];
|
auto& js = sigObject[sfSigners];
|
||||||
for (std::size_t i = 0; i < mySigners.size(); ++i)
|
for (std::size_t i = 0; i < mySigners.size(); ++i)
|
||||||
{
|
{
|
||||||
auto const& e = mySigners[i];
|
auto const& e = mySigners[i];
|
||||||
@@ -107,6 +115,10 @@ msig::operator()(Env& env, JTx& jt) const
|
|||||||
strHex(Slice{sig.data(), sig.size()});
|
strHex(Slice{sig.data(), sig.size()});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (!subField)
|
||||||
|
jt.mainSigners.emplace_back(callback);
|
||||||
|
else
|
||||||
|
jt.postSigners.emplace_back(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace jtx
|
} // namespace jtx
|
||||||
|
|||||||
@@ -29,12 +29,22 @@ sig::operator()(Env&, JTx& jt) const
|
|||||||
{
|
{
|
||||||
if (!manual_)
|
if (!manual_)
|
||||||
return;
|
return;
|
||||||
jt.fill_sig = false;
|
if (!subField)
|
||||||
|
jt.fill_sig = false;
|
||||||
if (account_)
|
if (account_)
|
||||||
{
|
{
|
||||||
// VFALCO Inefficient pre-C++14
|
// VFALCO Inefficient pre-C++14
|
||||||
auto const account = *account_;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,14 +44,20 @@ parse(Json::Value const& jv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
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;
|
Serializer ss;
|
||||||
ss.add32(HashPrefix::txSign);
|
ss.add32(HashPrefix::txSign);
|
||||||
parse(jv).addWithoutSigningFields(ss);
|
parse(jv).addWithoutSigningFields(ss);
|
||||||
auto const sig = ripple::sign(account.pk(), account.sk(), ss.slice());
|
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
|
void
|
||||||
|
|||||||
@@ -96,16 +96,53 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<Reg> signers;
|
std::vector<Reg> signers;
|
||||||
|
SField const* const subField = nullptr;
|
||||||
|
static constexpr SField* const topLevel = nullptr;
|
||||||
|
|
||||||
public:
|
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>
|
template <class AccountType, class... Accounts>
|
||||||
requires std::convertible_to<AccountType, Reg>
|
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)
|
explicit msig(AccountType&& a0, Accounts&&... aN)
|
||||||
: msig{std::vector<Reg>{
|
: msig{
|
||||||
std::forward<AccountType>(a0),
|
topLevel,
|
||||||
std::forward<Accounts>(aN)...}}
|
std::vector<Reg>{
|
||||||
|
std::forward<AccountType>(a0),
|
||||||
|
std::forward<Accounts>(aN)...}}
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ class sig
|
|||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
bool manual_ = true;
|
bool manual_ = true;
|
||||||
|
/// subField only supported with explicit account
|
||||||
|
SField const* const subField = nullptr;
|
||||||
std::optional<Account> account_;
|
std::optional<Account> account_;
|
||||||
|
static constexpr SField* const topLevel = nullptr;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit sig(autofill_t) : manual_(false)
|
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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ struct parse_error : std::logic_error
|
|||||||
STObject
|
STObject
|
||||||
parse(Json::Value const& jv);
|
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.
|
/** Sign automatically.
|
||||||
@note This only works on accounts with multi-signing off.
|
@note This only works on accounts with multi-signing off.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -159,9 +159,10 @@ class Invariants_test : public beast::unit_test::suite
|
|||||||
{{"an account root was deleted"}},
|
{{"an account root was deleted"}},
|
||||||
[](Account const& A1, Account const&, ApplyContext& ac) {
|
[](Account const& A1, Account const&, ApplyContext& ac) {
|
||||||
// remove an account from the view
|
// 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)
|
if (!sle)
|
||||||
return false;
|
return false;
|
||||||
|
sle->at(sfBalance) = beast::zero;
|
||||||
ac.view().erase(sle);
|
ac.view().erase(sle);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -185,10 +186,12 @@ class Invariants_test : public beast::unit_test::suite
|
|||||||
{{"account deletion succeeded but deleted multiple accounts"}},
|
{{"account deletion succeeded but deleted multiple accounts"}},
|
||||||
[](Account const& A1, Account const& A2, ApplyContext& ac) {
|
[](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||||
// remove two accounts from the view
|
// remove two accounts from the view
|
||||||
auto const sleA1 = ac.view().peek(keylet::account(A1.id()));
|
auto sleA1 = ac.view().peek(keylet::account(A1.id()));
|
||||||
auto const sleA2 = ac.view().peek(keylet::account(A2.id()));
|
auto sleA2 = ac.view().peek(keylet::account(A2.id()));
|
||||||
if (!sleA1 || !sleA2)
|
if (!sleA1 || !sleA2)
|
||||||
return false;
|
return false;
|
||||||
|
sleA1->at(sfBalance) = beast::zero;
|
||||||
|
sleA2->at(sfBalance) = beast::zero;
|
||||||
ac.view().erase(sleA1);
|
ac.view().erase(sleA1);
|
||||||
ac.view().erase(sleA2);
|
ac.view().erase(sleA2);
|
||||||
return true;
|
return true;
|
||||||
@@ -203,6 +206,43 @@ class Invariants_test : public beast::unit_test::suite
|
|||||||
using namespace test::jtx;
|
using namespace test::jtx;
|
||||||
testcase << "account root deletion left artifact";
|
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)
|
for (auto const& keyletInfo : directAccountKeylets)
|
||||||
{
|
{
|
||||||
// TODO: Use structured binding once LLVM 16 is the minimum
|
// 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
|
// Add an object to the ledger for account A1, then delete
|
||||||
// A1
|
// A1
|
||||||
auto const a1 = A1.id();
|
auto const a1 = A1.id();
|
||||||
auto const sleA1 = ac.view().peek(keylet::account(a1));
|
auto sleA1 = ac.view().peek(keylet::account(a1));
|
||||||
if (!sleA1)
|
if (!sleA1)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
auto const key = std::invoke(keyletfunc, a1);
|
auto const key = std::invoke(keyletfunc, a1);
|
||||||
auto const newSLE = std::make_shared<SLE>(key);
|
auto const newSLE = std::make_shared<SLE>(key);
|
||||||
ac.view().insert(newSLE);
|
ac.view().insert(newSLE);
|
||||||
|
sleA1->at(sfBalance) = beast::zero;
|
||||||
ac.view().erase(sleA1);
|
ac.view().erase(sleA1);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
XRPAmount{},
|
XRPAmount{},
|
||||||
STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
|
STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
|
||||||
};
|
}
|
||||||
|
|
||||||
// NFT special case
|
// NFT special case
|
||||||
doInvariantCheck(
|
doInvariantCheck(
|
||||||
{{"account deletion left behind a NFTokenPage object"}},
|
{{"account deletion left behind a NFTokenPage object"}},
|
||||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||||
// remove an account from the view
|
// 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)
|
if (!sle)
|
||||||
return false;
|
return false;
|
||||||
|
sle->at(sfBalance) = beast::zero;
|
||||||
|
sle->at(sfOwnerCount) = 0;
|
||||||
ac.view().erase(sle);
|
ac.view().erase(sle);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -269,13 +312,15 @@ class Invariants_test : public beast::unit_test::suite
|
|||||||
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
|
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||||
// Delete the AMM account without cleaning up the directory or
|
// Delete the AMM account without cleaning up the directory or
|
||||||
// deleting the AMM object
|
// deleting the AMM object
|
||||||
auto const sle = ac.view().peek(keylet::account(ammAcctID));
|
auto sle = ac.view().peek(keylet::account(ammAcctID));
|
||||||
if (!sle)
|
if (!sle)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
BEAST_EXPECT(sle->at(~sfAMMID));
|
BEAST_EXPECT(sle->at(~sfAMMID));
|
||||||
BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
|
BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
|
||||||
|
|
||||||
|
sle->at(sfBalance) = beast::zero;
|
||||||
|
sle->at(sfOwnerCount) = 0;
|
||||||
ac.view().erase(sle);
|
ac.view().erase(sle);
|
||||||
|
|
||||||
return true;
|
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
|
// Delete all the AMM's trust lines, remove the AMM from the AMM
|
||||||
// account's directory (this deletes the directory), and delete
|
// account's directory (this deletes the directory), and delete
|
||||||
// the AMM account. Do not delete the AMM object.
|
// 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)
|
if (!sle)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -338,6 +383,8 @@ class Invariants_test : public beast::unit_test::suite
|
|||||||
!ac.view().exists(ownerDirKeylet) ||
|
!ac.view().exists(ownerDirKeylet) ||
|
||||||
ac.view().emptyDirDelete(ownerDirKeylet));
|
ac.view().emptyDirDelete(ownerDirKeylet));
|
||||||
|
|
||||||
|
sle->at(sfBalance) = beast::zero;
|
||||||
|
sle->at(sfOwnerCount) = 0;
|
||||||
ac.view().erase(sle);
|
ac.view().erase(sle);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -1301,6 +1348,121 @@ class Invariants_test : public beast::unit_test::suite
|
|||||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
{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
|
void
|
||||||
testValidPseudoAccounts()
|
testValidPseudoAccounts()
|
||||||
{
|
{
|
||||||
@@ -1354,7 +1516,6 @@ class Invariants_test : public beast::unit_test::suite
|
|||||||
sle->at(~sfAMMID) = ~sle->at(~sfVaultID);
|
sle->at(~sfAMMID) = ~sle->at(~sfVaultID);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/*
|
|
||||||
{
|
{
|
||||||
"pseudo-account has 2 pseudo-account fields set",
|
"pseudo-account has 2 pseudo-account fields set",
|
||||||
[this](SLE::pointer& sle) {
|
[this](SLE::pointer& sle) {
|
||||||
@@ -1363,7 +1524,6 @@ class Invariants_test : public beast::unit_test::suite
|
|||||||
sle->at(~sfLoanBrokerID) = ~sle->at(~sfVaultID);
|
sle->at(~sfLoanBrokerID) = ~sle->at(~sfVaultID);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
*/
|
|
||||||
{
|
{
|
||||||
"pseudo-account sequence changed",
|
"pseudo-account sequence changed",
|
||||||
[](SLE::pointer& sle) { sle->at(sfSequence) = 12345; },
|
[](SLE::pointer& sle) { sle->at(sfSequence) = 12345; },
|
||||||
@@ -1413,6 +1573,245 @@ class Invariants_test : public beast::unit_test::suite
|
|||||||
return true;
|
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:
|
public:
|
||||||
void
|
void
|
||||||
run() override
|
run() override
|
||||||
@@ -1431,7 +1830,9 @@ public:
|
|||||||
testValidNewAccountRoot();
|
testValidNewAccountRoot();
|
||||||
testNFTokenPageInvariants();
|
testNFTokenPageInvariants();
|
||||||
testPermissionedDomainInvariants();
|
testPermissionedDomainInvariants();
|
||||||
|
testNoModifiedUnmodifiableFields();
|
||||||
testValidPseudoAccounts();
|
testValidPseudoAccounts();
|
||||||
|
testValidLoanBroker();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
618
src/xrpld/app/misc/LendingHelpers.h
Normal file
618
src/xrpld/app/misc/LendingHelpers.h
Normal 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
|
||||||
111
src/xrpld/app/misc/detail/LendingHelpers.cpp
Normal file
111
src/xrpld/app/misc/detail/LendingHelpers.cpp
Normal 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
|
||||||
@@ -38,7 +38,11 @@ NotTEC
|
|||||||
Transactor::preflight<Change>(PreflightContext const& ctx)
|
Transactor::preflight<Change>(PreflightContext const& ctx)
|
||||||
{
|
{
|
||||||
// 0 means "Allow any flags"
|
// 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;
|
return ret;
|
||||||
|
|
||||||
auto account = ctx.tx.getAccountID(sfAccount);
|
auto account = ctx.tx.getAccountID(sfAccount);
|
||||||
|
|||||||
@@ -415,10 +415,10 @@ void
|
|||||||
AccountRootsDeletedClean::visitEntry(
|
AccountRootsDeletedClean::visitEntry(
|
||||||
bool isDelete,
|
bool isDelete,
|
||||||
std::shared_ptr<SLE const> const& before,
|
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)
|
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
|
||||||
accountsDeleted_.emplace_back(before);
|
accountsDeleted_.emplace_back(before, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@@ -434,7 +434,8 @@ AccountRootsDeletedClean::finalize(
|
|||||||
// feature is enabled. Enabled, or not, though, a fatal-level message will
|
// feature is enabled. Enabled, or not, though, a fatal-level message will
|
||||||
// be logged
|
// be logged
|
||||||
[[maybe_unused]] bool const enforce =
|
[[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) {
|
auto const objectExists = [&view, enforce, &j](auto const& keylet) {
|
||||||
(void)enforce;
|
(void)enforce;
|
||||||
@@ -462,9 +463,33 @@ AccountRootsDeletedClean::finalize(
|
|||||||
return false;
|
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
|
// Simple types
|
||||||
for (auto const& [keyletfunc, _, __] : directAccountKeylets)
|
for (auto const& [keyletfunc, _, __] : directAccountKeylets)
|
||||||
{
|
{
|
||||||
@@ -490,9 +515,9 @@ AccountRootsDeletedClean::finalize(
|
|||||||
// Keys directly stored in the AccountRoot object
|
// Keys directly stored in the AccountRoot object
|
||||||
for (auto const& field : getPseudoAccountFields())
|
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)
|
if (objectExists(keylet::unchecked(key)) && enforce)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -949,7 +974,9 @@ ValidNewAccountRoot::finalize(
|
|||||||
result == tesSUCCESS)
|
result == tesSUCCESS)
|
||||||
{
|
{
|
||||||
bool const pseudoAccount =
|
bool const pseudoAccount =
|
||||||
(pseudoAccount_ && view.rules().enabled(featureSingleAssetVault));
|
(pseudoAccount_ &&
|
||||||
|
(view.rules().enabled(featureSingleAssetVault) ||
|
||||||
|
view.rules().enabled(featureLendingProtocol)));
|
||||||
|
|
||||||
if (pseudoAccount && !checkMyPrivilege(tx, createPseudoAcct))
|
if (pseudoAccount && !checkMyPrivilege(tx, createPseudoAcct))
|
||||||
{
|
{
|
||||||
@@ -1433,6 +1460,12 @@ ValidMPTIssuance::finalize(
|
|||||||
"succeeded but deleted issuances";
|
"succeeded but deleted issuances";
|
||||||
return false;
|
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 (
|
else if (
|
||||||
submittedByIssuer &&
|
submittedByIssuer &&
|
||||||
(mptokensCreated_ > 0 || mptokensDeleted_ > 0))
|
(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
|
void
|
||||||
ValidPseudoAccounts::visitEntry(
|
ValidPseudoAccounts::visitEntry(
|
||||||
bool isDelete,
|
bool isDelete,
|
||||||
@@ -1701,4 +1850,170 @@ ValidPseudoAccounts::finalize(
|
|||||||
return true;
|
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
|
} // namespace ripple
|
||||||
|
|||||||
@@ -174,7 +174,14 @@ public:
|
|||||||
*/
|
*/
|
||||||
class AccountRootsDeletedClean
|
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:
|
public:
|
||||||
void
|
void
|
||||||
@@ -618,6 +625,34 @@ public:
|
|||||||
beast::Journal const&);
|
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
|
* @brief Invariants: Pseudo-accounts have valid and consisent properties
|
||||||
*
|
*
|
||||||
@@ -646,6 +681,70 @@ public:
|
|||||||
beast::Journal const&);
|
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
|
// additional invariant checks can be declared above and then added to this
|
||||||
// tuple
|
// tuple
|
||||||
using InvariantChecks = std::tuple<
|
using InvariantChecks = std::tuple<
|
||||||
@@ -666,7 +765,10 @@ using InvariantChecks = std::tuple<
|
|||||||
ValidClawback,
|
ValidClawback,
|
||||||
ValidMPTIssuance,
|
ValidMPTIssuance,
|
||||||
ValidPermissionedDomain,
|
ValidPermissionedDomain,
|
||||||
ValidPseudoAccounts>;
|
NoModifiedUnmodifiableFields,
|
||||||
|
ValidPseudoAccounts,
|
||||||
|
ValidLoanBroker,
|
||||||
|
ValidLoan>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief get a tuple of all invariant checks
|
* @brief get a tuple of all invariant checks
|
||||||
|
|||||||
147
src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp
Normal file
147
src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp
Normal 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
|
||||||
53
src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h
Normal file
53
src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h
Normal 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
|
||||||
164
src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp
Normal file
164
src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp
Normal 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
|
||||||
53
src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h
Normal file
53
src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h
Normal 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
|
||||||
179
src/xrpld/app/tx/detail/LoanBrokerDelete.cpp
Normal file
179
src/xrpld/app/tx/detail/LoanBrokerDelete.cpp
Normal 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
|
||||||
53
src/xrpld/app/tx/detail/LoanBrokerDelete.h
Normal file
53
src/xrpld/app/tx/detail/LoanBrokerDelete.h
Normal 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
|
||||||
210
src/xrpld/app/tx/detail/LoanBrokerSet.cpp
Normal file
210
src/xrpld/app/tx/detail/LoanBrokerSet.cpp
Normal 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
|
||||||
53
src/xrpld/app/tx/detail/LoanBrokerSet.h
Normal file
53
src/xrpld/app/tx/detail/LoanBrokerSet.h
Normal 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
|
||||||
167
src/xrpld/app/tx/detail/LoanDelete.cpp
Normal file
167
src/xrpld/app/tx/detail/LoanDelete.cpp
Normal 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
|
||||||
53
src/xrpld/app/tx/detail/LoanDelete.h
Normal file
53
src/xrpld/app/tx/detail/LoanDelete.h
Normal 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
|
||||||
194
src/xrpld/app/tx/detail/LoanDraw.cpp
Normal file
194
src/xrpld/app/tx/detail/LoanDraw.cpp
Normal 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
|
||||||
53
src/xrpld/app/tx/detail/LoanDraw.h
Normal file
53
src/xrpld/app/tx/detail/LoanDraw.h
Normal 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
|
||||||
442
src/xrpld/app/tx/detail/LoanManage.cpp
Normal file
442
src/xrpld/app/tx/detail/LoanManage.cpp
Normal 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
|
||||||
56
src/xrpld/app/tx/detail/LoanManage.h
Normal file
56
src/xrpld/app/tx/detail/LoanManage.h
Normal 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
|
||||||
297
src/xrpld/app/tx/detail/LoanPay.cpp
Normal file
297
src/xrpld/app/tx/detail/LoanPay.cpp
Normal 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
|
||||||
53
src/xrpld/app/tx/detail/LoanPay.h
Normal file
53
src/xrpld/app/tx/detail/LoanPay.h
Normal 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
|
||||||
418
src/xrpld/app/tx/detail/LoanSet.cpp
Normal file
418
src/xrpld/app/tx/detail/LoanSet.cpp
Normal 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
|
||||||
74
src/xrpld/app/tx/detail/LoanSet.h
Normal file
74
src/xrpld/app/tx/detail/LoanSet.h
Normal 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
|
||||||
@@ -295,7 +295,9 @@ SetTrust::preclaim(PreclaimContext const& ctx)
|
|||||||
else
|
else
|
||||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
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)))
|
if (!ctx.view.exists(keylet::line(id, uDstAccountID, currency)))
|
||||||
return tecNO_PERMISSION;
|
return tecNO_PERMISSION;
|
||||||
|
|||||||
@@ -627,6 +627,16 @@ Transactor::checkSign(
|
|||||||
AccountID const& id,
|
AccountID const& id,
|
||||||
STObject const& sigObject)
|
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)
|
if (ctx.flags & tapDRY_RUN)
|
||||||
{
|
{
|
||||||
// This code must be different for `simulate`
|
// This code must be different for `simulate`
|
||||||
|
|||||||
@@ -383,6 +383,45 @@ public:
|
|||||||
emptyDirDelete(Keylet const& directory);
|
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
|
} // namespace ripple
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -504,7 +504,11 @@ dirNext(
|
|||||||
describeOwnerDir(AccountID const& account);
|
describeOwnerDir(AccountID const& account);
|
||||||
|
|
||||||
[[nodiscard]] TER
|
[[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
|
AccountID
|
||||||
pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey);
|
pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey);
|
||||||
|
|||||||
@@ -25,6 +25,128 @@
|
|||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
|
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 newRoot = std::make_shared<SLE>(directory);
|
||||||
|
newRoot->setFieldH256(sfRootIndex, directory.key);
|
||||||
|
describe(newRoot);
|
||||||
|
|
||||||
|
STVector256 v;
|
||||||
|
v.push_back(key);
|
||||||
|
newRoot->setFieldV256(sfIndexes, v);
|
||||||
|
|
||||||
|
view.insert(newRoot);
|
||||||
|
return std::uint64_t{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto
|
||||||
|
findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start)
|
||||||
|
{
|
||||||
|
std::uint64_t page = start->getFieldU64(sfIndexPrevious);
|
||||||
|
|
||||||
|
auto node = start;
|
||||||
|
|
||||||
|
if (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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
LogicError("dirInsert: double insertion");
|
||||||
|
|
||||||
|
indexes.push_back(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We can't be sure if this page is already sorted because
|
||||||
|
// it may be a legacy page we haven't yet touched. Take
|
||||||
|
// the time to sort it.
|
||||||
|
std::sort(indexes.begin(), indexes.end());
|
||||||
|
|
||||||
|
auto pos = std::lower_bound(indexes.begin(), indexes.end(), key);
|
||||||
|
|
||||||
|
if (pos != indexes.end() && key == *pos)
|
||||||
|
LogicError("dirInsert: double insertion");
|
||||||
|
|
||||||
|
indexes.insert(pos, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
node->setFieldV256(sfIndexes, indexes);
|
||||||
|
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);
|
||||||
|
view.update(node);
|
||||||
|
|
||||||
|
next->setFieldU64(sfIndexPrevious, page);
|
||||||
|
view.update(next);
|
||||||
|
|
||||||
|
// Insert the new key:
|
||||||
|
STVector256 indexes;
|
||||||
|
indexes.push_back(key);
|
||||||
|
|
||||||
|
node = std::make_shared<SLE>(keylet::page(directory, page));
|
||||||
|
node->setFieldH256(sfRootIndex, directory.key);
|
||||||
|
node->setFieldV256(sfIndexes, indexes);
|
||||||
|
|
||||||
|
// Save some space by not specifying the value 0 since
|
||||||
|
// it's the default.
|
||||||
|
if (page != 1)
|
||||||
|
node->setFieldU64(sfIndexPrevious, page - 1);
|
||||||
|
if (nextPage)
|
||||||
|
node->setFieldU64(sfIndexNext, nextPage);
|
||||||
|
describe(node);
|
||||||
|
view.insert(node);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace directory
|
||||||
|
|
||||||
std::optional<std::uint64_t>
|
std::optional<std::uint64_t>
|
||||||
ApplyView::dirAdd(
|
ApplyView::dirAdd(
|
||||||
bool preserveOrder,
|
bool preserveOrder,
|
||||||
@@ -37,89 +159,21 @@ ApplyView::dirAdd(
|
|||||||
if (!root)
|
if (!root)
|
||||||
{
|
{
|
||||||
// No root, make it.
|
// No root, make it.
|
||||||
root = std::make_shared<SLE>(directory);
|
return directory::createRoot(*this, directory, key, describe);
|
||||||
root->setFieldH256(sfRootIndex, directory.key);
|
|
||||||
describe(root);
|
|
||||||
|
|
||||||
STVector256 v;
|
|
||||||
v.push_back(key);
|
|
||||||
root->setFieldV256(sfIndexes, v);
|
|
||||||
|
|
||||||
insert(root);
|
|
||||||
return std::uint64_t{0};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint64_t page = root->getFieldU64(sfIndexPrevious);
|
auto [page, node, indexes] =
|
||||||
|
directory::findPreviousPage(*this, directory, root);
|
||||||
auto node = root;
|
|
||||||
|
|
||||||
if (page)
|
|
||||||
{
|
|
||||||
node = peek(keylet::page(directory, page));
|
|
||||||
if (!node)
|
|
||||||
LogicError("Directory chain: root back-pointer broken.");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto indexes = node->getFieldV256(sfIndexes);
|
|
||||||
|
|
||||||
// If there's space, we use it:
|
// If there's space, we use it:
|
||||||
if (indexes.size() < dirNodeMaxEntries)
|
if (indexes.size() < dirNodeMaxEntries)
|
||||||
{
|
{
|
||||||
if (preserveOrder)
|
return directory::insertKey(
|
||||||
{
|
*this, node, page, preserveOrder, indexes, key);
|
||||||
if (std::find(indexes.begin(), indexes.end(), key) != indexes.end())
|
|
||||||
LogicError("dirInsert: double insertion");
|
|
||||||
|
|
||||||
indexes.push_back(key);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We can't be sure if this page is already sorted because
|
|
||||||
// it may be a legacy page we haven't yet touched. Take
|
|
||||||
// the time to sort it.
|
|
||||||
std::sort(indexes.begin(), indexes.end());
|
|
||||||
|
|
||||||
auto pos = std::lower_bound(indexes.begin(), indexes.end(), key);
|
|
||||||
|
|
||||||
if (pos != indexes.end() && key == *pos)
|
|
||||||
LogicError("dirInsert: double insertion");
|
|
||||||
|
|
||||||
indexes.insert(pos, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
node->setFieldV256(sfIndexes, indexes);
|
|
||||||
update(node);
|
|
||||||
return page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether we're out of pages.
|
return directory::insertPage(
|
||||||
if (++page >= dirNodeMaxPages)
|
*this, page, node, 0, root, key, directory, describe);
|
||||||
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);
|
|
||||||
|
|
||||||
root->setFieldU64(sfIndexPrevious, page);
|
|
||||||
update(root);
|
|
||||||
|
|
||||||
// Insert the new key:
|
|
||||||
indexes.clear();
|
|
||||||
indexes.push_back(key);
|
|
||||||
|
|
||||||
node = std::make_shared<SLE>(keylet::page(directory, page));
|
|
||||||
node->setFieldH256(sfRootIndex, directory.key);
|
|
||||||
node->setFieldV256(sfIndexes, indexes);
|
|
||||||
|
|
||||||
// Save some space by not specifying the value 0 since
|
|
||||||
// it's the default.
|
|
||||||
if (page != 1)
|
|
||||||
node->setFieldU64(sfIndexPrevious, page - 1);
|
|
||||||
describe(node);
|
|
||||||
insert(node);
|
|
||||||
|
|
||||||
return page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
|||||||
@@ -1042,13 +1042,17 @@ describeOwnerDir(AccountID const& account)
|
|||||||
}
|
}
|
||||||
|
|
||||||
TER
|
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(
|
auto const page = view.dirInsert(
|
||||||
keylet::ownerDir(owner), object->key(), describeOwnerDir(owner));
|
keylet::ownerDir(owner), object->key(), describeOwnerDir(owner));
|
||||||
if (!page)
|
if (!page)
|
||||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||||
object->setFieldU64(sfOwnerNode, *page);
|
object->setFieldU64(node, *page);
|
||||||
return tesSUCCESS;
|
return tesSUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1139,9 +1143,10 @@ createPseudoAccount(
|
|||||||
// Pseudo-accounts can't submit transactions, so set the sequence number
|
// Pseudo-accounts can't submit transactions, so set the sequence number
|
||||||
// to 0 to make them easier to spot and verify, and add an extra level
|
// to 0 to make them easier to spot and verify, and add an extra level
|
||||||
// of protection.
|
// of protection.
|
||||||
std::uint32_t const seqno = //
|
std::uint32_t const seqno = //
|
||||||
view.rules().enabled(featureSingleAssetVault) //
|
view.rules().enabled(featureSingleAssetVault) || //
|
||||||
? 0 //
|
view.rules().enabled(featureLendingProtocol) //
|
||||||
|
? 0 //
|
||||||
: view.seq();
|
: view.seq();
|
||||||
account->setFieldU32(sfSequence, seqno);
|
account->setFieldU32(sfSequence, seqno);
|
||||||
// Ignore reserves requirement, disable the master key, allow default
|
// Ignore reserves requirement, disable the master key, allow default
|
||||||
|
|||||||
Reference in New Issue
Block a user