diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index e419940171e..88057a63005 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -20,10 +20,12 @@ #ifndef RIPPLE_JSON_JSON_VALUE_H_INCLUDED #define RIPPLE_JSON_JSON_VALUE_H_INCLUDED +#include #include #include #include #include +#include #include /** \brief JSON (JavaScript Object Notation). @@ -215,6 +217,7 @@ class Value Value(UInt value); Value(double value); Value(const char* value); + Value(ripple::Number const& value); /** \brief Constructs a value from a static string. * Like other value string constructor but do not duplicate the string for @@ -364,6 +367,8 @@ class Value */ Value& operator[](const StaticString& key); + Value const& + operator[](const StaticString& key) const; /// Return the member named key if it exist, defaultValue otherwise. Value @@ -435,6 +440,12 @@ class Value int allocated_ : 1; // Notes: if declared as bool, bitfield is useless. }; +inline Value +to_json(ripple::Number const& number) +{ + return to_string(number); +} + bool operator==(const Value&, const Value&); diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index 32988af5fc7..442f24d8785 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -48,14 +48,6 @@ class STObject; class STAmount; class Rules; -/** Calculate AMM account ID. - */ -AccountID -ammAccountID( - std::uint16_t prefix, - uint256 const& parentHash, - uint256 const& ammID); - /** Calculate Liquidity Provider Token (LPT) Currency. */ Currency diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 0d12cd40580..44381067389 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_PROTOCOL_ASSET_H_INCLUDED #define RIPPLE_PROTOCOL_ASSET_H_INCLUDED +#include #include #include #include @@ -27,6 +28,7 @@ namespace ripple { class Asset; +class STAmount; template concept ValidIssueType = @@ -92,6 +94,9 @@ class Asset void setJson(Json::Value& jv) const; + STAmount + operator()(Number const&) const; + bool native() const { @@ -114,6 +119,14 @@ class Asset equalTokens(Asset const& lhs, Asset const& rhs); }; +inline Json::Value +to_json(Asset const& asset) +{ + Json::Value jv; + asset.setJson(jv); + return jv; +} + template constexpr bool Asset::holds() const @@ -219,9 +232,6 @@ validJSONAsset(Json::Value const& jv); Asset assetFromJson(Json::Value const& jv); -Json::Value -to_json(Asset const& asset); - } // namespace ripple #endif // RIPPLE_PROTOCOL_ASSET_H_INCLUDED diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 1c476df617f..bdec238651c 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -33,35 +33,35 @@ * * Steps required to add new features to the code: * - * 1) In this file, increment `numFeatures` and add a uint256 declaration - * for the feature at the bottom - * 2) Add a uint256 definition for the feature to the corresponding source - * file (Feature.cpp). Use `registerFeature` to create the feature with - * the feature's name, `Supported::no`, and `VoteBehavior::DefaultNo`. This - * should be the only place the feature's name appears in code as a string. - * 3) Use the uint256 as the parameter to `view.rules.enabled()` to - * control flow into new code that this feature limits. - * 4) If the feature development is COMPLETE, and the feature is ready to be - * SUPPORTED, change the `registerFeature` parameter to Supported::yes. - * 5) When the feature is ready to be ENABLED, change the `registerFeature` - * parameter to `VoteBehavior::DefaultYes`. + * 1) Add a the appropriate XRPL_FEATURE or XRPL_FIX macro definition for the + * feature to features.macro with the feature's name, `Supported::no`, and + * `VoteBehavior::DefaultNo`. + * + * 2) Use the generated variable name as the parameter to `view.rules.enabled()` + * to control flow into new code that this feature limits. (featureName or + * fixName) + * + * 3) If the feature development is COMPLETE, and the feature is ready to be + * SUPPORTED, change the macro parameter in features.macro to Supported::yes. + * + * 4) When the feature is ready to be ENABLED, change the macro parameter in + * features.macro parameter to `VoteBehavior::DefaultYes`. + * * In general, any newly supported amendments (`Supported::yes`) should have * a `VoteBehavior::DefaultNo` for at least one full release cycle. High * priority bug fixes can be an exception to this rule of thumb. * * When a feature has been enabled for several years, the conditional code * may be removed, and the feature "retired". To retire a feature: - * 1) Remove the uint256 declaration from this file. - * 2) MOVE the uint256 definition in Feature.cpp to the "retired features" - * section at the end of the file. - * 3) CHANGE the name of the variable to start with "retired". - * 4) CHANGE the parameters of the `registerFeature` call to `Supported::yes` - * and `VoteBehavior::DefaultNo`. + * + * 1) MOVE the macro definition in features.macro to the "retired features" + * section at the end of the file, and change the macro to XRPL_RETIRE. + * * The feature must remain registered and supported indefinitely because it * still exists in the ledger, but there is no need to vote for it because - * there's nothing to vote for. If it is removed completely from the code, any - * instances running that code will get amendment blocked. Removing the - * feature from the ledger is beyond the scope of these instructions. + * there's nothing to vote for. If the definition is removed completely from the + * code, any instances running that code will get amendment blocked. Removing + * the feature from the ledger is beyond the scope of these instructions. * */ @@ -80,7 +80,27 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 88; +static constexpr std::size_t numFeatures = 0 +#pragma push_macro("XRPL_FEATURE") +#undef XRPL_FEATURE +#pragma push_macro("XRPL_FIX") +#undef XRPL_FIX +#pragma push_macro("XRPL_RETIRE") +#undef XRPL_RETIRE + +#define XRPL_FEATURE(name, supported, vote) +1 +#define XRPL_FIX(name, supported, vote) +1 +#define XRPL_RETIRE(name) +1 + +#include + +#undef XRPL_RETIRE +#pragma pop_macro("XRPL_RETIRE") +#undef XRPL_FIX +#pragma pop_macro("XRPL_FIX") +#undef XRPL_FEATURE +#pragma pop_macro("XRPL_FEATURE") + ; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -320,12 +340,17 @@ foreachFeature(FeatureBitset bs, F&& f) #undef XRPL_FEATURE #pragma push_macro("XRPL_FIX") #undef XRPL_FIX +#pragma push_macro("XRPL_RETIRE") +#undef XRPL_RETIRE #define XRPL_FEATURE(name, supported, vote) extern uint256 const feature##name; #define XRPL_FIX(name, supported, vote) extern uint256 const fix##name; +#define XRPL_RETIRE(name) #include +#undef XRPL_RETIRE +#pragma pop_macro("XRPL_RETIRE") #undef XRPL_FIX #pragma pop_macro("XRPL_FIX") #undef XRPL_FEATURE diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index e89feb123d0..4a2e1e5681f 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -26,7 +26,6 @@ #include #include #include -#include namespace ripple { diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index bbed5395927..43e64dc27a1 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -330,6 +330,15 @@ mptoken(uint256 const& mptokenKey) Keylet mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept; +Keylet +vault(AccountID const& owner, std::uint32_t seq) noexcept; + +inline Keylet +vault(uint256 const& vaultKey) +{ + return {ltVAULT, vaultKey}; +} + Keylet permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept; diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 5f3cca53ac8..57d111a6e57 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -188,9 +188,13 @@ enum LedgerSpecificFlags { // ltMPTOKEN lsfMPTAuthorized = 0x00000002, + lsfMPTDomainCheck = 0x00000004, // ltCREDENTIAL lsfAccepted = 0x00010000, + + // ltVAULT + lsfVaultPrivate = 0x00010000, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 244d6839156..419450eeb9e 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -24,15 +24,12 @@ #include #include #include -#include #include #include #include -#include #include -#include namespace ripple { diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index 028051ab1ae..d1c757337eb 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -42,8 +42,11 @@ class MPTIssue AccountID const& getIssuer() const; - MPTID const& - getMptID() const; + constexpr MPTID const& + getMptID() const + { + return mptID_; + } std::string getText() const; diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 81a45c383fb..9294bd9d486 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -115,6 +115,12 @@ std::size_t constexpr maxMPTokenMetadataLength = 1024; /** The maximum amount of MPTokenIssuance */ std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull; +/** The maximum length of Data payload */ +std::size_t constexpr maxDataPayloadLength = 256; + +/** Vault withdrawal policies */ +std::uint8_t constexpr vaultStrategyFirstComeFirstServe = 1; + /** A ledger index. */ using LedgerIndex = std::uint32_t; diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 01909b19862..cac768ad595 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -25,7 +25,6 @@ #include #include -#include namespace ripple { diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 23e4c5e5b59..655801eb1e4 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -153,6 +153,12 @@ class STAmount final : public STBase, public CountedObject template STAmount(A const& asset, int mantissa, int exponent = 0); + template + STAmount(A const& asset, Number const& number) + : STAmount(asset, number.mantissa(), number.exponent()) + { + } + // Legacy support for new-style amounts STAmount(IOUAmount const& amount, Issue const& issue); STAmount(XRPAmount const& amount); @@ -230,6 +236,9 @@ class STAmount final : public STBase, public CountedObject STAmount& operator=(XRPAmount const& amount); + STAmount& + operator=(Number const&); + //-------------------------------------------------------------------------- // // Modification @@ -268,7 +277,7 @@ class STAmount final : public STBase, public CountedObject std::string getText() const override; - Json::Value getJson(JsonOptions) const override; + Json::Value getJson(JsonOptions = JsonOptions::none) const override; void add(Serializer& s) const override; @@ -417,7 +426,7 @@ STAmount amountFromQuality(std::uint64_t rate); STAmount -amountFromString(Asset const& issue, std::string const& amount); +amountFromString(Asset const& asset, std::string const& amount); STAmount amountFromJson(SField const& name, Json::Value const& v); @@ -541,6 +550,16 @@ STAmount::operator=(XRPAmount const& amount) return *this; } +inline STAmount& +STAmount::operator=(Number const& number) +{ + mIsNegative = number.mantissa() < 0; + mValue = mIsNegative ? -number.mantissa() : number.mantissa(); + mOffset = number.exponent(); + canonicalize(); + return *this; +} + inline void STAmount::negate() { diff --git a/include/xrpl/protocol/STBase.h b/include/xrpl/protocol/STBase.h index 097341384f3..20d47e44567 100644 --- a/include/xrpl/protocol/STBase.h +++ b/include/xrpl/protocol/STBase.h @@ -90,6 +90,14 @@ struct JsonOptions } }; +template + requires requires(T const& t) { t.getJson(JsonOptions::none); } +Json::Value +to_json(T const& t) +{ + return t.getJson(JsonOptions::none); +} + namespace detail { class STVar; } @@ -155,7 +163,7 @@ class STBase virtual std::string getText() const; - virtual Json::Value getJson(JsonOptions /*options*/) const; + virtual Json::Value getJson(JsonOptions = JsonOptions::none) const; virtual void add(Serializer& s) const; diff --git a/include/xrpl/protocol/STIssue.h b/include/xrpl/protocol/STIssue.h index 08812c15aec..51c14c9ce67 100644 --- a/include/xrpl/protocol/STIssue.h +++ b/include/xrpl/protocol/STIssue.h @@ -37,6 +37,7 @@ class STIssue final : public STBase, CountedObject using value_type = Asset; STIssue() = default; + STIssue(STIssue const& rhs) = default; explicit STIssue(SerialIter& sit, SField const& name); @@ -45,6 +46,15 @@ class STIssue final : public STBase, CountedObject explicit STIssue(SField const& name); + STIssue& + operator=(STIssue const& rhs) = default; + STIssue& + operator=(Asset const& rhs) + { + asset_ = rhs; + return *this; + } + template TIss const& get() const; diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h index c0fce572c8c..3c1f73e4e66 100644 --- a/include/xrpl/protocol/STNumber.h +++ b/include/xrpl/protocol/STNumber.h @@ -63,6 +63,13 @@ class STNumber : public STBase, public CountedObject void setValue(Number const& v); + STNumber& + operator=(Number const& rhs) + { + setValue(rhs); + return *this; + } + bool isEquivalent(STBase const& t) const override; bool @@ -83,6 +90,19 @@ class STNumber : public STBase, public CountedObject std::ostream& operator<<(std::ostream& out, STNumber const& rhs); +struct NumberParts +{ + std::uint64_t mantissa = 0; + int exponent = 0; + bool negative = false; +}; + +NumberParts +partsFromString(std::string const& number); + +STNumber +numberFromJson(SField const& field, Json::Value const& value); + } // namespace ripple #endif diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 4c8db2e01e4..075b3ed8ba6 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -152,8 +152,7 @@ class STObject : public STBase, public CountedObject getText() const override; // TODO(tom): options should be an enum. - Json::Value - getJson(JsonOptions options) const override; + Json::Value getJson(JsonOptions = JsonOptions::none) const override; void addWithoutSigningFields(Serializer& s) const; @@ -482,9 +481,19 @@ class STObject : public STBase, public CountedObject template class STObject::Proxy { -protected: +public: using value_type = typename T::value_type; + value_type + value() const; + + value_type + operator*() const; + + T const* + operator->() const; + +protected: STObject* st_; SOEStyle style_; TypedField const* f_; @@ -493,9 +502,6 @@ class STObject::Proxy Proxy(STObject* st, TypedField const* f); - value_type - value() const; - T const* find() const; @@ -510,7 +516,7 @@ template concept IsArithmetic = std::is_arithmetic_v || std::is_same_v; template -class STObject::ValueProxy : private Proxy +class STObject::ValueProxy : public Proxy { private: using value_type = typename T::value_type; @@ -536,6 +542,13 @@ class STObject::ValueProxy : private Proxy operator value_type() const; + template + friend bool + operator==(U const& lhs, STObject::ValueProxy const& rhs) + { + return rhs.value() == lhs; + } + private: friend class STObject; @@ -543,7 +556,7 @@ class STObject::ValueProxy : private Proxy }; template -class STObject::OptionalProxy : private Proxy +class STObject::OptionalProxy : public Proxy { private: using value_type = typename T::value_type; @@ -563,15 +576,6 @@ class STObject::OptionalProxy : private Proxy explicit operator bool() const noexcept; - /** Return the contained value - - Throws: - - STObject::FieldErr if !engaged() - */ - value_type - operator*() const; - operator optional_type() const; /** Explicit conversion to std::optional */ @@ -715,6 +719,20 @@ STObject::Proxy::value() const -> value_type return value_type{}; } +template +auto +STObject::Proxy::operator*() const -> value_type +{ + return this->value(); +} + +template +T const* +STObject::Proxy::operator->() const +{ + return this->find(); +} + template inline T const* STObject::Proxy::find() const @@ -790,13 +808,6 @@ STObject::OptionalProxy::operator bool() const noexcept return engaged(); } -template -auto -STObject::OptionalProxy::operator*() const -> value_type -{ - return this->value(); -} - template STObject::OptionalProxy::operator typename STObject::OptionalProxy< T>::optional_type() const diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index 08b9a1bad10..018067ed8d1 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -101,6 +101,10 @@ class STTx final : public STObject, public CountedObject SeqProxy getSeqProxy() const; + /** Returns the first non-zero value of (Sequence, TicketSequence). */ + std::uint32_t + getSeqValue() const; + boost::container::flat_set getMentionedAccounts() const; diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 317e9c2c978..0b8b4e618ca 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -344,6 +344,9 @@ enum TECcodes : TERUnderlyingType { tecARRAY_TOO_LARGE = 191, tecLOCKED = 192, tecBAD_CREDENTIALS = 193, + tecWRONG_ASSET = 194, + tecLIMIT_EXCEEDED = 195, + tecINVALID_DOMAIN = 196, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index f0f6c7f223c..f0c02f5b949 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -224,6 +224,12 @@ constexpr std::uint32_t tfAMMClawbackMask = ~(tfUniversal | tfClawTwoAssets); // BridgeModify flags: constexpr std::uint32_t tfClearAccountCreateAmount = 0x00010000; constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount); + +// VaultCreate flags: +constexpr std::uint32_t const tfVaultPrivate = 0x00010000; +static_assert(tfVaultPrivate == lsfVaultPrivate); +constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000; +constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable); // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index 9a7284158e7..c95ee41dedd 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -61,7 +61,8 @@ using NodeID = base_uint<160, detail::NodeIDTag>; /** MPTID is a 192-bit value representing MPT Issuance ID, * which is a concatenation of a 32-bit sequence (big endian) * and a 160-bit account */ -using MPTID = base_uint<192>; +// TODO - edhennis - Add a tag +using MPTID = uint192; /** XRP currency. */ Currency const& diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 7b120c0b8d2..7f81417abfe 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -23,12 +23,15 @@ #if !defined(XRPL_FIX) #error "undefined macro: XRPL_FIX" #endif +#if !defined(XRPL_RETIRE) +#error "undefined macro: XRPL_RETIRE" +#endif // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. -// If you add an amendment here, then do not forget to increment `numFeatures` -// in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (FrozenLPTokenTransfer, Supported::yes, VoteBehavior::DefaultNo) @@ -121,3 +124,22 @@ XRPL_FIX (NFTokenDirV1, Supported::yes, VoteBehavior::Obsolete) XRPL_FEATURE(NonFungibleTokensV1, Supported::yes, VoteBehavior::Obsolete) XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete) +// The following amendments have been active for at least two years. Their +// pre-amendment code has been removed and the identifiers are deprecated. +// All known amendments and amendments that may appear in a validated +// ledger must be registered either here or above with the "active" amendments +XRPL_RETIRE(MultiSign) +XRPL_RETIRE(TrustSetAuth) +XRPL_RETIRE(FeeEscalation) +XRPL_RETIRE(PayChan) +XRPL_RETIRE(CryptoConditions) +XRPL_RETIRE(TickSize) +XRPL_RETIRE(fix1368) +XRPL_RETIRE(Escrow) +XRPL_RETIRE(fix1373) +XRPL_RETIRE(EnforceInvariants) +XRPL_RETIRE(SortedDirectories) +XRPL_RETIRE(fix1201) +XRPL_RETIRE(fix1512) +XRPL_RETIRE(fix1523) +XRPL_RETIRE(fix1528) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 5a652baf4f7..2bd7e2ef526 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -166,6 +166,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({ {sfBurnedNFTokens, soeDEFAULT}, {sfFirstNFTokenSequence, soeOPTIONAL}, {sfAMMID, soeOPTIONAL}, + {sfVaultID, soeOPTIONAL}, })) /** A ledger object which contains a list of object identifiers. @@ -390,21 +391,6 @@ LEDGER_ENTRY(ltAMM, 0x0079, AMM, amm, ({ {sfPreviousTxnLgrSeq, soeOPTIONAL}, })) -/** A ledger object which tracks Oracle - \sa keylet::oracle - */ -LEDGER_ENTRY(ltORACLE, 0x0080, Oracle, oracle, ({ - {sfOwner, soeREQUIRED}, - {sfProvider, soeREQUIRED}, - {sfPriceDataSeries, soeREQUIRED}, - {sfAssetClass, soeREQUIRED}, - {sfLastUpdateTime, soeREQUIRED}, - {sfURI, soeOPTIONAL}, - {sfOwnerNode, soeREQUIRED}, - {sfPreviousTxnID, soeREQUIRED}, - {sfPreviousTxnLgrSeq, soeREQUIRED}, -})) - /** A ledger object which tracks MPTokenIssuance \sa keylet::mptIssuance */ @@ -419,6 +405,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfMPTokenMetadata, soeOPTIONAL}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfDomainID, soeOPTIONAL}, })) /** A ledger object which tracks MPToken @@ -433,6 +420,21 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ {sfPreviousTxnLgrSeq, soeREQUIRED}, })) +/** A ledger object which tracks Oracle + \sa keylet::oracle + */ +LEDGER_ENTRY(ltORACLE, 0x0080, Oracle, oracle, ({ + {sfOwner, soeREQUIRED}, + {sfProvider, soeREQUIRED}, + {sfPriceDataSeries, soeREQUIRED}, + {sfAssetClass, soeREQUIRED}, + {sfLastUpdateTime, soeREQUIRED}, + {sfURI, soeOPTIONAL}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, +})) + /** A ledger object which tracks Credential \sa keylet::credential */ @@ -460,6 +462,29 @@ LEDGER_ENTRY(ltPERMISSIONED_DOMAIN, 0x0082, PermissionedDomain, permissioned_dom {sfPreviousTxnLgrSeq, soeREQUIRED}, })) +/** A ledger object representing a single asset vault. + + \sa keylet::mptoken + */ +LEDGER_ENTRY(ltVAULT, 0x0083, Vault, vault, ({ + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfSequence, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfOwner, soeREQUIRED}, + {sfAccount, soeREQUIRED}, + {sfData, soeDEFAULT}, + {sfAsset, soeREQUIRED}, + {sfAssetTotal, soeDEFAULT}, + {sfAssetAvailable, soeDEFAULT}, + {sfAssetMaximum, soeDEFAULT}, + {sfLossUnrealized, soeDEFAULT}, + {sfMPTokenIssuanceID, soeREQUIRED}, // sfShare + {sfWithdrawalPolicy, soeREQUIRED}, + // no ShareTotal ever (use MPTIssuance.sfOutstandingAmount) + // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 3217bab9134..b78a78363ae 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -42,6 +42,7 @@ TYPED_SFIELD(sfTickSize, UINT8, 16) TYPED_SFIELD(sfUNLModifyDisabling, UINT8, 17) TYPED_SFIELD(sfHookResult, UINT8, 18) TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19) +TYPED_SFIELD(sfWithdrawalPolicy, UINT8, 20) // 16-bit integers (common) TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::sMD_Never) @@ -191,9 +192,14 @@ TYPED_SFIELD(sfHookHash, UINT256, 31) TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) +TYPED_SFIELD(sfVaultID, UINT256, 35) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) +TYPED_SFIELD(sfAssetAvailable, NUMBER, 2) +TYPED_SFIELD(sfAssetMaximum, NUMBER, 3) +TYPED_SFIELD(sfAssetTotal, NUMBER, 4) +TYPED_SFIELD(sfLossUnrealized, NUMBER, 5) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dd3ac42325d..cff9ae380e6 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -465,6 +465,49 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, ({ {sfDomainID, soeREQUIRED}, })) +/** This transaction creates a single asset vault. */ +TRANSACTION(ttVAULT_CREATE, 64, VaultCreate, ({ + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAssetMaximum, soeOPTIONAL}, + {sfMPTokenMetadata, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, // PermissionedDomainID + {sfWithdrawalPolicy, soeOPTIONAL}, + {sfData, soeOPTIONAL}, +})) + +/** This transaction updates a single asset vault. */ +TRANSACTION(ttVAULT_SET, 65, VaultSet, ({ + {sfVaultID, soeREQUIRED}, + {sfAssetMaximum, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, // PermissionedDomainID + {sfData, soeOPTIONAL}, +})) + +/** This transaction deletes a single asset vault. */ +TRANSACTION(ttVAULT_DELETE, 66, VaultDelete, ({ + {sfVaultID, soeREQUIRED}, +})) + +/** This transaction trades assets for shares with a vault. */ +TRANSACTION(ttVAULT_DEPOSIT, 67, VaultDeposit, ({ + {sfVaultID, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, +})) + +/** This transaction trades shares for assets with a vault. */ +TRANSACTION(ttVAULT_WITHDRAW, 68, VaultWithdraw, ({ + {sfVaultID, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, + {sfDestination, soeOPTIONAL}, +})) + +/** This transaction trades shares for assets with a vault. */ +TRANSACTION(ttVAULT_CLAWBACK, 69, VaultClawback, ({ + {sfVaultID, soeREQUIRED}, + {sfHolder, soeREQUIRED}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 483b69a962f..85c7b95387b 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -95,11 +95,12 @@ JSS(SigningPubKey); // field. JSS(Subject); // in: Credential transactions JSS(TakerGets); // field. JSS(TakerPays); // field. -JSS(TxnSignature); // field. JSS(TradingFee); // in/out: AMM trading fee JSS(TransactionType); // in: TransactionSign. JSS(TransferRate); // in: TransferRate. +JSS(TxnSignature); // field. JSS(URI); // field. +JSS(VaultID); // field. JSS(VoteSlots); // out: AMM Vote JSS(aborted); // out: InboundLedger JSS(accepted); // out: LedgerToJson, OwnerInfo, SubmitTransaction @@ -726,6 +727,8 @@ JSS(write_load); // out: GetCounts #undef JSS +// clang-format on + } // namespace jss } // namespace ripple diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index 90926afc6c4..69ec2eace70 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -231,6 +231,13 @@ Value::Value(const char* value) : type_(stringValue), allocated_(true) value_.string_ = valueAllocator()->duplicateStringValue(value); } +Value::Value(ripple::Number const& value) : type_(stringValue), allocated_(true) +{ + auto const tmp = to_string(value); + value_.string_ = + valueAllocator()->duplicateStringValue(tmp.c_str(), tmp.length()); +} + Value::Value(std::string const& value) : type_(stringValue), allocated_(true) { value_.string_ = valueAllocator()->duplicateStringValue( @@ -887,6 +894,12 @@ Value::operator[](const StaticString& key) return resolveReference(key, true); } +Value const& +Value::operator[](const StaticString& key) const +{ + return (*this)[key.c_str()]; +} + Value& Value::append(const Value& value) { diff --git a/src/libxrpl/protocol/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp index 3bebfc4659a..2a484d69307 100644 --- a/src/libxrpl/protocol/AMMCore.cpp +++ b/src/libxrpl/protocol/AMMCore.cpp @@ -26,18 +26,6 @@ namespace ripple { -AccountID -ammAccountID( - std::uint16_t prefix, - uint256 const& parentHash, - uint256 const& ammID) -{ - ripesha_hasher rsh; - auto const hash = sha512Half(prefix, parentHash, ammID); - rsh(hash.data(), hash.size()); - return AccountID{static_cast(rsh)}; -} - Currency ammLPTCurrency(Currency const& cur1, Currency const& cur2) { diff --git a/src/libxrpl/protocol/Asset.cpp b/src/libxrpl/protocol/Asset.cpp index 5a496352840..8035c808e4b 100644 --- a/src/libxrpl/protocol/Asset.cpp +++ b/src/libxrpl/protocol/Asset.cpp @@ -43,6 +43,12 @@ Asset::setJson(Json::Value& jv) const std::visit([&](auto&& issue) { issue.setJson(jv); }, issue_); } +STAmount +Asset::operator()(Number const& number) const +{ + return STAmount{*this, number}; +} + std::string to_string(Asset const& asset) { @@ -70,11 +76,4 @@ assetFromJson(Json::Value const& v) return mptIssueFromJson(v); } -Json::Value -to_json(Asset const& asset) -{ - return std::visit( - [&](auto const& issue) { return to_json(issue); }, asset.value()); -} - } // namespace ripple diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index 05164489ec7..52e1d7217cf 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -250,12 +250,9 @@ FeatureCollections::registerFeature( Feature const* i = getByName(name); if (!i) { - // If this check fails, and you just added a feature, increase the - // numFeatures value in Feature.h check( features.size() < detail::numFeatures, - "More features defined than allocated. Adjust numFeatures in " - "Feature.h."); + "More features defined than allocated."); auto const f = sha512Half(Slice(name.data(), name.size())); @@ -424,45 +421,26 @@ featureToName(uint256 const& f) #undef XRPL_FEATURE #pragma push_macro("XRPL_FIX") #undef XRPL_FIX +#pragma push_macro("XRPL_RETIRE") +#undef XRPL_RETIRE #define XRPL_FEATURE(name, supported, vote) \ uint256 const feature##name = registerFeature(#name, supported, vote); #define XRPL_FIX(name, supported, vote) \ uint256 const fix##name = registerFeature("fix" #name, supported, vote); +#define XRPL_RETIRE(name) \ + [[deprecated("The referenced amendment has been retired"), maybe_unused]] \ + uint256 const retired##name = retireFeature(#name); #include +#undef XRPL_RETIRE +#pragma pop_macro("XRPL_RETIRE") #undef XRPL_FIX #pragma pop_macro("XRPL_FIX") #undef XRPL_FEATURE #pragma pop_macro("XRPL_FEATURE") -// clang-format off - -// The following amendments have been active for at least two years. Their -// pre-amendment code has been removed and the identifiers are deprecated. -// All known amendments and amendments that may appear in a validated -// ledger must be registered either here or above with the "active" amendments -[[deprecated("The referenced amendment has been retired"), maybe_unused]] -uint256 const - retiredMultiSign = retireFeature("MultiSign"), - retiredTrustSetAuth = retireFeature("TrustSetAuth"), - retiredFeeEscalation = retireFeature("FeeEscalation"), - retiredPayChan = retireFeature("PayChan"), - retiredCryptoConditions = retireFeature("CryptoConditions"), - retiredTickSize = retireFeature("TickSize"), - retiredFix1368 = retireFeature("fix1368"), - retiredEscrow = retireFeature("Escrow"), - retiredFix1373 = retireFeature("fix1373"), - retiredEnforceInvariants = retireFeature("EnforceInvariants"), - retiredSortedDirectories = retireFeature("SortedDirectories"), - retiredFix1201 = retireFeature("fix1201"), - retiredFix1512 = retireFeature("fix1512"), - retiredFix1523 = retireFeature("fix1523"), - retiredFix1528 = retireFeature("fix1528"); - -// clang-format on - // All of the features should now be registered, since variables in a cpp file // are initialized from top to bottom. // diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 046be444224..25604438903 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -79,6 +80,7 @@ enum class LedgerNameSpace : std::uint16_t { MPTOKEN = 't', CREDENTIAL = 'D', PERMISSIONED_DOMAIN = 'm', + VAULT = 'V', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -528,6 +530,12 @@ credential( indexHash(LedgerNameSpace::CREDENTIAL, subject, issuer, credType)}; } +Keylet +vault(AccountID const& owner, std::uint32_t seq) noexcept +{ + return vault(indexHash(LedgerNameSpace::VAULT, owner, seq)); +} + Keylet permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept { diff --git a/src/libxrpl/protocol/Keylet.cpp b/src/libxrpl/protocol/Keylet.cpp index 846c3bc07b3..dd55b9d4ce6 100644 --- a/src/libxrpl/protocol/Keylet.cpp +++ b/src/libxrpl/protocol/Keylet.cpp @@ -35,7 +35,7 @@ Keylet::check(STLedgerEntry const& sle) const if (type == ltCHILD) return sle.getType() != ltDIR_NODE; - return sle.getType() == type; + return sle.getType() == type && sle.key() == key; } } // namespace ripple diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index 38022a0ed3a..64b4df730c1 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -39,12 +39,6 @@ MPTIssue::getIssuer() const return *account; } -MPTID const& -MPTIssue::getMptID() const -{ - return mptID_; -} - std::string MPTIssue::getText() const { diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 37830830ade..dad5f435c45 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -20,15 +20,14 @@ #include #include #include -#include #include #include +#include #include #include #include #include #include -#include #include #include @@ -277,6 +276,7 @@ STAmount::xrp() const "Cannot return non-native STAmount as XRPAmount"); auto drops = static_cast(mValue); + XRPL_ASSERT(mOffset == 0, "ripple::STAmount::xrp : amount is canonical"); if (mIsNegative) drops = -drops; @@ -306,6 +306,7 @@ STAmount::mpt() const Throw("Cannot return STAmount as MPTAmount"); auto value = static_cast(mValue); + XRPL_ASSERT(mOffset == 0, "ripple::STAmount::mpt : amount is canonical"); if (mIsNegative) value = -value; @@ -833,75 +834,16 @@ amountFromQuality(std::uint64_t rate) STAmount amountFromString(Asset const& asset, std::string const& amount) { - static boost::regex const reNumber( - "^" // the beginning of the string - "([-+]?)" // (optional) + or - character - "(0|[1-9][0-9]*)" // a number (no leading zeroes, unless 0) - "(\\.([0-9]+))?" // (optional) period followed by any number - "([eE]([+-]?)([0-9]+))?" // (optional) E, optional + or -, any number - "$", - boost::regex_constants::optimize); - - boost::smatch match; - - if (!boost::regex_match(amount, match, reNumber)) - Throw("Number '" + amount + "' is not valid"); - - // Match fields: - // 0 = whole input - // 1 = sign - // 2 = integer portion - // 3 = whole fraction (with '.') - // 4 = fraction (without '.') - // 5 = whole exponent (with 'e') - // 6 = exponent sign - // 7 = exponent number - - // CHECKME: Why 32? Shouldn't this be 16? - if ((match[2].length() + match[4].length()) > 32) - Throw("Number '" + amount + "' is overlong"); - - bool negative = (match[1].matched && (match[1] == "-")); - - // Can't specify XRP or MPT using fractional representation - if ((asset.native() || asset.holds()) && match[3].matched) + auto const parts = partsFromString(amount); + if ((asset.native() || asset.holds()) && parts.exponent < 0) Throw( "XRP and MPT must be specified as integral amount."); - - std::uint64_t mantissa; - int exponent; - - if (!match[4].matched) // integer only - { - mantissa = - beast::lexicalCastThrow(std::string(match[2])); - exponent = 0; - } - else - { - // integer and fraction - mantissa = beast::lexicalCastThrow(match[2] + match[4]); - exponent = -(match[4].length()); - } - - if (match[5].matched) - { - // we have an exponent - if (match[6].matched && (match[6] == "-")) - exponent -= beast::lexicalCastThrow(std::string(match[7])); - else - exponent += beast::lexicalCastThrow(std::string(match[7])); - } - - return {asset, mantissa, exponent, negative}; + return {asset, parts.mantissa, parts.exponent, parts.negative}; } STAmount amountFromJson(SField const& name, Json::Value const& v) { - STAmount::mantissa_type mantissa = 0; - STAmount::exponent_type exponent = 0; - bool negative = false; Asset asset; Json::Value value; @@ -993,36 +935,38 @@ amountFromJson(SField const& name, Json::Value const& v) } } + NumberParts parts; + if (value.isInt()) { if (value.asInt() >= 0) { - mantissa = value.asInt(); + parts.mantissa = value.asInt(); } else { - mantissa = -value.asInt(); - negative = true; + parts.mantissa = -value.asInt(); + parts.negative = true; } } else if (value.isUInt()) { - mantissa = v.asUInt(); + parts.mantissa = v.asUInt(); } else if (value.isString()) { - auto const ret = amountFromString(asset, value.asString()); - - mantissa = ret.mantissa(); - exponent = ret.exponent(); - negative = ret.negative(); + parts = partsFromString(value.asString()); + // Can't specify XRP or MPT using fractional representation + if ((asset.native() || asset.holds()) && parts.exponent < 0) + Throw( + "XRP and MPT must be specified as integral amount."); } else { Throw("invalid amount type"); } - return {name, asset, mantissa, exponent, negative}; + return {name, asset, parts.mantissa, parts.exponent, parts.negative}; } bool diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index 74961bfbcab..0164bb806f8 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -19,8 +19,10 @@ #include +#include #include #include +#include namespace ripple { @@ -108,4 +110,101 @@ operator<<(std::ostream& out, STNumber const& rhs) return out << rhs.getText(); } +NumberParts +partsFromString(std::string const& number) +{ + static boost::regex const reNumber( + "^" // the beginning of the string + "([-+]?)" // (optional) + or - character + "(0|[1-9][0-9]*)" // a number (no leading zeroes, unless 0) + "(\\.([0-9]+))?" // (optional) period followed by any number + "([eE]([+-]?)([0-9]+))?" // (optional) E, optional + or -, any number + "$", + boost::regex_constants::optimize); + + boost::smatch match; + + if (!boost::regex_match(number, match, reNumber)) + Throw("'" + number + "' is not a number"); + + // Match fields: + // 0 = whole input + // 1 = sign + // 2 = integer portion + // 3 = whole fraction (with '.') + // 4 = fraction (without '.') + // 5 = whole exponent (with 'e') + // 6 = exponent sign + // 7 = exponent number + + bool negative = (match[1].matched && (match[1] == "-")); + + std::uint64_t mantissa; + int exponent; + + if (!match[4].matched) // integer only + { + mantissa = + beast::lexicalCastThrow(std::string(match[2])); + exponent = 0; + } + else + { + // integer and fraction + mantissa = beast::lexicalCastThrow(match[2] + match[4]); + exponent = -(match[4].length()); + } + + if (match[5].matched) + { + // we have an exponent + if (match[6].matched && (match[6] == "-")) + exponent -= beast::lexicalCastThrow(std::string(match[7])); + else + exponent += beast::lexicalCastThrow(std::string(match[7])); + } + + return {mantissa, exponent, negative}; +} + +STNumber +numberFromJson(SField const& field, Json::Value const& value) +{ + NumberParts parts; + + if (value.isInt()) + { + if (value.asInt() >= 0) + { + parts.mantissa = value.asInt(); + } + else + { + parts.mantissa = -value.asInt(); + parts.negative = true; + } + } + else if (value.isUInt()) + { + parts.mantissa = value.asUInt(); + } + else if (value.isString()) + { + parts = partsFromString(value.asString()); + // Only strings can represent out-of-range values. + if (parts.mantissa > std::numeric_limits::max()) + Throw("too high"); + } + else + { + Throw("not a number"); + } + + std::int64_t mantissa = parts.mantissa; + if (parts.negative) + mantissa = -mantissa; + + return STNumber{field, Number{mantissa, parts.exponent}}; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 7d08993a8ba..6410051bde3 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -574,6 +575,20 @@ parseLeaf( break; + case STI_NUMBER: + try + { + ret = + detail::make_stvar(numberFromJson(field, value)); + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + break; + case STI_VECTOR256: if (!value.isArrayOrNull()) { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index bd1c461c8c7..66d4a95429e 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -197,6 +197,12 @@ STTx::getSeqProxy() const return SeqProxy{SeqProxy::ticket, *ticketSeq}; } +std::uint32_t +STTx::getSeqValue() const +{ + return getSeqProxy().value(); +} + void STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey) { diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp index c5d3102bfa7..f997604e780 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -188,6 +188,9 @@ STVar::constructST(SerializedTypeID id, int depth, Args&&... args) case STI_AMOUNT: construct(std::forward(args)...); return; + case STI_NUMBER: + construct(std::forward(args)...); + return; case STI_UINT128: construct(std::forward(args)...); return; diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 815b27c0018..317e1ace891 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -117,6 +117,9 @@ transResults() MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."), MAKE_ERROR(tecLOCKED, "Fund is locked."), MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."), + MAKE_ERROR(tecWRONG_ASSET, "Wrong asset given."), + MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."), + MAKE_ERROR(tecINVALID_DOMAIN, "Invalid permissioned domain."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 96053b93b44..721549ae983 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -3535,7 +3535,7 @@ struct AMMExtended_test : public jtx::AMMTest if (!BEAST_EXPECT(checkArraySize(affected, 4u))) return; auto ff = - affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName]; + affected[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName]; BEAST_EXPECT( ff[sfHighLimit.fieldName] == bob["USD"](100).value().getJson(JsonOptions::none)); @@ -3809,10 +3809,10 @@ struct AMMExtended_test : public jtx::AMMTest auto ff = affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName]; BEAST_EXPECT( - ff[sfHighLimit.fieldName] == + ff[sfLowLimit.fieldName] == G1["USD"](0).value().getJson(JsonOptions::none)); - BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfLowFreeze)); - BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfHighFreeze); + BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfLowFreeze); + BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfHighFreeze)); env.close(); // test: Can make a payment via the new offer diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp new file mode 100644 index 00000000000..98f245cc025 --- /dev/null +++ b/src/test/app/Vault_test.cpp @@ -0,0 +1,622 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +using namespace test::jtx; + +class Vault_test : public beast::unit_test::suite +{ + void + testSequences() + { + using namespace test::jtx; + + static auto constexpr negativeAmount = + [](PrettyAsset const& asset) -> PrettyAmount { + return { + STAmount{asset.raw(), 1ul, 0, true, STAmount::unchecked{}}, ""}; + }; + + auto const testSequence = [this]( + std::string const& prefix, + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Vault& vault, + PrettyAsset const& asset) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + BEAST_EXPECT(env.le(keylet)); + + { + testcase(prefix + " fail to deposit more than assets held"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(10000)}); + env(tx, ter(tecINSUFFICIENT_FUNDS)); + } + + { + testcase(prefix + " fail to deposit negative amount"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = negativeAmount(asset)}); + env(tx, ter(temBAD_AMOUNT)); + } + + { + testcase(prefix + " fail to deposit zero amount"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(0)}); + env(tx, ter(temBAD_AMOUNT)); + } + + { + testcase(prefix + " deposit non-zero amount"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx); + } + + { + testcase(prefix + " deposit non-zero amount again"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx); + } + + { + testcase(prefix + " fail to delete non-empty vault"); + auto tx = vault.del({.owner = owner, .id = keylet.key}); + env(tx, ter(tecHAS_OBLIGATIONS)); + } + + { + testcase(prefix + " fail to update because wrong owner"); + auto tx = vault.set({.owner = issuer, .id = keylet.key}); + env(tx, ter(tecNO_PERMISSION)); + } + + { + testcase(prefix + " fail to update immutable flags"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfFlags] = tfVaultPrivate; + env(tx, ter(temINVALID_FLAG)); + } + + { + testcase( + prefix + " fail to set maximum lower than current amount"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfAssetMaximum] = asset(50).number(); + env(tx, ter(tecLIMIT_EXCEEDED)); + } + + { + testcase(prefix + " set maximum higher than current amount"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfAssetMaximum] = asset(200).number(); + env(tx); + } + + { + testcase(prefix + " fail to set zero domain"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(base_uint<256>(beast::zero)); + env(tx, ter(temMALFORMED)); + } + + { + testcase(prefix + " fail to set nonexistent domain"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(base_uint<256>(42ul)); + env(tx, ter(tecINVALID_DOMAIN)); + } + + { + testcase(prefix + " fail to deposit more than maximum"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(200)}); + env(tx, ter(tecLIMIT_EXCEEDED)); + } + + { + testcase(prefix + " fail to withdraw negative amount"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = negativeAmount(asset)}); + env(tx, ter(temBAD_AMOUNT)); + } + + { + testcase(prefix + " fail to withdraw zero amount"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(0)}); + env(tx, ter(temBAD_AMOUNT)); + } + + { + testcase(prefix + " fail to withdraw more than assets held"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(1000)}); + env(tx, ter(tecINSUFFICIENT_FUNDS)); + } + + { + testcase(prefix + " deposit up to maximum"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx); + } + + if (!asset.raw().native()) + { + testcase(prefix + " fail to clawback because wrong issuer"); + auto tx = vault.clawback( + {.issuer = owner, + .id = keylet.key, + .holder = depositor, + .amount = asset(50)}); + env(tx, ter(temMALFORMED)); + } + + { + testcase(prefix + " fail to clawback negative amount"); + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = negativeAmount(asset)}); + env(tx, ter(temBAD_AMOUNT)); + } + + { + testcase(prefix + " clawback some"); + auto code = + asset.raw().native() ? ter(temMALFORMED) : ter(tesSUCCESS); + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = asset(10)}); + env(tx, code); + } + + { + testcase(prefix + " clawback all"); + auto code = asset.raw().native() ? ter(tecNO_PERMISSION) + : ter(tesSUCCESS); + auto tx = vault.clawback( + {.issuer = issuer, .id = keylet.key, .holder = depositor}); + env(tx, code); + } + + if (!asset.raw().native()) + { + testcase(prefix + " deposit again"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(200)}); + env(tx); + } + + { + testcase(prefix + " withdraw non-zero assets"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(200)}); + env(tx); + } + + { + testcase(prefix + " fail to delete because wrong owner"); + auto tx = vault.del({.owner = issuer, .id = keylet.key}); + env(tx, ter(tecNO_PERMISSION)); + } + + { + testcase(prefix + " delete empty vault"); + auto tx = vault.del({.owner = owner, .id = keylet.key}); + env(tx); + BEAST_EXPECT(!env.le(keylet)); + } + }; + + auto testCases = + [this, &testSequence]( + std::string prefix, + std::function + setup) { + Env env{*this}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + auto vault = env.vault(); + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(issuer, asfAllowTrustLineClawback)); + + PrettyAsset asset = setup(env, issuer, depositor); + testSequence( + prefix, env, issuer, owner, depositor, vault, asset); + }; + + testCases( + "XRP", + [](Env& env, Account const& issuer, Account const& depositor) + -> PrettyAsset { return {xrpIssue(), 1'000'000}; }); + + testCases( + "IOU", + [](Env& env, + Account const& issuer, + Account const& depositor) -> Asset { + PrettyAsset asset = issuer["IOU"]; + env.trust(asset(1000), depositor); + env(pay(issuer, depositor, asset(1000))); + env.close(); + return asset; + }); + + testCases( + "MPT", + [](Env& env, + Account const& issuer, + Account const& depositor) -> Asset { + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset asset = mptt.issuanceID(); + mptt.authorize({.account = depositor}); + env(pay(issuer, depositor, asset(1000))); + env.close(); + return asset; + }); + } + + // Test for non-asset specific behaviors. + void + testCreateFailXRP() + { + using namespace test::jtx; + + auto testCase = [this](std::function test) { + Env env{*this}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + auto vault = env.vault(); + Asset asset = xrpIssue(); + + test(env, issuer, owner, depositor, asset, vault); + }; + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + testcase("nothing to delete"); + auto tx = vault.del({.owner = issuer, .id = keylet::skip().key}); + env(tx, ter(tecOBJECT_NOT_FOUND)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + testcase("transaction is good"); + env(tx); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + tx[sfWithdrawalPolicy] = 1; + testcase("explicitly select withdrawal policy"); + env(tx); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + tx[sfWithdrawalPolicy] = 0; + testcase("invalid withdrawal policy"); + env(tx, ter(temMALFORMED)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + testcase("insufficient fee"); + env(tx, fee(env.current()->fees().base), ter(telINSUF_FEE_P)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + testcase("insufficient reserve"); + // It is possible to construct a complicated mathematical + // expression for this amount, but it is sadly not easy. + env(pay(owner, issuer, XRP(775))); + env.close(); + env(tx, ter(tecINSUFFICIENT_RESERVE)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + testcase("empty data"); + tx[sfData] = ""; + env(tx, ter(temMALFORMED)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + testcase("data too large"); + // A hexadecimal string of 257 bytes. + tx[sfData] = std::string(514, 'A'); + env(tx, ter(temMALFORMED)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + testcase("empty metadata"); + tx[sfMPTokenMetadata] = ""; + env(tx, ter(temMALFORMED)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + testcase("metadata too large"); + // This metadata is for the share token. + // A hexadecimal string of 1025 bytes. + tx[sfMPTokenMetadata] = std::string(2050, 'B'); + env(tx, ter(temMALFORMED)); + }); + } + + void + testCreateFailIOU() + { + using namespace test::jtx; + Env env{*this}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + auto vault = env.vault(); + Asset asset = issuer["IOU"]; + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + env(fset(issuer, asfGlobalFreeze)); + env.close(); + env(tx, ter(tecFROZEN)); + env.close(); + } + + void + testCreateFailMPT() + { + using namespace test::jtx; + Env env{*this}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + auto vault = env.vault(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + + // Locked because that is the default flag. + mptt.create(); + Asset asset = mptt.issuanceID(); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx, ter(tecNO_AUTH)); + } + + void + testWithMPT() + { + using namespace test::jtx; + + auto testCase = [this](std::function test) { + Env env{*this}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + auto vault = env.vault(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset asset = mptt.issuanceID(); + mptt.authorize({.account = depositor}); + env(pay(issuer, depositor, asset(1000))); + env.close(); + + test(env, issuer, owner, depositor, asset, vault, mptt); + }; + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("global lock"); + mptt.set({.account = issuer, .flags = tfMPTLock}); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx, ter(tecLOCKED)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("deposit non-zero amount"); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx); + env.close(); + // Check that the OutstandingAmount field of MPTIssuance + // accounts for the issued shares. + auto v = env.le(keylet); + BEAST_EXPECT(v); + MPTID share = (*v)[sfMPTokenIssuanceID]; + auto issuance = env.le(keylet::mptIssuance(share)); + BEAST_EXPECT(issuance); + Number outstandingShares = issuance->at(sfOutstandingAmount); + BEAST_EXPECT(outstandingShares > 0); + BEAST_EXPECT(outstandingShares == 100); + }); + } + +public: + void + run() override + { + testSequences(); + testCreateFailXRP(); + testCreateFailIOU(); + testCreateFailMPT(); + testWithMPT(); + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(Vault, tx, ripple, 1); + +} // namespace ripple diff --git a/src/test/basics/IOUAmount_test.cpp b/src/test/basics/IOUAmount_test.cpp index 306953d5ab9..6ba1cfd6f19 100644 --- a/src/test/basics/IOUAmount_test.cpp +++ b/src/test/basics/IOUAmount_test.cpp @@ -44,6 +44,11 @@ class IOUAmount_test : public beast::unit_test::suite IOUAmount const zz(beast::zero); BEAST_EXPECT(z == zz); + + // https://github.com/XRPLF/rippled/issues/5170 + IOUAmount const zzz{}; + BEAST_EXPECT(zzz == beast::zero); + // BEAST_EXPECT(zzz == zz); } void diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 66845ae91dc..dac72c44c92 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -472,6 +473,12 @@ class Env std::uint32_t ownerCount(Account const& account) const; + Vault + vault() + { + return Vault{*this}; + } + /** Return an account root. @return empty if the account does not exist. */ diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 9990c77c38c..56e174aafca 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -21,12 +21,12 @@ #define RIPPLE_TEST_JTX_AMOUNT_H_INCLUDED #include -#include #include #include #include #include #include +#include #include #include #include @@ -126,12 +126,23 @@ struct PrettyAmount return amount_; } + Number + number() const + { + return amount_; + } + operator STAmount const&() const { return amount_; } operator AnyAmount() const; + + operator Json::Value() const + { + return to_json(value()); + } }; inline bool @@ -149,6 +160,49 @@ operator!=(PrettyAmount const& lhs, PrettyAmount const& rhs) std::ostream& operator<<(std::ostream& os, PrettyAmount const& amount); +struct PrettyAsset +{ +private: + Asset asset_; + unsigned int scale_; + +public: + template + requires std::convertible_to + PrettyAsset(A const& asset, unsigned int scale = 1) + : PrettyAsset{Asset{asset}, scale} + { + } + + PrettyAsset(Asset const& asset, unsigned int scale = 1) + : asset_(asset), scale_(scale) + { + } + + Asset const& + raw() const + { + return asset_; + } + + operator Asset const&() const + { + return asset_; + } + + operator Json::Value() const + { + return to_json(asset_); + } + + template + PrettyAmount + operator()(T v) const + { + STAmount amount{asset_, v * scale_}; + return {amount, ""}; + } +}; //------------------------------------------------------------------------------ // Specifies an order book diff --git a/src/test/jtx/basic_prop.h b/src/test/jtx/basic_prop.h index 742b8744ef5..a8daafba410 100644 --- a/src/test/jtx/basic_prop.h +++ b/src/test/jtx/basic_prop.h @@ -20,6 +20,8 @@ #ifndef RIPPLE_TEST_JTX_BASIC_PROP_H_INCLUDED #define RIPPLE_TEST_JTX_BASIC_PROP_H_INCLUDED +#include + namespace ripple { namespace test { namespace jtx { diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 43286ab7824..9b840befdb0 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -605,6 +605,5 @@ Env::disableFeature(uint256 const feature) } } // namespace jtx - } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/vault.cpp b/src/test/jtx/impl/vault.cpp new file mode 100644 index 00000000000..b8a3e001b7f --- /dev/null +++ b/src/test/jtx/impl/vault.cpp @@ -0,0 +1,104 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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 +#include + +#include +#include +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +std::tuple +Vault::create(CreateArgs const& args) +{ + auto keylet = keylet::vault(args.owner.id(), env.seq(args.owner)); + Json::Value jv; + jv[jss::TransactionType] = jss::VaultCreate; + jv[jss::Account] = args.owner.human(); + jv[jss::Asset] = to_json(args.asset); + jv[jss::Fee] = STAmount(env.current()->fees().increment).getJson(); + if (args.flags) + jv[jss::Flags] = *args.flags; + return {jv, keylet}; +} + +Json::Value +Vault::set(SetArgs const& args) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::VaultSet; + jv[jss::Account] = args.owner.human(); + jv[jss::VaultID] = to_string(args.id); + return jv; +} + +Json::Value +Vault::del(DeleteArgs const& args) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::VaultDelete; + jv[jss::Account] = args.owner.human(); + jv[jss::VaultID] = to_string(args.id); + return jv; +} + +Json::Value +Vault::deposit(DepositArgs const& args) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::VaultDeposit; + jv[jss::Account] = args.depositor.human(); + jv[jss::VaultID] = to_string(args.id); + jv[jss::Amount] = to_json(args.amount); + return jv; +} + +Json::Value +Vault::withdraw(WithdrawArgs const& args) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::VaultWithdraw; + jv[jss::Account] = args.depositor.human(); + jv[jss::VaultID] = to_string(args.id); + jv[jss::Amount] = to_json(args.amount); + return jv; +} + +Json::Value +Vault::clawback(ClawbackArgs const& args) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::VaultClawback; + jv[jss::Account] = args.issuer.human(); + jv[jss::VaultID] = to_string(args.id); + jv[jss::Holder] = args.holder.human(); + if (args.amount) + jv[jss::Amount] = to_json(*args.amount); + return jv; +} + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 12b9d74d27c..7de6425b9dc 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -88,11 +88,12 @@ class requireAny struct MPTInit { std::vector holders = {}; - PrettyAmount const& xrp = XRP(10'000); - PrettyAmount const& xrpHolders = XRP(10'000); + PrettyAmount const xrp = XRP(10'000); + PrettyAmount const xrpHolders = XRP(10'000); bool fund = true; bool close = true; }; +static MPTInit const mptInitNoFund{.fund = false}; struct MPTCreate { diff --git a/src/test/jtx/vault.h b/src/test/jtx/vault.h new file mode 100644 index 00000000000..74c482bf17b --- /dev/null +++ b/src/test/jtx/vault.h @@ -0,0 +1,109 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_TEST_JTX_VAULT_H_INCLUDED +#define RIPPLE_TEST_JTX_VAULT_H_INCLUDED + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +class Env; + +struct Vault +{ + Env& env; + + struct CreateArgs + { + Account owner; + Asset asset; + std::optional flags{}; + }; + + /** Return a VaultCreate transaction and the Vault's expected keylet. */ + std::tuple + create(CreateArgs const& args); + + struct SetArgs + { + Account owner; + uint256 id; + }; + + Json::Value + set(SetArgs const& args); + + struct DeleteArgs + { + Account owner; + uint256 id; + }; + + Json::Value + del(DeleteArgs const& args); + + struct DepositArgs + { + Account depositor; + uint256 id; + STAmount amount; + }; + + Json::Value + deposit(DepositArgs const& args); + + struct WithdrawArgs + { + Account depositor; + uint256 id; + STAmount amount; + }; + + Json::Value + withdraw(WithdrawArgs const& args); + + struct ClawbackArgs + { + Account issuer; + uint256 id; + Account holder; + std::optional amount{}; + }; + + Json::Value + clawback(ClawbackArgs const& args); +}; + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index ecf1c8e3979..bfd45043594 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -115,12 +115,14 @@ class Invariants_test : public beast::unit_test::suite sink.messages().str().starts_with("Invariant failed:") || sink.messages().str().starts_with( "Transaction caused an exception")); - // uncomment if you want to log the invariant failure message - // log << " --> " << sink.messages().str() << std::endl; for (auto const& m : expect_logs) { - BEAST_EXPECT( - sink.messages().str().find(m) != std::string::npos); + if (sink.messages().str().find(m) == std::string::npos) + { + // uncomment if you want to log the invariant failure + // message log << " --> " << m << std::endl; + fail(); + } } } } @@ -783,7 +785,7 @@ class Invariants_test : public beast::unit_test::suite testcase << "valid new account root"; doInvariantCheck( - {{"account root created by a non-Payment"}}, + {{"account root created illegally"}}, [](Account const&, Account const&, ApplyContext& ac) { // Insert a new account root created by a non-payment into // the view. diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp index ed255e32f1c..6a3afc5e0a6 100644 --- a/src/test/protocol/STNumber_test.cpp +++ b/src/test/protocol/STNumber_test.cpp @@ -78,6 +78,15 @@ struct STNumber_test : public beast::unit_test::suite STAmount const totalAmount{totalValue, strikePrice.issue()}; BEAST_EXPECT(totalAmount == Number{10'000}); } + + { + BEAST_EXPECT( + numberFromJson(sfNumber, "123") == STNumber(sfNumber, 123)); + BEAST_EXPECT( + numberFromJson(sfNumber, "3.14e2") == STNumber(sfNumber, 314)); + BEAST_EXPECT( + numberFromJson(sfNumber, "1000e-2") == STNumber(sfNumber, 10)); + } } }; diff --git a/src/xrpld/app/misc/CredentialHelpers.cpp b/src/xrpld/app/misc/CredentialHelpers.cpp index a18cd40336b..62d47204807 100644 --- a/src/xrpld/app/misc/CredentialHelpers.cpp +++ b/src/xrpld/app/misc/CredentialHelpers.cpp @@ -19,6 +19,10 @@ #include #include +#include +#include +#include +#include #include #include @@ -38,12 +42,11 @@ checkExpired( } bool -removeExpired(ApplyView& view, STTx const& tx, beast::Journal const j) +removeExpired(ApplyView& view, STVector256 const& arr, beast::Journal const j) { auto const closeTime = view.info().parentCloseTime; bool foundExpired = false; - STVector256 const& arr(tx.getFieldV256(sfCredentialIDs)); for (auto const& h : arr) { // Credentials already checked in preclaim. Look only for expired here. @@ -185,15 +188,61 @@ valid(PreclaimContext const& ctx, AccountID const& src) } TER -authorized(ApplyContext const& ctx, AccountID const& dst) +validDomain(ReadView const& view, uint256 domainID, AccountID const& subject) +{ + // Note, permissioned domain objects can be deleted at any time + auto const slePD = view.read(keylet::permissionedDomain(domainID)); + if (!slePD) + return tecINVALID_DOMAIN; + else if (!slePD->isFieldPresent(sfAcceptedCredentials)) + return tefINTERNAL; + + auto const closeTime = view.info().parentCloseTime; + bool foundExpired = false; + for (auto const& h : slePD->getFieldArray(sfAcceptedCredentials)) + { + if (!h.isFieldPresent(sfIssuer) || !h.isFieldPresent(sfCredentialType)) + return tefINTERNAL; + + auto const issuer = h.getAccountID(sfIssuer); + auto const type = makeSlice(h.getFieldVL(sfCredentialType)); + auto const sleCredential = + view.read(keylet::credential(subject, issuer, type)); + + // We cannot delete expired credentials, that would require ApplyView& + // However we can check if credentials are expired. Expected transaction + // flow is to use `validDomain` in preclaim, converting tecEXPIRED to + // tesSUCCESS, then proceed to call `verifyValidDomain` in doApply. This + // allows expired credentials to be deleted by any transaction. + if (sleCredential) + { + if (checkExpired(sleCredential, closeTime)) + { + foundExpired = true; + continue; + } + else if (sleCredential->getFlags() & lsfAccepted) + return tesSUCCESS; + else + continue; + } + } + + return foundExpired ? tecEXPIRED : tecNO_PERMISSION; +} + +TER +authorizedDepositPreauth( + ApplyView const& view, + STVector256 const& credIDs, + AccountID const& dst) { - auto const& credIDs(ctx.tx.getFieldV256(sfCredentialIDs)); std::set> sorted; std::vector> lifeExtender; lifeExtender.reserve(credIDs.size()); for (auto const& h : credIDs) { - auto sleCred = ctx.view().read(keylet::credential(h)); + auto sleCred = view.read(keylet::credential(h)); if (!sleCred) // already checked in preclaim return tefINTERNAL; @@ -204,11 +253,8 @@ authorized(ApplyContext const& ctx, AccountID const& dst) lifeExtender.push_back(std::move(sleCred)); } - if (!ctx.view().exists(keylet::depositPreauth(dst, sorted))) - { - JLOG(ctx.journal.trace()) << "DepositPreauth doesn't exist"; + if (!view.exists(keylet::depositPreauth(dst, sorted))) return tecNO_PERMISSION; - } return tesSUCCESS; } @@ -272,6 +318,48 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j) } // namespace credentials +TER +verifyValidDomain( + ApplyView& view, + AccountID const& account, + uint256 domainID, + beast::Journal j) +{ + auto const slePD = view.read(keylet::permissionedDomain(domainID)); + if (!slePD || !slePD->isFieldPresent(sfAcceptedCredentials)) + return tefINTERNAL; + + // Collect all matching credentials on a side, so we can remove expired ones + // We may finish the loop with this collection empty, it's fine. + STVector256 credentials; + for (auto const& h : slePD->getFieldArray(sfAcceptedCredentials)) + { + if (!h.isFieldPresent(sfIssuer) || !h.isFieldPresent(sfCredentialType)) + return tefINTERNAL; + + auto const issuer = h.getAccountID(sfIssuer); + auto const type = makeSlice(h.getFieldVL(sfCredentialType)); + auto const keyletCredential = keylet::credential(account, issuer, type); + if (view.exists(keyletCredential)) + credentials.push_back(keyletCredential.key); + } + + // Result intentionally ignored. + [[maybe_unused]] bool _ = credentials::removeExpired(view, credentials, j); + + for (auto const& h : credentials) + { + auto sleCredential = view.read(keylet::credential(h)); + if (!sleCredential) + continue; // expired, i.e. deleted in credentials::removeExpired + + if (sleCredential->getFlags() & lsfAccepted) + return tesSUCCESS; + } + + return tecNO_PERMISSION; +} + TER verifyDepositPreauth( ApplyContext& ctx, @@ -288,7 +376,8 @@ verifyDepositPreauth( bool const credentialsPresent = ctx.tx.isFieldPresent(sfCredentialIDs); if (credentialsPresent && - credentials::removeExpired(ctx.view(), ctx.tx, ctx.journal)) + credentials::removeExpired( + ctx.view(), ctx.tx.getFieldV256(sfCredentialIDs), ctx.journal)) return tecEXPIRED; if (sleDst && (sleDst->getFlags() & lsfDepositAuth)) @@ -296,8 +385,12 @@ verifyDepositPreauth( if (src != dst) { if (!ctx.view().exists(keylet::depositPreauth(dst, src))) - return !credentialsPresent ? tecNO_PERMISSION - : credentials::authorized(ctx, dst); + return !credentialsPresent + ? tecNO_PERMISSION + : credentials::authorizedDepositPreauth( + ctx.view(), + ctx.tx.getFieldV256(sfCredentialIDs), + dst); } } diff --git a/src/xrpld/app/misc/CredentialHelpers.h b/src/xrpld/app/misc/CredentialHelpers.h index acc4f2621db..77c189a7ccf 100644 --- a/src/xrpld/app/misc/CredentialHelpers.h +++ b/src/xrpld/app/misc/CredentialHelpers.h @@ -34,9 +34,9 @@ checkExpired( std::shared_ptr const& sleCredential, NetClock::time_point const& closed); -// Return true if at least 1 expired credentials was found(and deleted) +// Return true if any expired credential was found in arr (and deleted) bool -removeExpired(ApplyView& view, STTx const& tx, beast::Journal const j); +removeExpired(ApplyView& view, STVector256 const& arr, beast::Journal const j); // Actually remove a credentials object from the ledger TER @@ -49,14 +49,25 @@ deleteSLE( NotTEC checkFields(PreflightContext const& ctx); -// Accessing the ledger to check if provided credentials are valid +// Accessing the ledger to check if provided credentials are valid. Do not use +// in doApply (only in preclaim) since it does not remove expired credentials. +// If you call it in prelaim, you also must call verifyDepositPreauth in doApply TER valid(PreclaimContext const& ctx, AccountID const& src); -// This function is only called when we about to return tecNO_PERMISSION because -// all the checks for the DepositPreauth authorization failed. +// Check if subject has any credential maching the given domain. If you call it +// in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in +// doApply. This will ensure that expired credentials are deleted. TER -authorized(ApplyContext const& ctx, AccountID const& dst); +validDomain(ReadView const& view, uint256 domainID, AccountID const& subject); + +// This function is only called when we about to return tecNO_PERMISSION +// because all the checks for the DepositPreauth authorization failed. +TER +authorizedDepositPreauth( + ApplyView const& view, + STVector256 const& ctx, + AccountID const& dst); // Sort credentials array, return empty set if there are duplicates std::set> @@ -69,6 +80,15 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j); } // namespace credentials +// Check expired credentials and for credentials maching DomainID of the ledger +// object +TER +verifyValidDomain( + ApplyView& view, + AccountID const& account, + uint256 domainID, + beast::Journal j); + // Check expired credentials and for existing DepositPreauth ledger object TER verifyDepositPreauth( diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 31773166d4a..b1f0cc41831 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -220,64 +220,38 @@ applyCreate( auto const ammKeylet = keylet::amm(amount.issue(), amount2.issue()); // Mitigate same account exists possibility - auto const ammAccount = [&]() -> Expected { - std::uint16_t constexpr maxAccountAttempts = 256; - for (auto p = 0; p < maxAccountAttempts; ++p) - { - auto const ammAccount = - ammAccountID(p, sb.info().parentHash, ammKeylet.key); - if (!sb.read(keylet::account(ammAccount))) - return ammAccount; - } - return Unexpected(tecDUPLICATE); - }(); - + auto const maybeAccount = + createPseudoAccount(sb, ammKeylet.key, PseudoAccountOwnerType::AMM); // AMM account already exists (should not happen) - if (!ammAccount) + if (!maybeAccount) { - JLOG(j_.error()) << "AMM Instance: AMM already exists."; - return {ammAccount.error(), false}; + JLOG(j_.error()) << "AMM Instance: failed to create pseudo account."; + return {maybeAccount.error(), false}; } + auto& account = *maybeAccount; + auto const accountId = (*account)[sfAccount]; // LP Token already exists. (should not happen) auto const lptIss = ammLPTIssue( - amount.issue().currency, amount2.issue().currency, *ammAccount); - if (sb.read(keylet::line(*ammAccount, lptIss))) + amount.issue().currency, amount2.issue().currency, accountId); + if (sb.read(keylet::line(accountId, lptIss))) { JLOG(j_.error()) << "AMM Instance: LP Token already exists."; return {tecDUPLICATE, false}; } - // Create AMM Root Account. - auto sleAMMRoot = std::make_shared(keylet::account(*ammAccount)); - sleAMMRoot->setAccountID(sfAccount, *ammAccount); - sleAMMRoot->setFieldAmount(sfBalance, STAmount{}); - std::uint32_t const seqno{ - ctx_.view().rules().enabled(featureDeletableAccounts) - ? ctx_.view().seq() - : 1}; - sleAMMRoot->setFieldU32(sfSequence, seqno); - // Ignore reserves requirement, disable the master key, allow default - // rippling (AMM LPToken can be used in payments and offer crossing but - // not as a token in another AMM), and enable deposit authorization to - // prevent payments into AMM. // Note, that the trustlines created by AMM have 0 credit limit. // This prevents shifting the balance between accounts via AMM, // or sending unsolicited LPTokens. This is a desired behavior. // A user can only receive LPTokens through affirmative action - // either an AMMDeposit, TrustSet, crossing an offer, etc. - sleAMMRoot->setFieldU32( - sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); - // Link the root account and AMM object - sleAMMRoot->setFieldH256(sfAMMID, ammKeylet.key); - sb.insert(sleAMMRoot); // Calculate initial LPT balance. auto const lpTokens = ammLPTokens(amount, amount2, lptIss); // Create ltAMM auto ammSle = std::make_shared(ammKeylet); - ammSle->setAccountID(sfAccount, *ammAccount); + ammSle->setAccountID(sfAccount, accountId); ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); auto const& [issue1, issue2] = std::minmax(amount.issue(), amount2.issue()); ammSle->setFieldIssue(sfAsset, STIssue{sfAsset, issue1}); @@ -287,14 +261,7 @@ applyCreate( ctx_.view(), ammSle, account_, lptIss, ctx_.tx[sfTradingFee]); // Add owner directory to link the root account and AMM object. - if (auto const page = sb.dirInsert( - keylet::ownerDir(*ammAccount), - ammSle->key(), - describeOwnerDir(*ammAccount))) - { - ammSle->setFieldU64(sfOwnerNode, *page); - } - else + if (auto ter = dirLink(sb, accountId, ammSle); ter) { JLOG(j_.debug()) << "AMM Instance: failed to insert owner dir"; return {tecDIR_FULL, false}; @@ -302,7 +269,7 @@ applyCreate( sb.insert(ammSle); // Send LPT to LP. - auto res = accountSend(sb, *ammAccount, account_, lpTokens, ctx_.journal); + auto res = accountSend(sb, accountId, account_, lpTokens, ctx_.journal); if (res != tesSUCCESS) { JLOG(j_.debug()) << "AMM Instance: failed to send LPT " << lpTokens; @@ -313,7 +280,7 @@ applyCreate( if (auto const res = accountSend( sb, account_, - *ammAccount, + accountId, amount, ctx_.journal, WaiveTransferFee::Yes)) @@ -322,7 +289,7 @@ applyCreate( if (!isXRP(amount)) { if (SLE::pointer sleRippleState = - sb.peek(keylet::line(*ammAccount, amount.issue())); + sb.peek(keylet::line(accountId, amount.issue())); !sleRippleState) return tecINTERNAL; else @@ -351,7 +318,7 @@ applyCreate( return {res, false}; } - JLOG(j_.debug()) << "AMM Instance: success " << *ammAccount << " " + JLOG(j_.debug()) << "AMM Instance: success " << accountId << " " << ammKeylet.key << " " << lpTokens << " " << amount << " " << amount2; auto addOrderBook = diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index f6e5f6f3e3f..ba2071704d1 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -212,7 +212,7 @@ CashCheck::preclaim(PreclaimContext const& ctx) if (!sleTrustLine) { // We can only create a trust line if the issuer does not - // have requireAuth set. + // have lsfRequireAuth set. return tecNO_AUTH; } diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index d39492c1085..bd09e7292b9 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -329,7 +329,8 @@ AccountRootsNotDeleted::finalize( // A successful AccountDelete or AMMDelete MUST delete exactly // one account root. if ((tx.getTxnType() == ttACCOUNT_DELETE || - tx.getTxnType() == ttAMM_DELETE) && + tx.getTxnType() == ttAMM_DELETE || + tx.getTxnType() == ttVAULT_DELETE) && result == tesSUCCESS) { if (accountsDeleted_ == 1) @@ -488,6 +489,7 @@ LedgerEntryTypesMatch::visitEntry( case ltMPTOKEN: case ltCREDENTIAL: case ltPERMISSIONED_DOMAIN: + case ltVAULT: break; default: invalidTypeAdded_ = true; @@ -905,6 +907,7 @@ ValidNewAccountRoot::finalize( // From this point on we know exactly one account was created. if ((tx.getTxnType() == ttPAYMENT || tx.getTxnType() == ttAMM_CREATE || + tx.getTxnType() == ttVAULT_CREATE || tx.getTxnType() == ttXCHAIN_ADD_CLAIM_ATTESTATION || tx.getTxnType() == ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION) && result == tesSUCCESS) @@ -921,9 +924,7 @@ ValidNewAccountRoot::finalize( return true; } - JLOG(j.fatal()) << "Invariant failed: account root created " - "by a non-Payment, by an unsuccessful transaction, " - "or by AMM"; + JLOG(j.fatal()) << "Invariant failed: account root created illegally"; return false; } @@ -1311,28 +1312,30 @@ ValidMPTIssuance::finalize( { if (result == tesSUCCESS) { - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE) + if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE || + tx.getTxnType() == ttVAULT_CREATE) { if (mptIssuancesCreated_ == 0) { - JLOG(j.fatal()) << "Invariant failed: MPT issuance creation " + JLOG(j.fatal()) << "Invariant failed: transaction " "succeeded without creating a MPT issuance"; } else if (mptIssuancesDeleted_ != 0) { - JLOG(j.fatal()) << "Invariant failed: MPT issuance creation " + JLOG(j.fatal()) << "Invariant failed: transaction " "succeeded while removing MPT issuances"; } else if (mptIssuancesCreated_ > 1) { - JLOG(j.fatal()) << "Invariant failed: MPT issuance creation " + JLOG(j.fatal()) << "Invariant failed: transaction " "succeeded but created multiple issuances"; } return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; } - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY) + if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY || + tx.getTxnType() == ttVAULT_DELETE) { if (mptIssuancesDeleted_ == 0) { @@ -1353,7 +1356,8 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; } - if (tx.getTxnType() == ttMPTOKEN_AUTHORIZE) + if (tx.getTxnType() == ttMPTOKEN_AUTHORIZE || + tx.getTxnType() == ttVAULT_DEPOSIT) { bool const submittedByIssuer = tx.isFieldPresent(sfHolder); @@ -1379,7 +1383,7 @@ ValidMPTIssuance::finalize( return false; } else if ( - !submittedByIssuer && + !submittedByIssuer && (tx.getTxnType() != ttVAULT_DEPOSIT) && (mptokensCreated_ + mptokensDeleted_ != 1)) { // if the holder submitted this tx, then a mptoken must be diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 8042c9c6982..ce7b1a9dc74 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -141,7 +141,7 @@ MPTokenAuthorize::authorize( beast::Journal journal, MPTAuthorizeArgs const& args) { - auto const sleAcct = view.peek(keylet::account(args.account)); + auto const sleAcct = view.peek(keylet::account(args.accountID)); if (!sleAcct) return tecINTERNAL; @@ -150,19 +150,22 @@ MPTokenAuthorize::authorize( // `holderID` is NOT used if (!args.holderID) { + auto const mptokenKey = + keylet::mptoken(args.mptIssuanceID, args.accountID); + auto sleMpt = view.peek(mptokenKey); + // When a holder wants to unauthorize/delete a MPT, the ledger must // - delete mptokenKey from owner directory // - delete the MPToken if (args.flags & tfMPTUnauthorize) { - auto const mptokenKey = - keylet::mptoken(args.mptIssuanceID, args.account); - auto const sleMpt = view.peek(mptokenKey); - if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0) - return tecINTERNAL; + if (!sleMpt) + return tecOBJECT_NOT_FOUND; + if ((*sleMpt)[sfMPTAmount] != 0) + return tecHAS_OBLIGATIONS; if (!view.dirRemove( - keylet::ownerDir(args.account), + keylet::ownerDir(args.accountID), (*sleMpt)[sfOwnerNode], sleMpt->key(), false)) @@ -191,27 +194,19 @@ MPTokenAuthorize::authorize( if (args.priorBalance < reserveCreate) return tecINSUFFICIENT_RESERVE; - auto const mptokenKey = - keylet::mptoken(args.mptIssuanceID, args.account); - - auto const ownerNode = view.dirInsert( - keylet::ownerDir(args.account), - mptokenKey, - describeOwnerDir(args.account)); - - if (!ownerNode) - return tecDIR_FULL; - - auto mptoken = std::make_shared(mptokenKey); - (*mptoken)[sfAccount] = args.account; - (*mptoken)[sfMPTokenIssuanceID] = args.mptIssuanceID; - (*mptoken)[sfFlags] = 0; - (*mptoken)[sfOwnerNode] = *ownerNode; - view.insert(mptoken); + if (sleMpt) + return tecDUPLICATE; + sleMpt = std::make_shared(mptokenKey); - // Update owner count. + if (auto ter = dirLink(view, args.accountID, sleMpt)) + return ter; adjustOwnerCount(view, sleAcct, 1, journal); + (*sleMpt)[sfAccount] = args.accountID; + (*sleMpt)[sfMPTokenIssuanceID] = args.mptIssuanceID; + (*sleMpt)[sfFlags] = 0; + view.insert(sleMpt); + return tesSUCCESS; } @@ -223,7 +218,7 @@ MPTokenAuthorize::authorize( // If the account that submitted this tx is the issuer of the MPT // Note: `account_` is issuer's account // `holderID` is holder's account - if (args.account != (*sleMptIssuance)[sfIssuer]) + if (args.accountID != (*sleMptIssuance)[sfIssuer]) return tecINTERNAL; auto const sleMpt = @@ -259,7 +254,7 @@ MPTokenAuthorize::doApply() ctx_.journal, {.priorBalance = mPriorBalance, .mptIssuanceID = tx[sfMPTokenIssuanceID], - .account = account_, + .accountID = account_, .flags = tx.getFlags(), .holderID = tx[~sfHolder]}); } diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 79dc1734b5b..e869ac43dc3 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -27,10 +27,10 @@ namespace ripple { struct MPTAuthorizeArgs { XRPAmount const& priorBalance; - uint192 const& mptIssuanceID; - AccountID const& account; - std::uint32_t flags; - std::optional holderID; + MPTID const& mptIssuanceID; + AccountID const& accountID; + std::uint32_t flags{}; + std::optional holderID{}; }; class MPTokenAuthorize : public Transactor diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index 1297a918e1d..8e0d1ef6c95 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -67,7 +67,7 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) return preflight2(ctx); } -TER +Expected MPTokenIssuanceCreate::create( ApplyView& view, beast::Journal journal, @@ -75,14 +75,10 @@ MPTokenIssuanceCreate::create( { auto const acct = view.peek(keylet::account(args.account)); if (!acct) - return tecINTERNAL; - - if (args.priorBalance < - view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) - return tecINSUFFICIENT_RESERVE; + return Unexpected(tecINTERNAL); - auto const mptIssuanceKeylet = - keylet::mptIssuance(args.sequence, args.account); + auto mptId = makeMptID(args.sequence, args.account); + auto const mptIssuanceKeylet = keylet::mptIssuance(mptId); // create the MPTokenIssuance { @@ -92,7 +88,7 @@ MPTokenIssuanceCreate::create( describeOwnerDir(args.account)); if (!ownerNode) - return tecDIR_FULL; + return Unexpected(tecDIR_FULL); auto mptIssuance = std::make_shared(mptIssuanceKeylet); (*mptIssuance)[sfFlags] = args.flags & ~tfUniversal; @@ -113,30 +109,38 @@ MPTokenIssuanceCreate::create( if (args.metadata) (*mptIssuance)[sfMPTokenMetadata] = *args.metadata; + if (args.domainId) + (*mptIssuance)[sfDomainID] = *args.domainId; + view.insert(mptIssuance); } // Update owner count. adjustOwnerCount(view, acct, 1, journal); - return tesSUCCESS; + return mptId; } TER MPTokenIssuanceCreate::doApply() { auto const& tx = ctx_.tx; - return create( - ctx_.view(), - ctx_.journal, - {.priorBalance = mPriorBalance, - .account = account_, + + auto const acct = view().peek(keylet::account(account_)); + if (mPriorBalance < view().fees().accountReserve((*acct)[sfOwnerCount] + 1)) + return tecINSUFFICIENT_RESERVE; + + auto result = create( + view(), + j_, + {.account = account_, .sequence = tx.getSeqProxy().value(), .flags = tx.getFlags(), .maxAmount = tx[~sfMaximumAmount], .assetScale = tx[~sfAssetScale], .transferFee = tx[~sfTransferFee], .metadata = tx[~sfMPTokenMetadata]}); + return result ? tesSUCCESS : result.error(); } } // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h index 1346c3e31d7..a91490e6a2e 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -21,19 +21,21 @@ #define RIPPLE_TX_MPTOKENISSUANCECREATE_H_INCLUDED #include +#include +#include namespace ripple { struct MPTCreateArgs { - XRPAmount const& priorBalance; AccountID const& account; std::uint32_t sequence; - std::uint32_t flags; - std::optional maxAmount; - std::optional assetScale; - std::optional transferFee; - std::optional const& metadata; + std::uint32_t flags = 0; + std::optional maxAmount{}; + std::optional assetScale{}; + std::optional transferFee{}; + std::optional const& metadata{}; + std::optional domainId{}; }; class MPTokenIssuanceCreate : public Transactor @@ -51,7 +53,7 @@ class MPTokenIssuanceCreate : public Transactor TER doApply() override; - static TER + static Expected create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args); }; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index a0f0b9d8602..0e642fca13c 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -63,22 +63,39 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx) } TER -MPTokenIssuanceDestroy::doApply() +MPTokenIssuanceDestroy::destroy( + ApplyView& view, + beast::Journal journal, + MPTDestroyArgs const& args) { - auto const mpt = - view().peek(keylet::mptIssuance(ctx_.tx[sfMPTokenIssuanceID])); - if (account_ != mpt->getAccountID(sfIssuer)) - return tecINTERNAL; + auto const mpt = view.peek(keylet::mptIssuance(args.issuanceID)); + if (!mpt) + return tecOBJECT_NOT_FOUND; - if (!view().dirRemove( - keylet::ownerDir(account_), (*mpt)[sfOwnerNode], mpt->key(), false)) - return tefBAD_LEDGER; + if ((*mpt)[sfIssuer] != args.account) + return tecNO_PERMISSION; + auto const& issuer = args.account; - view().erase(mpt); + if ((*mpt)[~sfOutstandingAmount] != 0) + return tecHAS_OBLIGATIONS; - adjustOwnerCount(view(), view().peek(keylet::account(account_)), -1, j_); + if (!view.dirRemove( + keylet::ownerDir(issuer), (*mpt)[sfOwnerNode], mpt->key(), false)) + return tefBAD_LEDGER; + view.erase(mpt); + adjustOwnerCount(view, view.peek(keylet::account(issuer)), -1, journal); return tesSUCCESS; } +TER +MPTokenIssuanceDestroy::doApply() +{ + return destroy( + view(), + j_, + {.account = ctx_.tx[sfAccount], + .issuanceID = ctx_.tx[sfMPTokenIssuanceID]}); +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h index 69abb99feb0..278f77b7b55 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h @@ -24,6 +24,12 @@ namespace ripple { +struct MPTDestroyArgs +{ + AccountID const& account; + MPTID issuanceID; +}; + class MPTokenIssuanceDestroy : public Transactor { public: @@ -39,6 +45,12 @@ class MPTokenIssuanceDestroy : public Transactor static TER preclaim(PreclaimContext const& ctx); + static TER + destroy( + ApplyView& view, + beast::Journal journal, + MPTDestroyArgs const& args); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 1ed3bacbbd8..f8d016ec651 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -486,8 +486,7 @@ Payment::doApply() // - can't send between holders // - holder can send back to issuer // - issuer can send to holder - if (isFrozen(view(), account_, mptIssue) || - isFrozen(view(), dstAccountID, mptIssue)) + if (isAnyFrozen(view(), account_, dstAccountID, mptIssue)) return tecLOCKED; // Get the rate for a payment between the holders. diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp new file mode 100644 index 00000000000..d3b7bdd052c --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -0,0 +1,169 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +VaultClawback::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureSingleAssetVault)) + return temDISABLED; + + if (auto const ter = preflight1(ctx)) + return ter; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + AccountID const issuer = ctx.tx[sfAccount]; + AccountID const holder = ctx.tx[sfHolder]; + + if (issuer == holder) + return temMALFORMED; + + auto const amount = ctx.tx[~sfAmount]; + if (amount) + { + // Note, zero amount is valid, it means "all". It is also the default. + if (*amount < beast::zero) + return temBAD_AMOUNT; + else if (isXRP(amount->asset())) + return temMALFORMED; + else if (amount->asset().getIssuer() != issuer) + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +VaultClawback::preclaim(PreclaimContext const& ctx) +{ + auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID])); + if (!vault) + return tecOBJECT_NOT_FOUND; + + auto account = ctx.tx[sfAccount]; + auto const issuer = ctx.view.read(keylet::account(account)); + if (!issuer) + return tefINTERNAL; // Transactor should have enforced this + + Asset const asset = vault->at(sfAsset); + if (asset.native()) + return tecNO_PERMISSION; // Cannot clawback XRP. + else if (asset.getIssuer() != account) + return tecNO_PERMISSION; // Only issuers can clawback. + + auto const amount = ctx.tx[~sfAmount]; + if (amount && asset != amount->asset()) + return tecWRONG_ASSET; + + std::uint32_t const issuerFlags = issuer->getFieldU32(sfFlags); + + // If AllowTrustLineClawback is not set or NoFreeze is set, return no + // permission + if (!(issuerFlags & lsfAllowTrustLineClawback) || + (issuerFlags & lsfNoFreeze)) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + +TER +VaultClawback::doApply() +{ + auto const& tx = ctx_.tx; + auto const vault = view().peek(keylet::vault(tx[sfVaultID])); + if (!vault) + return tecOBJECT_NOT_FOUND; + + auto const mptIssuanceID = (*vault)[sfMPTokenIssuanceID]; + auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + return tefINTERNAL; + + Asset const asset = vault->at(sfAsset); + STAmount const amount = [&]() -> STAmount { + auto const maybeAmount = tx[~sfAmount]; + if (maybeAmount) + return *maybeAmount; + return {sfAmount, asset, 0}; + }(); + XRPL_ASSERT( + amount.asset() == asset, + "ripple::VaultClawback::doApply : matching asset"); + + AccountID holder = tx[sfHolder]; + STAmount assets, shares; + if (amount == beast::zero) + { + Asset share = *(*vault)[sfMPTokenIssuanceID]; + shares = accountHolds( + view(), + holder, + share, + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_); + assets = sharesToAssetsWithdraw(vault, sleIssuance, shares); + } + else + { + assets = amount; + shares = assetsToSharesWithdraw(vault, sleIssuance, assets); + } + + // Clamp to maximum. + Number maxAssets = *vault->at(sfAssetAvailable); + if (assets > maxAssets) + { + assets = maxAssets; + shares = assetsToSharesWithdraw(vault, sleIssuance, assets); + } + + if (shares == beast::zero) + return tecINSUFFICIENT_FUNDS; + + vault->at(sfAssetTotal) -= assets; + vault->at(sfAssetAvailable) -= assets; + view().update(vault); + + auto const& vaultAccount = vault->at(sfAccount); + // Transfer shares from holder to vault. + if (auto ter = accountSend(view(), holder, vaultAccount, shares, j_)) + return ter; + + // Transfer assets from vault to issuer. + if (auto ter = accountSend(view(), vaultAccount, account_, assets, j_)) + return ter; + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/VaultClawback.h b/src/xrpld/app/tx/detail/VaultClawback.h new file mode 100644 index 00000000000..65f01646867 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultClawback.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_VAULTCLAWBACK_H_INCLUDED +#define RIPPLE_TX_VAULTCLAWBACK_H_INCLUDED + +#include + +namespace ripple { + +class VaultClawback : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit VaultClawback(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp new file mode 100644 index 00000000000..48b5ff4df4d --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -0,0 +1,200 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +VaultCreate::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureSingleAssetVault)) + return temDISABLED; + + if (ctx.tx.isFieldPresent(sfDomainID) && + !ctx.rules.enabled(featurePermissionedDomains)) + return temDISABLED; + + if (auto const ter = preflight1(ctx)) + return ter; + + if (ctx.tx.getFlags() & tfVaultCreateMask) + return temINVALID_FLAG; + + if (auto const data = ctx.tx[~sfData]) + { + if (data->empty() || data->length() > maxDataPayloadLength) + return temMALFORMED; + } + + if (auto const data = ctx.tx[~sfWithdrawalPolicy]) + { + // Enforce valid withdrawal policy + if (*data != vaultStrategyFirstComeFirstServe) + return temMALFORMED; + } + + if (auto const domain = ctx.tx[~sfDomainID]) + { + if (!ctx.rules.enabled(featurePermissionedDomains)) + return temDISABLED; + else if (*domain == beast::zero) + return temMALFORMED; + else if ((ctx.tx.getFlags() & tfVaultPrivate) == 0) + return temMALFORMED; // DomainID only allowed on private vaults + } + + if (auto const metadata = ctx.tx[~sfMPTokenMetadata]) + { + if (metadata->length() == 0 || + metadata->length() > maxMPTokenMetadataLength) + return temMALFORMED; + } + + return preflight2(ctx); +} + +XRPAmount +VaultCreate::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + // One reserve increment is typically much greater than one base fee. + return view.fees().increment; +} + +TER +VaultCreate::preclaim(PreclaimContext const& ctx) +{ + auto asset = ctx.tx[sfAsset]; + auto account = ctx.tx[sfAccount]; + + if (asset.holds()) + { + auto mptID = asset.get().getMptID(); + auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); + if (!issuance) + return tecNO_ENTRY; + if (issuance->getFlags() & lsfMPTLocked) + return tecLOCKED; + if ((issuance->getFlags() & lsfMPTCanTransfer) == 0) + return tecNO_AUTH; + } + + // Cannot create Vault for an Asset frozen for the vault owner + if (isFrozen(ctx.view, account, asset)) + return tecFROZEN; + + if (auto const domain = ctx.tx[~sfDomainID]) + { + auto const sleDomain = + ctx.view.read(keylet::permissionedDomain(*domain)); + if (!sleDomain) + return tecNO_ENTRY; + } + + return tesSUCCESS; +} + +TER +VaultCreate::doApply() +{ + // All return codes in `doApply` must be `tec`, `ter`, or `tes`. + // As we move checks into `preflight` and `preclaim`, + // we can consider downgrading them to `tef` or `tem`. + + auto const& tx = ctx_.tx; + auto const& ownerId = account_; + auto sequence = tx.getSeqValue(); + + auto owner = view().peek(keylet::account(ownerId)); + auto vault = std::make_shared(keylet::vault(ownerId, sequence)); + + if (auto ter = dirLink(view(), ownerId, vault)) + return ter; + // Should the next 3 lines be folded into `dirLink`? + adjustOwnerCount(view(), owner, 1, j_); + auto ownerCount = owner->at(sfOwnerCount); + if (mPriorBalance < view().fees().accountReserve(ownerCount)) + return tecINSUFFICIENT_RESERVE; + + auto maybePseudo = createPseudoAccount( + view(), vault->key(), PseudoAccountOwnerType::Vault); + if (!maybePseudo) + return maybePseudo.error(); + auto& pseudo = *maybePseudo; + auto pseudoId = pseudo->at(sfAccount); + + if (auto ter = + addEmptyHolding(view(), pseudoId, mPriorBalance, tx[sfAsset], j_)) + return ter; + + auto txFlags = tx.getFlags(); + std::uint32_t mptFlags = 0; + if ((txFlags & tfVaultShareNonTransferable) == 0) + mptFlags |= (lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer); + if (txFlags & tfVaultPrivate) + mptFlags |= lsfMPTRequireAuth; + + auto maybeShare = MPTokenIssuanceCreate::create( + view(), + j_, + { + // The operator-> gives the underlying STAccount, + // whose value function returns a const&. + .account = pseudoId->value(), + .sequence = 1, + .flags = mptFlags, + .metadata = tx[~sfMPTokenMetadata], + .domainId = tx[~sfDomainID], + }); + if (!maybeShare) + return maybeShare.error(); + auto& share = *maybeShare; + + vault->at(sfFlags) = txFlags & tfVaultPrivate; + vault->at(sfSequence) = sequence; + vault->at(sfOwner) = ownerId; + vault->at(sfAccount) = pseudoId; + vault->at(sfAsset) = tx[sfAsset]; + // Leave default values for AssetTotal and AssetAvailable, both zero. + if (auto value = tx[~sfAssetMaximum]) + vault->at(sfAssetMaximum) = *value; + vault->at(sfMPTokenIssuanceID) = share; + if (auto value = tx[~sfData]) + vault->at(sfData) = *value; + // Required field, default to vaultStrategyFirstComeFirstServe + if (auto value = tx[~sfWithdrawalPolicy]) + vault->at(sfWithdrawalPolicy) = *value; + else + vault->at(sfWithdrawalPolicy) = vaultStrategyFirstComeFirstServe; + // No `LossUnrealized`. + view().insert(vault); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/VaultCreate.h b/src/xrpld/app/tx/detail/VaultCreate.h new file mode 100644 index 00000000000..55556446295 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultCreate.h @@ -0,0 +1,51 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_VAULTCREATE_H_INCLUDED +#define RIPPLE_TX_VAULTCREATE_H_INCLUDED + +#include + +namespace ripple { + +class VaultCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit VaultCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/VaultDelete.cpp b/src/xrpld/app/tx/detail/VaultDelete.cpp new file mode 100644 index 00000000000..5a7893696de --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultDelete.cpp @@ -0,0 +1,110 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +VaultDelete::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureSingleAssetVault)) + return temDISABLED; + + if (auto const ter = preflight1(ctx)) + return ter; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + return preflight2(ctx); +} + +TER +VaultDelete::preclaim(PreclaimContext const& ctx) +{ + auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID])); + if (!vault) + return tecOBJECT_NOT_FOUND; + + if (vault->at(sfOwner) != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + + if (vault->at(sfAssetAvailable) != 0) + return tecHAS_OBLIGATIONS; + + return tesSUCCESS; +} + +TER +VaultDelete::doApply() +{ + auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + if (!vault) + return tefINTERNAL; + + // Destroy the asset holding. + if (auto ter = removeEmptyHolding( + view(), vault->at(sfAccount), vault->at(sfAsset), j_)) + return ter; + + // Destroy the share issuance. + if (auto ter = MPTokenIssuanceDestroy::destroy( + view(), + j_, + // The operator-> gives the underlying STAccount, + // whose value function returns a const&. + {.account = vault->at(sfAccount)->value(), + .issuanceID = vault->at(sfMPTokenIssuanceID)})) + return ter; + + // The psuedo-account's directory should have been deleted already. + auto const& pseudoID = vault->at(sfAccount); + if (view().peek(keylet::ownerDir(pseudoID))) + return tecHAS_OBLIGATIONS; + + // Destroy the pseudo-account. + view().erase(view().peek(keylet::account(pseudoID))); + + // Remove the vault from its owner's directory. + auto const ownerID = vault->at(sfOwner); + if (!view().dirRemove( + keylet::ownerDir(ownerID), + vault->at(sfOwnerNode), + vault->key(), + false)) + return tefBAD_LEDGER; + auto const owner = view().peek(keylet::account(ownerID)); + if (!owner) + return tefBAD_LEDGER; + adjustOwnerCount(view(), owner, -1, j_); + + // Destroy the vault. + view().erase(vault); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/VaultDelete.h b/src/xrpld/app/tx/detail/VaultDelete.h new file mode 100644 index 00000000000..2b77e844698 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultDelete.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_VAULTDELETE_H_INCLUDED +#define RIPPLE_TX_VAULTDELETE_H_INCLUDED + +#include + +namespace ripple { + +class VaultDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit VaultDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp new file mode 100644 index 00000000000..cf7b41f4367 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -0,0 +1,174 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +VaultDeposit::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureSingleAssetVault)) + return temDISABLED; + + if (auto const ter = preflight1(ctx)) + return ter; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + return preflight2(ctx); +} + +TER +VaultDeposit::preclaim(PreclaimContext const& ctx) +{ + auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID])); + if (!vault) + return tecOBJECT_NOT_FOUND; + + auto account = ctx.tx[sfAccount]; + auto const assets = ctx.tx[sfAmount]; + auto asset = vault->at(sfAsset); + if (assets.asset() != asset) + return tecWRONG_ASSET; + + // Cannot deposit inside Vault an Asset frozen for the depositor + if (isFrozen(ctx.view, account, asset)) + return tecFROZEN; + + auto const share = MPTIssue(vault->at(sfMPTokenIssuanceID)); + if (isFrozen(ctx.view, account, share)) + return tecFROZEN; + + if (vault->getFlags() == tfVaultPrivate && account != vault->at(sfOwner)) + { + auto const err = requireAuth( + ctx.view, MPTIssue(vault->at(sfMPTokenIssuanceID)), account); + return err; + + // The above will perform authorization check based on DomainID stored + // in MPTokenIssuance. Had this been a regular MPToken, it would also + // allow use of authorization granted by the issuer explicitly, but + // Vault does not have an MPT issuer (instead it uses pseudo-account). + // + // If we passed the above check then we also need to do similar check + // inside doApply(), in order to check for expired credentials. + } + + return tesSUCCESS; +} + +TER +VaultDeposit::doApply() +{ + auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + if (!vault) + return tecOBJECT_NOT_FOUND; + + auto const assets = ctx_.tx[sfAmount]; + Asset const& asset = vault->at(sfAsset); + + if (accountHolds( + view(), + account_, + asset, + FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahZERO_IF_UNAUTHORIZED, + j_) < assets) + { + return tecINSUFFICIENT_FUNDS; + } + + // Make sure the depositor can hold shares. + auto const mptIssuanceID = (*vault)[sfMPTokenIssuanceID]; + auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + return tefINTERNAL; + + auto const& vaultAccount = vault->at(sfAccount); + + MPTIssue const mptIssue(mptIssuanceID); + if (vault->getFlags() == tfVaultPrivate) + { + if (auto const err = enforceMPTokenAuthorization( + ctx_.view(), mptIssue, account_, mPriorBalance, j_); + !isTesSuccess(err)) + return err; + } + else + { + // No authorization needed, but must ensure there is MPToken + auto sleMpt = view().read(keylet::mptoken(mptIssuanceID, account_)); + if (!sleMpt && account_ != vaultAccount) + { + if (auto const err = MPTokenAuthorize::authorize( + view(), + ctx_.journal, + {.priorBalance = mPriorBalance, + // The operator-> gives the underlying STUInt192 + // whose value function returns a const&. + .mptIssuanceID = mptIssuanceID->value(), + .accountID = account_}); + !isTesSuccess(err)) + return err; + } + } + + // Compute exchange before transferring any amounts. + auto const shares = assetsToSharesDeposit(vault, sleIssuance, assets); + XRPL_ASSERT( + shares.asset() != assets.asset(), + "ripple::VaultDeposit::doApply : assets are not shares"); + + vault->at(sfAssetTotal) += assets; + vault->at(sfAssetAvailable) += assets; + view().update(vault); + + // A deposit must not push the vault over its limit. + auto const maximum = *vault->at(sfAssetMaximum); + if (maximum != 0 && *vault->at(sfAssetTotal) > maximum) + return tecLIMIT_EXCEEDED; + + // Transfer assets from depositor to vault. + if (auto ter = accountSend(view(), account_, vaultAccount, assets, j_)) + return ter; + + // Transfer shares from vault to depositor. + if (auto ter = accountSend(view(), vaultAccount, account_, shares, j_)) + return ter; + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/VaultDeposit.h b/src/xrpld/app/tx/detail/VaultDeposit.h new file mode 100644 index 00000000000..50515ce3d87 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultDeposit.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_VAULTDEPOSIT_H_INCLUDED +#define RIPPLE_TX_VAULTDEPOSIT_H_INCLUDED + +#include + +namespace ripple { + +class VaultDeposit : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit VaultDeposit(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp new file mode 100644 index 00000000000..2f9b9cf6454 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -0,0 +1,120 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +VaultSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureSingleAssetVault)) + return temDISABLED; + + if (ctx.tx.isFieldPresent(sfDomainID) && + !ctx.rules.enabled(featurePermissionedDomains)) + return temDISABLED; + + if (auto const ter = preflight1(ctx)) + return ter; + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + if (auto const data = ctx.tx[~sfData]) + { + if (data->empty() || data->length() > maxDataPayloadLength) + return temMALFORMED; + } + + auto const domain = ctx.tx[~sfDomainID]; + if (domain) + { + if (!ctx.rules.enabled(featurePermissionedDomains)) + return temDISABLED; + else if (*domain == beast::zero) + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +VaultSet::preclaim(PreclaimContext const& ctx) +{ + auto const id = ctx.tx[sfVaultID]; + auto const sle = ctx.view.read(keylet::vault(id)); + if (!sle) + return tecOBJECT_NOT_FOUND; + + // Assert that submitter is the Owner. + if (ctx.tx[sfAccount] != sle->at(sfOwner)) + return tecNO_PERMISSION; + + // We can only set domain if private flag was originally set + if (auto const domain = ctx.tx[~sfDomainID]) + { + if ((sle->getFlags() & tfVaultPrivate) == 0) + return tecINVALID_DOMAIN; + } + + return tesSUCCESS; +} + +TER +VaultSet::doApply() +{ + // All return codes in `doApply` must be `tec`, `ter`, or `tes`. + // As we move checks into `preflight` and `preclaim`, + // we can consider downgrading them to `tef` or `tem`. + + auto const& tx = ctx_.tx; + + // Update existing object. + auto vault = view().peek({ltVAULT, tx[sfVaultID]}); + if (!vault) + return tecOBJECT_NOT_FOUND; + + // Update mutable flags and fields if given. + if (tx.isFieldPresent(sfData)) + vault->at(sfData) = tx[sfData]; + if (tx.isFieldPresent(sfAssetMaximum)) + { + if (tx[sfAssetMaximum] < *vault->at(sfAssetTotal)) + return tecLIMIT_EXCEEDED; + vault->at(sfAssetMaximum) = tx[sfAssetMaximum]; + } + if (tx.isFieldPresent(sfDomainID)) + { + // In VaultSet::preclaim we enforce that tfVaultPrivate must have been + // set in the vault. We currently do not support making such a vault + // public (i.e. removal of tfVaultPrivate flag) + vault->setFieldH256(sfDomainID, tx.getFieldH256(sfDomainID)); + } + + view().update(vault); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/VaultSet.h b/src/xrpld/app/tx/detail/VaultSet.h new file mode 100644 index 00000000000..f16aa6c2840 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultSet.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_VAULTSET_H_INCLUDED +#define RIPPLE_TX_VAULTSET_H_INCLUDED + +#include + +namespace ripple { + +class VaultSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit VaultSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp new file mode 100644 index 00000000000..c0b133acfdb --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -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 + +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +VaultWithdraw::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureSingleAssetVault)) + return temDISABLED; + + if (auto const ter = preflight1(ctx)) + return ter; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + return preflight2(ctx); +} + +TER +VaultWithdraw::preclaim(PreclaimContext const& ctx) +{ + auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID])); + if (!vault) + return tecOBJECT_NOT_FOUND; + + // Enforce valid withdrawal policy + if (vault->at(sfWithdrawalPolicy) != vaultStrategyFirstComeFirstServe) + return tefINTERNAL; + + auto const assets = ctx.tx[sfAmount]; + auto const asset = vault->at(sfAsset); + auto const share = vault->at(sfMPTokenIssuanceID); + if (assets.asset() != asset && assets.asset() != share) + return tecWRONG_ASSET; + + auto const account = ctx.tx[sfAccount]; + // Cannot withdraw from a Vault an Asset frozen for the account + if (isFrozen(ctx.view, account, asset)) + return tecFROZEN; + + if (isFrozen(ctx.view, account, share)) + return tecFROZEN; + + return tesSUCCESS; +} + +TER +VaultWithdraw::doApply() +{ + auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + if (!vault) + return tecOBJECT_NOT_FOUND; + + auto const mptIssuanceID = (*vault)[sfMPTokenIssuanceID]; + auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + return tefINTERNAL; + + // Note, we intentionally do not check lsfVaultPrivate flag on the Vault. If + // you have a share in the vault, it means you were at some point authorized + // to deposit into it, and this means you are also indefinitely authorized + // to withdraw from it. + + auto amount = ctx_.tx[sfAmount]; + auto const asset = vault->at(sfAsset); + auto const share = MPTIssue(mptIssuanceID); + STAmount shares, assets; + if (amount.asset() == asset) + { + // Fixed assets, variable shares. + assets = amount; + shares = assetsToSharesWithdraw(vault, sleIssuance, assets); + } + else if (amount.asset() == share) + { + // Fixed shares, variable assets. + shares = amount; + assets = sharesToAssetsWithdraw(vault, sleIssuance, shares); + } + else + return tefINTERNAL; + + if (accountHolds( + view(), + account_, + share, + FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahZERO_IF_UNAUTHORIZED, + j_) < shares) + { + return tecINSUFFICIENT_FUNDS; + } + + // The vault must have enough assets on hand. The vault may hold assets that + // it has already pledged. That is why we look at AssetAvailable instead of + // the pseudo-account balance. + if (*vault->at(sfAssetAvailable) < assets) + return tecINSUFFICIENT_FUNDS; + + vault->at(sfAssetTotal) -= assets; + vault->at(sfAssetAvailable) -= assets; + view().update(vault); + + auto const& vaultAccount = vault->at(sfAccount); + // Transfer shares from depositor to vault. + if (auto ter = accountSend(view(), account_, vaultAccount, shares, j_)) + return ter; + + // Transfer assets from vault to depositor. + if (auto ter = accountSend(view(), vaultAccount, account_, assets, j_)) + return ter; + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.h b/src/xrpld/app/tx/detail/VaultWithdraw.h new file mode 100644 index 00000000000..0b713d403bf --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultWithdraw.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_VAULTWITHDRAW_H_INCLUDED +#define RIPPLE_TX_VAULTWITHDRAW_H_INCLUDED + +#include + +namespace ripple { + +class VaultWithdraw : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit VaultWithdraw(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 95cc3521a91..8a659554ef7 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -60,6 +60,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index aca3f9fa6d8..b92539e77de 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -39,8 +39,6 @@ #include #include -#include - namespace ripple { enum class WaiveTransferFee : bool { No = false, Yes }; @@ -91,6 +89,9 @@ isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); [[nodiscard]] bool isGlobalFrozen(ReadView const& view, Asset const& asset); +[[nodiscard]] bool +isVaultPseudoAccountFrozen(ReadView const& view, MPTIssue const& mptShare); + [[nodiscard]] bool isIndividualFrozen( ReadView const& view, @@ -153,6 +154,49 @@ isFrozen(ReadView const& view, AccountID const& account, Asset const& asset) asset.value()); } +[[nodiscard]] bool +isAnyFrozen( + ReadView const& view, + AccountID const& account1, + AccountID const& account2, + MPTIssue const& mptIssue); + +/* +We do not have use a case for these (yet ?) + +[[nodiscard]] bool +isAnyFrozen( + ReadView const& view, + AccountID const& account1, + AccountID const& account2, + Currency const& currency, + AccountID const& issuer); + +[[nodiscard]] inline bool +isAnyFrozen( + ReadView const& view, + AccountID const& account1, + AccountID const& account2, + Issue const& issue) +{ + return isAnyFrozen(view, account1, account2, issue.currency, issue.account); +} + +[[nodiscard]] inline bool +isAnyFrozen( + ReadView const& view, + AccountID const& account1, + AccountID const& account2, + Asset const& asset) +{ + return std::visit( + [&](auto const& issue) { + return isAnyFrozen(view, account1, account2, issue); + }, + asset.value()); +} +*/ + [[nodiscard]] bool isDeepFrozen( ReadView const& view, @@ -196,6 +240,15 @@ accountHolds( AuthHandling zeroIfUnauthorized, beast::Journal j); +[[nodiscard]] STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + Asset const& asset, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j); + // Returns the amount an account can spend of the currency type saDefault, or // returns saDefault if this account is the issuer of the currency in // question. Should be used in favor of accountHolds when questioning how much @@ -434,6 +487,18 @@ dirNext( [[nodiscard]] std::function describeOwnerDir(AccountID const& account); +[[nodiscard]] TER +dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr& object); + +// Which of the owner-object fields should we set: sfAMMID, sfVaultID +enum class PseudoAccountOwnerType : int { AMM, Vault }; + +[[nodiscard]] Expected, TER> +createPseudoAccount( + ApplyView& view, + uint256 const& pseudoOwnerKey, + PseudoAccountOwnerType type); + // VFALCO NOTE Both STAmount parameters should just // be "Amount", a unit-less number. // @@ -469,6 +534,30 @@ trustDelete( AccountID const& uHighAccountID, beast::Journal j); +/** Create the structures necessary for an account to hold an asset. + * + * If the asset is: + * - XRP: Do nothing. + * - IOU: Check that the asset is not globally frozen, + * and create a trust line (with limit 0). + * - MPT: Check that the asset is not globally locked, + * and create an MPToken. + */ +[[nodiscard]] TER +addEmptyHolding( + ApplyView& view, + AccountID const& account, + XRPAmount priorBalance, + Asset const& asset, + beast::Journal journal); + +[[nodiscard]] TER +removeEmptyHolding( + ApplyView& view, + AccountID const& account, + Asset const& asset, + beast::Journal journal); + /** Delete an offer. Requirements: @@ -539,18 +628,51 @@ transferXRP( STAmount const& amount, beast::Journal j); +struct TokenDescriptor +{ + std::shared_ptr token; + std::shared_ptr issuance; +}; + +[[nodiscard]] TER +requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); + /** Check if the account lacks required authorization. + * * Return tecNO_AUTH or tecNO_LINE if it does * and tesSUCCESS otherwise. */ [[nodiscard]] TER requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); + +/** Check if the account lacks required authorization. + * + * This will also check for expired credentials. If it is called directly + * from preclaim, the user should convert result tecEXPIRED to tesSUCCESS and + * proceed to also check permissions with verifyValidDomain inside doApply. + * This will ensure that any expired credentials are deleted. + */ [[nodiscard]] TER requireAuth( ReadView const& view, MPTIssue const& mptIssue, AccountID const& account); +/** Check if the account lacks required authorization. + * + * Called from doApply - it will check for expired (and delete if found any) + * credentials maching DomainID set in MPTIssuance. Must be called if + * requireAuth(...MPTIssue...) check passed in preclaim. Will create MPToken + * if needed, on the basis of non-expired credentals found. + */ +[[nodiscard]] TER +enforceMPTokenAuthorization( + ApplyView& view, + MPTIssue const& mptIssue, + AccountID const& account, + XRPAmount const& priorBalance, + beast::Journal j); + /** Check if the destination account is allowed * to receive MPT. Return tecNO_AUTH if it doesn't * and tesSUCCESS otherwise. @@ -596,6 +718,33 @@ deleteAMMTrustLine( std::optional const& ammAccountID, beast::Journal j); +// From the perspective of a vault, +// return the number of shares to give the depositor +// when they deposit a fixed amount of assets. +[[nodiscard]] STAmount +assetsToSharesDeposit( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& assets); + +// From the perspective of a vault, +// return the number of shares to demand from the depositor +// when they ask to withdraw a fixed amount of assets. +[[nodiscard]] STAmount +assetsToSharesWithdraw( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& assets); + +// From the perspective of a vault, +// return the number of assets to give the depositor +// when they redeem a fixed amount of shares. +[[nodiscard]] STAmount +sharesToAssetsWithdraw( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& shares); + } // namespace ripple #endif diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 85abf7fc62c..6bb20343e70 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -18,16 +18,28 @@ //============================================================================== #include +// TODO: Move the helper out of the `app` module. +#include +#include #include +#include #include #include #include #include #include +#include +#include +#include #include #include +#include +#include +#include #include #include +#include +#include namespace ripple { @@ -264,7 +276,56 @@ isFrozen( MPTIssue const& mptIssue) { return isGlobalFrozen(view, mptIssue) || - isIndividualFrozen(view, account, mptIssue); + isIndividualFrozen(view, account, mptIssue) || + isVaultPseudoAccountFrozen(view, mptIssue); +} + +[[nodiscard]] bool +isAnyFrozen( + ReadView const& view, + AccountID const& account1, + AccountID const& account2, + MPTIssue const& mptIssue) +{ + return isGlobalFrozen(view, mptIssue) || + isIndividualFrozen(view, account1, mptIssue) || + isIndividualFrozen(view, account2, mptIssue) || + isVaultPseudoAccountFrozen(view, mptIssue); +} + +bool +isVaultPseudoAccountFrozen(ReadView const& view, MPTIssue const& mptShare) +{ + if (!view.rules().enabled(featureSingleAssetVault)) + return false; + + auto const mptIssuance = + view.read(keylet::mptIssuance(mptShare.getMptID())); + XRPL_ASSERT( + mptIssuance, + "ripple::isVaultPseudoAccountFrozen : non-null MPTokenIssuance"); + if (!mptIssuance) + return false; + + auto const issuer = mptIssuance->getAccountID(sfIssuer); + auto const mptIssuer = view.read(keylet::account(issuer)); + XRPL_ASSERT( + mptIssuer, + "ripple::isVaultPseudoAccountFrozen : non-null MPToken issuer"); + if (!mptIssuer) + return false; + + if (!mptIssuer->isFieldPresent(sfVaultID)) + return false; // not a Vault pseudo-account + + auto const vault = + view.read(keylet::vault(mptIssuer->getFieldH256(sfVaultID))); + XRPL_ASSERT(vault, "ripple::isVaultPseudoAccountFrozen : non-null vault"); + if (!vault) + return false; + + Asset const asset{vault->at(sfAsset)}; + return isFrozen(view, issuer, asset); } bool @@ -411,6 +472,7 @@ accountHolds( auto const sleMpt = view.read(keylet::mptoken(mptIssue.getMptID(), account)); + if (!sleMpt) amount.clear(mptIssue); else if ( @@ -420,9 +482,16 @@ accountHolds( { amount = STAmount{mptIssue, sleMpt->getFieldU64(sfMPTAmount)}; - // only if auth check is needed, as it needs to do an additional read - // operation - if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED) + // Only if auth check is needed, as it needs to do an additional read + // operation. Note featureSingleAssetVault will affect error codes. + if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED && + view.rules().enabled(featureSingleAssetVault)) + { + if (auto const err = requireAuth(view, mptIssue, account); + !isTesSuccess(err)) + amount.clear(mptIssue); + } + else if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED) { auto const sleIssuance = view.read(keylet::mptIssuance(mptIssue.getMptID())); @@ -438,6 +507,29 @@ accountHolds( return amount; } +[[nodiscard]] STAmount +accountHolds( + ReadView const& view, + AccountID const& account, + Asset const& asset, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + return std::visit( + [&](auto const& value) { + if constexpr (std::is_same_v< + std::remove_cvref_t, + Issue>) + { + return accountHolds(view, account, value, zeroIfFrozen, j); + } + return accountHolds( + view, account, value, zeroIfFrozen, zeroIfUnauthorized, j); + }, + asset.value()); +} + STAmount accountFunds( ReadView const& view, @@ -929,6 +1021,61 @@ describeOwnerDir(AccountID const& account) }; } +TER +dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr& object) +{ + auto const page = view.dirInsert( + keylet::ownerDir(owner), object->key(), describeOwnerDir(owner)); + if (!page) + return tecDIR_FULL; + object->setFieldU64(sfOwnerNode, *page); + return tesSUCCESS; +} + +Expected, TER> +createPseudoAccount( + ApplyView& view, + uint256 const& pseudoOwnerKey, + PseudoAccountOwnerType type) +{ + AccountID accountId; + for (auto i = 0;; ++i) + { + if (i >= 256) + return Unexpected(tecDUPLICATE); + ripesha_hasher rsh; + auto const hash = sha512Half(i, view.info().parentHash, pseudoOwnerKey); + rsh(hash.data(), hash.size()); + accountId = static_cast(rsh); + if (!view.read(keylet::account(accountId))) + break; + } + + // Create pseudo-account. + auto account = std::make_shared(keylet::account(accountId)); + account->setAccountID(sfAccount, accountId); + account->setFieldAmount(sfBalance, STAmount{}); + std::uint32_t const seqno{ + view.rules().enabled(featureDeletableAccounts) ? view.seq() : 1}; + account->setFieldU32(sfSequence, seqno); + // Ignore reserves requirement, disable the master key, allow default + // rippling, and enable deposit authorization to prevent payments into + // pseudo-account. + account->setFieldU32( + sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + // Link the pseudo-account with its owner object. + if (type == PseudoAccountOwnerType::AMM) + account->setFieldH256(sfAMMID, pseudoOwnerKey); + else if (type == PseudoAccountOwnerType::Vault) + account->setFieldH256(sfVaultID, pseudoOwnerKey); + else + UNREACHABLE("ripple::createPseudoAccount : unknown owner key type"); + + view.insert(account); + + return account; +} + TER trustCreate( ApplyView& view, @@ -1088,6 +1235,121 @@ trustDelete( return tesSUCCESS; } +[[nodiscard]] TER +addEmptyHolding( + ApplyView& view, + AccountID const& accountID, + XRPAmount priorBalance, + Asset const& asset, + beast::Journal journal) +{ + if (asset.holds()) + { + auto const& issue = asset.get(); + // Every account can hold XRP. + if (issue.native()) + return tesSUCCESS; + + auto const& issuerId = issue.getIssuer(); + auto const& currency = issue.currency; + if (isGlobalFrozen(view, issuerId)) + return tecFROZEN; + + auto const& srcId = issuerId; + auto const& dstId = accountID; + auto const high = srcId > dstId; + auto const index = keylet::line(srcId, dstId, currency); + auto const sle = view.peek(keylet::account(accountID)); + return trustCreate( + view, + high, + srcId, + dstId, + index.key, + sle, + /*auth=*/false, + /*noRipple=*/true, + /*freeze=*/false, + /*deepFreeze*/ false, + /*balance=*/STAmount{Issue{currency, noAccount()}}, + /*limit=*/STAmount{Issue{currency, dstId}}, + /*qualityIn=*/0, + /*qualityOut=*/0, + journal); + } + + if (asset.holds()) + { + auto const& mptIssue = asset.get(); + auto const& mptID = mptIssue.getMptID(); + auto const mpt = view.peek(keylet::mptIssuance(mptID)); + if (mpt->getFlags() & lsfMPTLocked) + return tecLOCKED; + return MPTokenAuthorize::authorize( + view, + journal, + {.priorBalance = priorBalance, + .mptIssuanceID = mptID, + .accountID = accountID}); + } + + // Should be unreachable. + return tecINTERNAL; +} + +[[nodiscard]] TER +removeEmptyHolding( + ApplyView& view, + AccountID const& accountID, + Asset const& asset, + beast::Journal journal) +{ + if (asset.holds()) + { + auto const& issue = asset.get(); + if (issue.native()) + { + auto const sle = view.read(keylet::account(accountID)); + if (!sle) + return tecINTERNAL; + auto const balance = sle->getFieldAmount(sfBalance); + if (balance.xrp() != 0) + return tecHAS_OBLIGATIONS; + return tesSUCCESS; + } + + // `asset` is an IOU. + auto const line = view.peek(keylet::line(accountID, issue)); + if (!line) + return tecOBJECT_NOT_FOUND; + if (line->at(sfBalance)->iou() != beast::zero) + return tecHAS_OBLIGATIONS; + return trustDelete( + view, + line, + line->at(sfLowLimit)->getIssuer(), + line->at(sfHighLimit)->getIssuer(), + journal); + } + + if (asset.holds()) + { + auto const& mptIssue = asset.get(); + auto const& mptID = mptIssue.getMptID(); + // `MPTokenAuthorize::authorize` asserts that the balance is 0. + return MPTokenAuthorize::authorize( + view, + journal, + {.priorBalance = {}, + .mptIssuanceID = mptID, + .accountID = accountID, + .flags = tfMPTUnauthorize}); + } + + // Should be unreachable. + return tecINTERNAL; +} + TER offerDelete(ApplyView& view, std::shared_ptr const& sle, beast::Journal j) { @@ -1462,6 +1724,7 @@ rippleCreditMPT( STAmount const& saAmount, beast::Journal j) { + // Do not check MPT authorization here - it must have been checked earlier auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); auto const issuer = saAmount.getIssuer(); auto sleIssuance = view.peek(mptID); @@ -1511,6 +1774,7 @@ rippleCreditMPT( else return tecNO_AUTH; } + return tesSUCCESS; } @@ -1923,24 +2187,141 @@ requireAuth( { auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); auto const sleIssuance = view.read(mptID); - if (!sleIssuance) return tecOBJECT_NOT_FOUND; auto const mptIssuer = sleIssuance->getAccountID(sfIssuer); // issuer is always "authorized" - if (mptIssuer == account) + if (mptIssuer == account) // Issuer won't have MPToken return tesSUCCESS; auto const mptokenID = keylet::mptoken(mptID.key, account); auto const sleToken = view.read(mptokenID); + // Note, this is not amendment-gated because we do not want to maintain in + // this file the list of all the amendments which can write to this field. + // Without additional amendments this field is always empty. + auto const maybeDomainID = sleIssuance->at(~sfDomainID); + if (!maybeDomainID) + { + // if account has no MPToken, fail + if (!sleToken) + return tecNO_AUTH; + + // mptoken must be authorized if issuance enabled requireAuth + if (sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth && + !(sleToken->getFlags() & lsfMPTAuthorized)) + return tecNO_AUTH; + + return tesSUCCESS; + } + + // err = tefINTERNAL | tecINVALID_DOMAIN | tecNO_PERMISSION | tecEXPIRED + if (auto const err = + credentials::validDomain(view, *maybeDomainID, account); + !isTesSuccess(err)) + return err; + + // We are authorized by permissioned domain. + return tesSUCCESS; +} + +[[nodiscard]] TER +enforceMPTokenAuthorization( + ApplyView& view, + MPTIssue const& mptIssue, + AccountID const& account, + XRPAmount const& priorBalance, // for MPToken authorization + beast::Journal j) +{ + auto const mptIssuanceID = mptIssue.getMptID(); + auto const sleIssuance = view.read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + return tefINTERNAL; // Should have called requireAuth earlier + + if (account == sleIssuance->at(sfIssuer)) + return tesSUCCESS; // Won't create MPToken for the token issuer + + auto sleToken = view.read(keylet::mptoken(mptIssuanceID, account)); + bool const domainOwned = + (sleToken && (sleToken->getFlags() & lsfMPTDomainCheck)); + + bool authorizedByDomain = false; + if (domainOwned || sleToken == nullptr) + { + // We check DomainID if: + // 1. Token not found or + // 2. Token found and has lsfMPTDomainCheck flag + auto const maybeDomainID = sleIssuance->at(~sfDomainID); + authorizedByDomain = maybeDomainID.has_value() && + verifyValidDomain(view, account, *maybeDomainID, j) == tesSUCCESS; + } + + if (!authorizedByDomain && sleToken == nullptr) + { + // Intentionally empty. This could be either of: + // 1. DomainID not set in MPTokenIssuance or + // 2. account has no matching and accepted credentials or + // 3. only expired credentials (removed in verifyValidDomain) + // Either way, move to check at the end of this function + } + else if (!authorizedByDomain && domainOwned) + { + auto sleMpt = view.peek(keylet::mptoken(mptIssuanceID, account)); + XRPL_ASSERT(sleMpt, "ripple::verifyAuth : non-null old MPToken"); + std::uint32_t const flags = sleMpt->getFieldU32(sfFlags); + // Remove lsfMPTAuthorized flag + sleMpt->setFieldU32(sfFlags, flags & ~lsfMPTAuthorized); + view.update(sleMpt); + + sleToken = nullptr; + } + else if (authorizedByDomain && sleToken) + { + XRPL_ASSERT( + sleToken->getFlags() & lsfMPTDomainCheck, + "ripple::verifyAuth : MPToken owned by domain"); + + if ((sleToken->getFlags() & lsfMPTAuthorized) == 0) + { + // Must set lsfMPTAuthorized, we found new credentials + auto sleMpt = view.peek(keylet::mptoken(mptIssuanceID, account)); + XRPL_ASSERT(sleMpt, "ripple::verifyAuth : non-null new MPToken"); + std::uint32_t const flags = sleMpt->getFieldU32(sfFlags); + sleMpt->setFieldU32(sfFlags, flags | lsfMPTAuthorized); + view.update(sleMpt); + + sleToken = sleMpt; // with lsfMPTAuthorized + } + } + else if (authorizedByDomain && sleToken == nullptr) + { + // Create MPToken with the lsfMPTDomainCheck flag set + if (auto const err = MPTokenAuthorize::authorize( + view, + j, + { + .priorBalance = priorBalance, + .mptIssuanceID = mptIssuanceID, + .accountID = account, + .flags = 0, + }); + !isTesSuccess(err)) + return err; + + auto sleMpt = view.peek(keylet::mptoken(mptIssuanceID, account)); + XRPL_ASSERT(sleMpt, "ripple::verifyAuth : found new MPToken"); + std::uint32_t const flags = sleMpt->getFieldU32(sfFlags); + sleMpt->setFieldU32( + sfFlags, flags | lsfMPTDomainCheck | lsfMPTAuthorized); + view.update(sleMpt); + + sleToken = sleMpt; // with lsfMPTAuthorized + } - // if account has no MPToken, fail if (!sleToken) return tecNO_AUTH; - // mptoken must be authorized if issuance enabled requireAuth if (sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth && !(sleToken->getFlags() & lsfMPTAuthorized)) return tecNO_AUTH; @@ -2123,4 +2504,61 @@ rippleCredit( saAmount.asset().value()); } +[[nodiscard]] STAmount +assetsToSharesDeposit( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& assets) +{ + XRPL_ASSERT( + assets.asset() == vault->at(sfAsset), + "ripple::assetsToSharesDeposit : assets and vault match"); + Number assetTotal = vault->at(sfAssetTotal); + STAmount shares{ + vault->at(sfMPTokenIssuanceID), static_cast(assets)}; + if (assetTotal == 0) + return shares; + Number shareTotal = issuance->at(sfOutstandingAmount); + shares = shareTotal * (assets / assetTotal); + return shares; +} + +[[nodiscard]] STAmount +assetsToSharesWithdraw( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& assets) +{ + XRPL_ASSERT( + assets.asset() == vault->at(sfAsset), + "ripple::assetsToSharesWithdraw : assets and vault match"); + Number assetTotal = vault->at(sfAssetTotal); + assetTotal -= vault->at(sfLossUnrealized); + STAmount shares{vault->at(sfMPTokenIssuanceID)}; + if (assetTotal == 0) + return shares; + Number shareTotal = issuance->at(sfOutstandingAmount); + shares = shareTotal * (assets / assetTotal); + return shares; +} + +[[nodiscard]] STAmount +sharesToAssetsWithdraw( + std::shared_ptr const& vault, + std::shared_ptr const& issuance, + STAmount const& shares) +{ + XRPL_ASSERT( + shares.asset() == vault->at(sfMPTokenIssuanceID), + "ripple::sharesToAssetsWithdraw : shares and vault match"); + Number assetTotal = vault->at(sfAssetTotal); + assetTotal -= vault->at(sfLossUnrealized); + STAmount assets{vault->at(sfAsset)}; + if (assetTotal == 0) + return assets; + Number shareTotal = issuance->at(sfOutstandingAmount); + assets = assetTotal * (shares / shareTotal); + return assets; +} + } // namespace ripple