From e514de76ed4eb9b1ad55c94170a56d03e52562e0 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Tue, 20 May 2025 19:06:41 +0100 Subject: [PATCH] Add single asset vault (XLS-65d) (#5224) - Specification: XRPLF/XRPL-Standards#239 - Amendment: `SingleAssetVault` - Implements a vault feature used to store a fungible asset (XRP, IOU, or MPT, but not NFT) and to receive shares in the vault (an MPT) in exchange. - A vault can be private or public. - A private vault can use permissioned domains, subject to the `PermissionedDomains` amendment. - Shares can be exchanged back into asset with `VaultWithdraw`. - Permissions on the asset in the vault are transitively applied on shares in the vault. - Issuer of the asset in the vault can clawback with `VaultClawback`. - Extended `MPTokenIssuance` with `DomainID`, used by the permissioned domain on the vault shares. Co-authored-by: John Freeman --- include/xrpl/json/json_value.h | 11 + include/xrpl/protocol/AMMCore.h | 8 - include/xrpl/protocol/Asset.h | 16 +- include/xrpl/protocol/IOUAmount.h | 1 - include/xrpl/protocol/Indexes.h | 9 + include/xrpl/protocol/LedgerFormats.h | 3 + include/xrpl/protocol/MPTAmount.h | 3 - include/xrpl/protocol/MPTIssue.h | 7 +- include/xrpl/protocol/Protocol.h | 10 + include/xrpl/protocol/SField.h | 1 - include/xrpl/protocol/STAmount.h | 23 +- include/xrpl/protocol/STBase.h | 12 +- include/xrpl/protocol/STIssue.h | 4 + include/xrpl/protocol/STNumber.h | 20 + include/xrpl/protocol/STObject.h | 59 +- include/xrpl/protocol/STTx.h | 4 + include/xrpl/protocol/TER.h | 16 + include/xrpl/protocol/TxFlags.h | 6 + include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/ledger_entries.macro | 57 +- include/xrpl/protocol/detail/sfields.macro | 7 + .../xrpl/protocol/detail/transactions.macro | 43 + include/xrpl/protocol/jss.h | 5 +- src/libxrpl/json/json_value.cpp | 13 + src/libxrpl/protocol/AMMCore.cpp | 12 - src/libxrpl/protocol/Asset.cpp | 14 +- src/libxrpl/protocol/Indexes.cpp | 7 + src/libxrpl/protocol/Keylet.cpp | 2 +- src/libxrpl/protocol/MPTIssue.cpp | 6 - src/libxrpl/protocol/STAmount.cpp | 90 +- src/libxrpl/protocol/STNumber.cpp | 100 + src/libxrpl/protocol/STParsedJSON.cpp | 15 + src/libxrpl/protocol/STTx.cpp | 6 + src/libxrpl/protocol/STVar.cpp | 4 + src/libxrpl/protocol/TER.cpp | 4 + src/test/app/AMM_test.cpp | 73 +- src/test/app/Credentials_test.cpp | 28 +- src/test/app/MPToken_test.cpp | 43 +- src/test/app/Vault_test.cpp | 3085 +++++++++++++++++ src/test/basics/IOUAmount_test.cpp | 5 + src/test/jtx/Env.h | 1 + src/test/jtx/amount.h | 55 +- src/test/jtx/basic_prop.h | 2 + src/test/jtx/credentials.h | 10 + src/test/jtx/impl/Env.cpp | 1 - src/test/jtx/impl/vault.cpp | 104 + src/test/jtx/mpt.h | 5 +- src/test/jtx/vault.h | 109 + src/test/ledger/Invariants_test.cpp | 84 +- src/test/protocol/STNumber_test.cpp | 194 ++ src/test/rpc/Transaction_test.cpp | 3 +- src/xrpld/app/misc/CredentialHelpers.cpp | 122 +- src/xrpld/app/misc/CredentialHelpers.h | 32 +- src/xrpld/app/misc/NetworkOPs.cpp | 2 +- src/xrpld/app/tx/detail/AMMCreate.cpp | 72 +- src/xrpld/app/tx/detail/CashCheck.cpp | 2 +- src/xrpld/app/tx/detail/Clawback.cpp | 7 +- src/xrpld/app/tx/detail/CreateCheck.cpp | 9 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 2 +- src/xrpld/app/tx/detail/Escrow.cpp | 10 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 61 +- src/xrpld/app/tx/detail/InvariantCheck.h | 2 + src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 21 +- src/xrpld/app/tx/detail/MPTokenAuthorize.h | 6 +- .../app/tx/detail/MPTokenIssuanceCreate.cpp | 47 +- .../app/tx/detail/MPTokenIssuanceCreate.h | 18 +- .../app/tx/detail/MPTokenIssuanceDestroy.cpp | 2 +- src/xrpld/app/tx/detail/PayChan.cpp | 10 +- src/xrpld/app/tx/detail/Payment.cpp | 12 +- src/xrpld/app/tx/detail/SetTrust.cpp | 44 +- src/xrpld/app/tx/detail/VaultClawback.cpp | 239 ++ src/xrpld/app/tx/detail/VaultClawback.h | 48 + src/xrpld/app/tx/detail/VaultCreate.cpp | 244 ++ src/xrpld/app/tx/detail/VaultCreate.h | 51 + src/xrpld/app/tx/detail/VaultDelete.cpp | 189 + src/xrpld/app/tx/detail/VaultDelete.h | 48 + src/xrpld/app/tx/detail/VaultDeposit.cpp | 283 ++ src/xrpld/app/tx/detail/VaultDeposit.h | 48 + src/xrpld/app/tx/detail/VaultSet.cpp | 197 ++ src/xrpld/app/tx/detail/VaultSet.h | 48 + src/xrpld/app/tx/detail/VaultWithdraw.cpp | 258 ++ src/xrpld/app/tx/detail/VaultWithdraw.h | 48 + src/xrpld/app/tx/detail/applySteps.cpp | 6 + src/xrpld/ledger/View.h | 280 +- src/xrpld/ledger/detail/View.cpp | 595 +++- src/xrpld/net/detail/RPCCall.cpp | 19 + src/xrpld/rpc/detail/Handler.cpp | 1 + src/xrpld/rpc/detail/RPCHelpers.cpp | 1 + src/xrpld/rpc/detail/RPCHelpers.h | 2 + src/xrpld/rpc/handlers/AccountObjects.cpp | 4 +- src/xrpld/rpc/handlers/Handlers.h | 2 + src/xrpld/rpc/handlers/LedgerEntry.cpp | 35 + src/xrpld/rpc/handlers/VaultInfo.cpp | 114 + 93 files changed, 7257 insertions(+), 385 deletions(-) create mode 100644 src/test/app/Vault_test.cpp create mode 100644 src/test/jtx/impl/vault.cpp create mode 100644 src/test/jtx/vault.h create mode 100644 src/xrpld/app/tx/detail/VaultClawback.cpp create mode 100644 src/xrpld/app/tx/detail/VaultClawback.h create mode 100644 src/xrpld/app/tx/detail/VaultCreate.cpp create mode 100644 src/xrpld/app/tx/detail/VaultCreate.h create mode 100644 src/xrpld/app/tx/detail/VaultDelete.cpp create mode 100644 src/xrpld/app/tx/detail/VaultDelete.h create mode 100644 src/xrpld/app/tx/detail/VaultDeposit.cpp create mode 100644 src/xrpld/app/tx/detail/VaultDeposit.h create mode 100644 src/xrpld/app/tx/detail/VaultSet.cpp create mode 100644 src/xrpld/app/tx/detail/VaultSet.h create mode 100644 src/xrpld/app/tx/detail/VaultWithdraw.cpp create mode 100644 src/xrpld/app/tx/detail/VaultWithdraw.h create mode 100644 src/xrpld/rpc/handlers/VaultInfo.cpp diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index 3431ab7744..2e815b79f2 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -20,11 +20,13 @@ #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). @@ -216,6 +218,7 @@ public: Value(UInt value); Value(double value); Value(char const* 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 @@ -365,6 +368,8 @@ public: */ Value& operator[](StaticString const& key); + Value const& + operator[](StaticString const& key) const; /// Return the member named key if it exist, defaultValue otherwise. Value @@ -436,6 +441,12 @@ private: 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==(Value const&, Value const&); diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index 32988af5fc..442f24d878 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 0d12cd4058..4438106738 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 @@ public: void setJson(Json::Value& jv) const; + STAmount + operator()(Number const&) const; + bool native() const { @@ -114,6 +119,14 @@ public: 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/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index 6895ed08ae..a27069e37b 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -28,7 +28,6 @@ #include #include -#include namespace ripple { diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 979a994c10..57c8727ae6 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -334,6 +334,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 e2ac5bd071..3edd656213 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -191,6 +191,9 @@ enum LedgerSpecificFlags { // ltCREDENTIAL lsfAccepted = 0x00010000, + + // ltVAULT + lsfVaultPrivate = 0x00010000, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 244d683915..419450eeb9 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 028051ab1a..d1c757337e 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -42,8 +42,11 @@ public: 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 041b53d6cb..49bad8a076 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -116,6 +116,16 @@ 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; + +/** Maximum recursion depth for vault shares being put as an asset inside + * another vault; counted from 0 */ +std::uint8_t constexpr maxAssetCheckDepth = 5; + /** A ledger index. */ using LedgerIndex = std::uint32_t; diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 04d4dc82fc..777cfa02ba 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 518edacf0b..c66d273254 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -153,6 +153,12 @@ public: 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 @@ public: STAmount& operator=(XRPAmount const& amount); + STAmount& + operator=(Number const&); + //-------------------------------------------------------------------------- // // Modification @@ -268,7 +277,7 @@ public: 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 8d0aaabe48..eec9a97987 100644 --- a/include/xrpl/protocol/STBase.h +++ b/include/xrpl/protocol/STBase.h @@ -92,6 +92,16 @@ struct JsonOptions } }; +template + requires requires(T const& t) { + { t.getJson(JsonOptions::none) } -> std::convertible_to; + } +Json::Value +to_json(T const& t) +{ + return t.getJson(JsonOptions::none); +} + namespace detail { class STVar; } @@ -157,7 +167,7 @@ public: 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 c729854e1b..9fe61f32cd 100644 --- a/include/xrpl/protocol/STIssue.h +++ b/include/xrpl/protocol/STIssue.h @@ -37,6 +37,7 @@ public: using value_type = Asset; STIssue() = default; + STIssue(STIssue const& rhs) = default; explicit STIssue(SerialIter& sit, SField const& name); @@ -45,6 +46,9 @@ public: explicit STIssue(SField const& name); + STIssue& + operator=(STIssue const& rhs) = default; + template TIss const& get() const; diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h index c0fce572c8..3c1f73e4e6 100644 --- a/include/xrpl/protocol/STNumber.h +++ b/include/xrpl/protocol/STNumber.h @@ -63,6 +63,13 @@ public: 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 @@ private: 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 2efa828267..6cd083ef85 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -154,8 +154,7 @@ public: 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; @@ -484,9 +483,19 @@ private: 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_; @@ -495,9 +504,6 @@ protected: Proxy(STObject* st, TypedField const* f); - value_type - value() const; - T const* find() const; @@ -512,7 +518,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; @@ -538,6 +544,13 @@ public: operator value_type() const; + template + friend bool + operator==(U const& lhs, STObject::ValueProxy const& rhs) + { + return rhs.value() == lhs; + } + private: friend class STObject; @@ -545,7 +558,7 @@ private: }; template -class STObject::OptionalProxy : private Proxy +class STObject::OptionalProxy : public Proxy { private: using value_type = typename T::value_type; @@ -565,15 +578,6 @@ public: 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 */ @@ -717,6 +721,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 @@ -792,13 +810,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 8de2c8cc31..b00495bf76 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -102,6 +102,10 @@ public: 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 da3788cd6a..b87bc3f8a4 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -225,6 +225,8 @@ enum TERcodes : TERUnderlyingType { terQUEUED, // Transaction is being held in TxQ until fee drops terPRE_TICKET, // Ticket is not yet in ledger but might be on its way terNO_AMM, // AMM doesn't exist for the asset pair + terADDRESS_COLLISION, // Failed to allocate AccountID when trying to + // create a pseudo-account }; //------------------------------------------------------------------------------ @@ -265,6 +267,17 @@ enum TECcodes : TERUnderlyingType { // Otherwise, treated as terRETRY. // // DO NOT CHANGE THESE NUMBERS: They appear in ledger meta data. + // + // Note: + // tecNO_ENTRY is often used interchangeably with tecOBJECT_NOT_FOUND. + // While there does not seem to be a clear rule which to use when, the + // following guidance will help to keep errors consistent with the + // majority of (but not all) transaction types: + // - tecNO_ENTRY : cannot find the primary ledger object on which the + // transaction is being attempted + // - tecOBJECT_NOT_FOUND : cannot find the additional object(s) needed to + // complete the transaction + tecCLAIM = 100, tecPATH_PARTIAL = 101, tecUNFUNDED_ADD = 102, // Unused legacy code @@ -344,6 +357,9 @@ enum TECcodes : TERUnderlyingType { tecARRAY_TOO_LARGE = 191, tecLOCKED = 192, tecBAD_CREDENTIALS = 193, + tecWRONG_ASSET = 194, + tecLIMIT_EXCEEDED = 195, + tecPSEUDO_ACCOUNT = 196, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 7a600676f8..505000cfd6 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -233,6 +233,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/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 31b5c25d91..3be0fd426c 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -32,6 +32,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 66573eaf4a..a902b32026 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -165,7 +165,8 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({ {sfMintedNFTokens, soeDEFAULT}, {sfBurnedNFTokens, soeDEFAULT}, {sfFirstNFTokenSequence, soeOPTIONAL}, - {sfAMMID, soeOPTIONAL}, + {sfAMMID, soeOPTIONAL}, // pseudo-account designator + {sfVaultID, soeOPTIONAL}, // pseudo-account designator })) /** 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 */ @@ -472,6 +474,29 @@ LEDGER_ENTRY(ltDELEGATE, 0x0083, Delegate, delegate, ({ {sfPreviousTxnLgrSeq, soeREQUIRED}, })) +/** A ledger object representing a single asset vault. + + \sa keylet::mptoken + */ +LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfSequence, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfOwner, soeREQUIRED}, + {sfAccount, soeREQUIRED}, + {sfData, soeOPTIONAL}, + {sfAsset, soeREQUIRED}, + {sfAssetsTotal, soeREQUIRED}, + {sfAssetsAvailable, soeREQUIRED}, + {sfAssetsMaximum, soeDEFAULT}, + {sfLossUnrealized, soeREQUIRED}, + {sfShareMPTID, soeREQUIRED}, + {sfWithdrawalPolicy, soeREQUIRED}, + // no SharesTotal 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 e98709c8c3..63bc52de6a 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) @@ -155,6 +156,7 @@ TYPED_SFIELD(sfTakerGetsIssuer, UINT160, 4) // 192-bit (common) TYPED_SFIELD(sfMPTokenIssuanceID, UINT192, 1) +TYPED_SFIELD(sfShareMPTID, UINT192, 2) // 256-bit (common) TYPED_SFIELD(sfLedgerHash, UINT256, 1) @@ -192,9 +194,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(sfAssetsAvailable, NUMBER, 2) +TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3) +TYPED_SFIELD(sfAssetsTotal, 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 54f97f942f..0f614df692 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -471,6 +471,49 @@ TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, Delegation::notDelegatable, ({ {sfPermissions, soeREQUIRED}, })) +/** This transaction creates a single asset vault. */ +TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({ + {sfAsset, soeREQUIRED, soeMPTSupported}, + {sfAssetsMaximum, soeOPTIONAL}, + {sfMPTokenMetadata, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, // PermissionedDomainID + {sfWithdrawalPolicy, soeOPTIONAL}, + {sfData, soeOPTIONAL}, +})) + +/** This transaction updates a single asset vault. */ +TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, ({ + {sfVaultID, soeREQUIRED}, + {sfAssetsMaximum, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, // PermissionedDomainID + {sfData, soeOPTIONAL}, +})) + +/** This transaction deletes a single asset vault. */ +TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, Delegation::delegatable, ({ + {sfVaultID, soeREQUIRED}, +})) + +/** This transaction trades assets for shares with a vault. */ +TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, Delegation::delegatable, ({ + {sfVaultID, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, +})) + +/** This transaction trades shares for assets with a vault. */ +TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::delegatable, ({ + {sfVaultID, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, + {sfDestination, soeOPTIONAL}, +})) + +/** This transaction claws back tokens from a vault. */ +TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, ({ + {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 bb2ffa7bb0..de3560d7f9 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -95,10 +95,10 @@ 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(VoteSlots); // out: AMM Vote JSS(aborted); // out: InboundLedger @@ -449,6 +449,7 @@ JSS(node_reads_hit); // out: GetCounts JSS(node_reads_total); // out: GetCounts JSS(node_reads_duration_us); // out: GetCounts JSS(node_size); // out: server_info +JSS(nodes); // out: VaultInfo JSS(nodestore); // out: GetCounts JSS(node_writes); // out: GetCounts JSS(node_written_bytes); // out: GetCounts @@ -559,6 +560,7 @@ JSS(server_status); // out: NetworkOPs JSS(server_version); // out: NetworkOPs JSS(settle_delay); // out: AccountChannels JSS(severity); // in: LogLevel +JSS(shares); // out: VaultInfo JSS(signature); // out: NetworkOPs, ChannelAuthorize JSS(signature_verified); // out: ChannelVerify JSS(signing_key); // out: NetworkOPs @@ -684,6 +686,7 @@ JSS(validations); // out: AmendmentTableImpl JSS(validator_list_threshold); // out: ValidatorList JSS(validator_sites); // out: ValidatorSites JSS(value); // out: STAmount +JSS(vault_id); // in: VaultInfo JSS(version); // out: RPCVersion JSS(vetoed); // out: AmendmentTableImpl JSS(volume_a); // out: BookChanges diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index 86a8ed5aee..a1e0a04875 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -237,6 +237,13 @@ Value::Value(char const* 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( @@ -893,6 +900,12 @@ Value::operator[](StaticString const& key) return resolveReference(key, true); } +Value const& +Value::operator[](StaticString const& key) const +{ + return (*this)[key.c_str()]; +} + Value& Value::append(Value const& value) { diff --git a/src/libxrpl/protocol/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp index aa48827195..60660357ea 100644 --- a/src/libxrpl/protocol/AMMCore.cpp +++ b/src/libxrpl/protocol/AMMCore.cpp @@ -39,18 +39,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 d4a2fccb4a..104d627d81 100644 --- a/src/libxrpl/protocol/Asset.cpp +++ b/src/libxrpl/protocol/Asset.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -51,6 +52,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) { @@ -78,11 +85,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/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 8256c7a77c..2426092d13 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -95,6 +95,7 @@ enum class LedgerNameSpace : std::uint16_t { CREDENTIAL = 'D', PERMISSIONED_DOMAIN = 'm', DELEGATE = 'E', + VAULT = 'V', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -552,6 +553,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 8deb2d735a..a83186547c 100644 --- a/src/libxrpl/protocol/Keylet.cpp +++ b/src/libxrpl/protocol/Keylet.cpp @@ -37,7 +37,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 c8decb2b3f..9238b4302d 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -48,12 +48,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 f02042bc2c..02de5d4c58 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -309,6 +310,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; @@ -338,6 +340,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; @@ -865,75 +868,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; @@ -1025,36 +969,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 c0cdcccd6e..975fd5723b 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -18,12 +18,16 @@ //============================================================================== #include +#include #include #include #include #include #include +#include +#include + #include #include #include @@ -115,4 +119,100 @@ 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 = boost::lexical_cast(std::string(match[2])); + exponent = 0; + } + else + { + // integer and fraction + mantissa = boost::lexical_cast(match[2] + match[4]); + exponent = -(match[4].length()); + } + + if (match[5].matched) + { + // we have an exponent + if (match[6].matched && (match[6] == "-")) + exponent -= boost::lexical_cast(std::string(match[7])); + else + exponent += boost::lexical_cast(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 1437ed922b..bc9aad0a13 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -613,6 +614,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 a60f7325f0..7b6b4c1ee2 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -224,6 +224,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 3af0345c4e..24954c4add 100644 --- a/src/libxrpl/protocol/STVar.cpp +++ b/src/libxrpl/protocol/STVar.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -192,6 +193,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 8a3a6af0de..943a0e601b 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -123,6 +123,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(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -228,6 +231,7 @@ transResults() MAKE_ERROR(terQUEUED, "Held until escalated fee drops."), MAKE_ERROR(terPRE_TICKET, "Ticket is not yet in ledger."), MAKE_ERROR(terNO_AMM, "AMM doesn't exist for the asset pair."), + MAKE_ERROR(terADDRESS_COLLISION, "Failed to allocate an unique account address."), MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."), }; diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 87988315f4..e0b3dc1ec7 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -31,6 +32,7 @@ #include #include #include +#include #include @@ -54,11 +56,27 @@ private: using namespace jtx; - // XRP to IOU - testAMM([&](AMM& ammAlice, Env&) { - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0})); - }); + // XRP to IOU, with featureSingleAssetVault + testAMM( + [&](AMM& ammAlice, Env&) { + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0})); + }, + {}, + 0, + {}, + {supported_amendments() | featureSingleAssetVault}); + + // XRP to IOU, without featureSingleAssetVault + testAMM( + [&](AMM& ammAlice, Env&) { + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0})); + }, + {}, + 0, + {}, + {supported_amendments() - featureSingleAssetVault}); // IOU to IOU testAMM( @@ -7137,6 +7155,50 @@ private: }); } + void + testFailedPseudoAccount() + { + using namespace test::jtx; + + auto const testCase = [&](std::string suffix, FeatureBitset features) { + testcase("Failed pseudo-account allocation " + suffix); + Env env{*this, features}; + env.fund(XRP(30'000), gw, alice); + env.close(); + env(trust(alice, gw["USD"](30'000), 0)); + env(pay(gw, alice, USD(10'000))); + env.close(); + + STAmount amount = XRP(10'000); + STAmount amount2 = USD(10'000); + auto const keylet = keylet::amm(amount.issue(), amount2.issue()); + for (int i = 0; i < 256; ++i) + { + AccountID const accountId = + ripple::pseudoAccountAddress(*env.current(), keylet.key); + + env(pay(env.master.id(), accountId, XRP(1000)), + seq(autofill), + fee(autofill), + sig(autofill)); + } + + AMM ammAlice( + env, + alice, + amount, + amount2, + features[featureSingleAssetVault] ? ter{terADDRESS_COLLISION} + : ter{tecDUPLICATE}); + }; + + testCase( + "tecDUPLICATE", supported_amendments() - featureSingleAssetVault); + testCase( + "terADDRESS_COLLISION", + supported_amendments() | featureSingleAssetVault); + } + void run() override { @@ -7192,6 +7254,7 @@ private: testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback); testFixReserveCheckOnWithdrawal(all); testFixReserveCheckOnWithdrawal(all - fixAMMv1_2); + testFailedPseudoAccount(); } }; diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index 87946c13bb..fa6505e926 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -43,16 +43,6 @@ checkVL( return strHex(expected) == strHex(sle->getFieldVL(field)); } -static inline Keylet -credentialKeylet( - test::jtx::Account const& subject, - test::jtx::Account const& issuer, - std::string_view credType) -{ - return keylet::credential( - subject.id(), issuer.id(), Slice(credType.data(), credType.size())); -} - struct Credentials_test : public beast::unit_test::suite { void @@ -72,7 +62,7 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Create for subject."); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = credentials::keylet(subject, issuer, credType); env.fund(XRP(5000), subject, issuer, other); env.close(); @@ -150,7 +140,7 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Create for themself."); - auto const credKey = credentialKeylet(issuer, issuer, credType); + auto const credKey = credentials::keylet(issuer, issuer, credType); env(credentials::create(issuer, issuer, credType), credentials::uri(uri)); @@ -224,7 +214,7 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete issuer before accept"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = credentials::keylet(subject, issuer, credType); env(credentials::create(subject, issuer, credType)); env.close(); @@ -260,7 +250,7 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete issuer after accept"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = credentials::keylet(subject, issuer, credType); env(credentials::create(subject, issuer, credType)); env.close(); env(credentials::accept(subject, issuer, credType)); @@ -298,7 +288,7 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete subject before accept"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = credentials::keylet(subject, issuer, credType); env(credentials::create(subject, issuer, credType)); env.close(); @@ -334,7 +324,7 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete subject after accept"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = credentials::keylet(subject, issuer, credType); env(credentials::create(subject, issuer, credType)); env.close(); env(credentials::accept(subject, issuer, credType)); @@ -372,7 +362,7 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete by other"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = credentials::keylet(subject, issuer, credType); auto jv = credentials::create(subject, issuer, credType); uint32_t const t = env.current() ->info() @@ -417,7 +407,7 @@ struct Credentials_test : public beast::unit_test::suite env.close(); { auto const credKey = - credentialKeylet(subject, issuer, credType); + credentials::keylet(subject, issuer, credType); BEAST_EXPECT(!env.le(credKey)); BEAST_EXPECT(!ownerCount(env, subject)); BEAST_EXPECT(!ownerCount(env, issuer)); @@ -439,7 +429,7 @@ struct Credentials_test : public beast::unit_test::suite env.close(); { auto const credKey = - credentialKeylet(subject, issuer, credType); + credentials::keylet(subject, issuer, credType); BEAST_EXPECT(!env.le(credKey)); BEAST_EXPECT(!ownerCount(env, subject)); BEAST_EXPECT(!ownerCount(env, issuer)); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 0f29e22dd9..a6055d85f6 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -412,6 +412,9 @@ class MPToken_test : public beast::unit_test::suite // bob creates a mptoken mptAlice.authorize({.account = bob, .holderCount = 1}); + mptAlice.authorize( + {.account = bob, .holderCount = 1, .err = tecDUPLICATE}); + // bob deletes his mptoken mptAlice.authorize( {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize}); @@ -621,6 +624,25 @@ class MPToken_test : public beast::unit_test::suite // locks up bob's mptoken again mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); + if (!features[featureSingleAssetVault]) + { + // Delete bobs' mptoken even though it is locked + mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize}); + + mptAlice.set( + {.account = alice, + .holder = bob, + .flags = tfMPTUnlock, + .err = tecOBJECT_NOT_FOUND}); + + return; + } + + // Cannot delete locked MPToken + mptAlice.authorize( + {.account = bob, + .flags = tfMPTUnauthorize, + .err = tecNO_PERMISSION}); // alice unlocks mptissuance mptAlice.set({.account = alice, .flags = tfMPTUnlock}); @@ -2283,20 +2305,27 @@ public: FeatureBitset const all{supported_amendments()}; // MPTokenIssuanceCreate - testCreateValidation(all); - testCreateEnabled(all); + testCreateValidation(all - featureSingleAssetVault); + testCreateValidation(all | featureSingleAssetVault); + testCreateEnabled(all - featureSingleAssetVault); + testCreateEnabled(all | featureSingleAssetVault); // MPTokenIssuanceDestroy - testDestroyValidation(all); - testDestroyEnabled(all); + testDestroyValidation(all - featureSingleAssetVault); + testDestroyValidation(all | featureSingleAssetVault); + testDestroyEnabled(all - featureSingleAssetVault); + testDestroyEnabled(all | featureSingleAssetVault); // MPTokenAuthorize - testAuthorizeValidation(all); - testAuthorizeEnabled(all); + testAuthorizeValidation(all - featureSingleAssetVault); + testAuthorizeValidation(all | featureSingleAssetVault); + testAuthorizeEnabled(all - featureSingleAssetVault); + testAuthorizeEnabled(all | featureSingleAssetVault); // MPTokenIssuanceSet testSetValidation(all); - testSetEnabled(all); + testSetEnabled(all - featureSingleAssetVault); + testSetEnabled(all | featureSingleAssetVault); // MPT clawback testClawbackValidation(all); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp new file mode 100644 index 0000000000..67cc3812df --- /dev/null +++ b/src/test/app/Vault_test.cpp @@ -0,0 +1,3085 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include + +#include +#include +#include +#include +#include +#include +#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 +{ + static auto constexpr negativeAmount = + [](PrettyAsset const& asset) -> PrettyAmount { + return {STAmount{asset.raw(), 1ul, 0, true, STAmount::unchecked{}}, ""}; + }; + + void + testSequences() + { + using namespace test::jtx; + + auto const testSequence = [this]( + std::string const& prefix, + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Account const& charlie, + Vault& vault, + PrettyAsset const& asset) { + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + tx[sfData] = "AFEED00E"; + tx[sfAssetsMaximum] = asset(100).number(); + env(tx); + env.close(); + BEAST_EXPECT(env.le(keylet)); + + auto const share = [&env, keylet = keylet, this]() -> PrettyAsset { + auto const vault = env.le(keylet); + BEAST_EXPECT(vault != nullptr); + return MPTIssue(vault->at(sfShareMPTID)); + }(); + + // Several 3rd party accounts which cannot receive funds + Account alice{"alice"}; + Account dave{"dave"}; + Account erin{"erin"}; // not authorized by issuer + env.fund(XRP(1000), alice, dave, erin); + env(fset(alice, asfDepositAuth)); + env(fset(dave, asfRequireDest)); + env.close(); + + { + 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 + " 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}); + tx[sfAssetsMaximum] = asset(50).number(); + env(tx, ter(tecNO_PERMISSION)); + } + + { + testcase( + prefix + " fail to set maximum lower than current amount"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfAssetsMaximum] = 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[sfAssetsMaximum] = asset(150).number(); + env(tx); + } + + { + testcase(prefix + " set data"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfData] = "0"; + env(tx); + } + + { + testcase(prefix + " fail to set domain on public vault"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(base_uint<256>(42ul)); + env(tx, ter{tecNO_PERMISSION}); + } + + { + testcase(prefix + " fail to deposit more than maximum"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter(tecLIMIT_EXCEEDED)); + } + + { + testcase(prefix + " reset maximum to zero i.e. not enforced"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfAssetsMaximum] = asset(0).number(); + env(tx); + } + + { + 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 some more"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx); + } + + { + 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 + " fail to withdraw to 3rd party lsfDepositAuth"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + tx[sfDestination] = alice.human(); + env(tx, ter{tecNO_PERMISSION}); + } + + if (!asset.raw().native()) + { + testcase( + prefix + " fail to withdraw to 3rd party no authorization"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + tx[sfDestination] = erin.human(); + env(tx, + ter{asset.raw().holds() ? tecNO_LINE : tecNO_AUTH}); + } + + if (!asset.raw().native() && asset.raw().holds()) + { + testcase(prefix + " temporary authorization for 3rd party"); + env(trust(erin, asset(1000))); + env(trust(issuer, asset(0), erin, tfSetfAuth)); + env(pay(issuer, erin, asset(10))); + + // Erin deposits all in vault, then sends shares to depositor + auto tx = vault.deposit( + {.depositor = erin, .id = keylet.key, .amount = asset(10)}); + env(tx); + env(pay(erin, depositor, share(10))); + + testcase(prefix + " withdraw to authorized 3rd party"); + // Depositor withdraws shares, destined to Erin + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(10)}); + tx[sfDestination] = erin.human(); + env(tx); + // Erin returns assets to issuer + env(pay(erin, issuer, asset(10))); + + testcase(prefix + " fail to pay to unauthorized 3rd party"); + env(trust(erin, asset(0))); + // Erin has MPToken but is no longer authorized to hold assets + env(pay(depositor, erin, share(1)), ter{tecNO_LINE}); + } + + { + testcase( + prefix + + " fail to withdraw to 3rd party lsfRequireDestTag"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + tx[sfDestination] = dave.human(); + env(tx, ter{tecDST_TAG_NEEDED}); + } + + { + testcase(prefix + " withdraw to authorized 3rd party"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + tx[sfDestination] = charlie.human(); + env(tx); + } + + { + testcase(prefix + " withdraw to issuer"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + tx[sfDestination] = issuer.human(); + env(tx); + } + + { + testcase(prefix + " withdraw remaining assets"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + 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, supported_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + Account charlie{"charlie"}; // authorized 3rd party + Vault vault{env}; + env.fund(XRP(1000), issuer, owner, depositor, charlie); + env.close(); + env(fset(issuer, asfAllowTrustLineClawback)); + env(fset(issuer, asfRequireAuth)); + env.close(); + env.require(flags(issuer, asfAllowTrustLineClawback)); + env.require(flags(issuer, asfRequireAuth)); + + PrettyAsset asset = setup(env, issuer, owner, depositor, charlie); + testSequence( + prefix, env, issuer, owner, depositor, charlie, vault, asset); + }; + + testCases( + "XRP", + [](Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Account const& charlie) -> PrettyAsset { + return {xrpIssue(), 1'000'000}; + }); + + testCases( + "IOU", + [](Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Account const& charlie) -> Asset { + PrettyAsset asset = issuer["IOU"]; + env(trust(owner, asset(1000))); + env(trust(depositor, asset(1000))); + env(trust(charlie, asset(1000))); + env(trust(issuer, asset(0), owner, tfSetfAuth)); + env(trust(issuer, asset(0), depositor, tfSetfAuth)); + env(trust(issuer, asset(0), charlie, tfSetfAuth)); + env(pay(issuer, depositor, asset(1000))); + env.close(); + return asset; + }); + + testCases( + "MPT", + [](Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Account const& charlie) -> Asset { + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = + tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset asset = mptt.issuanceID(); + mptt.authorize({.account = depositor}); + mptt.authorize({.account = charlie}); + env(pay(issuer, depositor, asset(1000))); + env.close(); + return asset; + }); + } + + void + testPreflight() + { + using namespace test::jtx; + + struct CaseArgs + { + FeatureBitset features = + supported_amendments() | featureSingleAssetVault; + }; + + auto testCase = [&, this]( + std::function test, + CaseArgs args = {}) { + Env env{*this, args.features}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Vault vault{env}; + env.fund(XRP(1000), issuer, owner); + env.close(); + + env(fset(issuer, asfAllowTrustLineClawback)); + env(fset(issuer, asfRequireAuth)); + env.close(); + + PrettyAsset asset = issuer["IOU"]; + env(trust(owner, asset(1000))); + env(trust(issuer, asset(0), owner, tfSetfAuth)); + env(pay(issuer, owner, asset(1000))); + env.close(); + + test(env, issuer, owner, asset, vault); + }; + + testCase( + [&](Env& env, + Account const& issuer, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("disabled single asset vault"); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + env(tx, ter{temDISABLED}); + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + env(tx, ter{temDISABLED}); + } + + { + auto tx = vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + env(tx, ter{temDISABLED}); + } + + { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + env(tx, ter{temDISABLED}); + } + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(10)}); + env(tx, ter{temDISABLED}); + } + + { + auto tx = vault.del({.owner = owner, .id = keylet.key}); + env(tx, ter{temDISABLED}); + } + }, + {.features = supported_amendments() - featureSingleAssetVault}); + + testCase([&](Env& env, + Account const& issuer, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("invalid flags"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + tx[sfFlags] = tfClearDeepFreeze; + env(tx, ter{temINVALID_FLAG}); + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfFlags] = tfClearDeepFreeze; + env(tx, ter{temINVALID_FLAG}); + } + + { + auto tx = vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + tx[sfFlags] = tfClearDeepFreeze; + env(tx, ter{temINVALID_FLAG}); + } + + { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + tx[sfFlags] = tfClearDeepFreeze; + env(tx, ter{temINVALID_FLAG}); + } + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(10)}); + tx[sfFlags] = tfClearDeepFreeze; + env(tx, ter{temINVALID_FLAG}); + } + + { + auto tx = vault.del({.owner = owner, .id = keylet.key}); + tx[sfFlags] = tfClearDeepFreeze; + env(tx, ter{temINVALID_FLAG}); + } + }); + + testCase([&](Env& env, + Account const& issuer, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("invalid fee"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + tx[jss::Fee] = "-1"; + env(tx, ter{temBAD_FEE}); + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[jss::Fee] = "-1"; + env(tx, ter{temBAD_FEE}); + } + + { + auto tx = vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + tx[jss::Fee] = "-1"; + env(tx, ter{temBAD_FEE}); + } + + { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + tx[jss::Fee] = "-1"; + env(tx, ter{temBAD_FEE}); + } + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(10)}); + tx[jss::Fee] = "-1"; + env(tx, ter{temBAD_FEE}); + } + + { + auto tx = vault.del({.owner = owner, .id = keylet.key}); + tx[jss::Fee] = "-1"; + env(tx, ter{temBAD_FEE}); + } + }); + + testCase( + [&](Env& env, + Account const&, + Account const& owner, + Asset const&, + Vault& vault) { + testcase("disabled permissioned domain"); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = xrpIssue()}); + tx[sfDomainID] = to_string(base_uint<256>(42ul)); + env(tx, ter{temDISABLED}); + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(base_uint<256>(42ul)); + env(tx, ter{temDISABLED}); + } + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = "0"; + env(tx, ter{temDISABLED}); + } + }, + {.features = (supported_amendments() | featureSingleAssetVault) - + featurePermissionedDomains}); + + testCase([&](Env& env, + Account const& issuer, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("use zero vault"); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = xrpIssue()}); + + { + auto tx = vault.set({ + .owner = owner, + .id = beast::zero, + }); + env(tx, ter{temMALFORMED}); + } + + { + auto tx = vault.deposit( + {.depositor = owner, + .id = beast::zero, + .amount = asset(10)}); + env(tx, ter(temMALFORMED)); + } + + { + auto tx = vault.withdraw( + {.depositor = owner, + .id = beast::zero, + .amount = asset(10)}); + env(tx, ter{temMALFORMED}); + } + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = beast::zero, + .holder = owner, + .amount = asset(10)}); + env(tx, ter{temMALFORMED}); + } + + { + auto tx = vault.del({ + .owner = owner, + .id = beast::zero, + }); + env(tx, ter{temMALFORMED}); + } + }); + + testCase([&](Env& env, + Account const& issuer, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("clawback from self"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = issuer, + .amount = asset(10)}); + env(tx, ter{temMALFORMED}); + } + }); + + testCase([&](Env& env, + Account const&, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("withdraw to bad destination"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + tx[jss::Destination] = "0"; + env(tx, ter{temMALFORMED}); + } + }); + + testCase([&](Env& env, + Account const&, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("create or set invalid data"); + + auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = tx1; + tx[sfData] = ""; + env(tx, ter(temMALFORMED)); + } + + { + auto tx = tx1; + // A hexadecimal string of 257 bytes. + tx[sfData] = std::string(514, 'A'); + env(tx, ter(temMALFORMED)); + } + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfData] = ""; + env(tx, ter{temMALFORMED}); + } + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + // A hexadecimal string of 257 bytes. + tx[sfData] = std::string(514, 'A'); + env(tx, ter{temMALFORMED}); + } + }); + + testCase([&](Env& env, + Account const&, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("set nothing updated"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + env(tx, ter{temMALFORMED}); + } + }); + + testCase([&](Env& env, + Account const&, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("create with invalid metadata"); + + auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = tx1; + tx[sfMPTokenMetadata] = ""; + env(tx, ter(temMALFORMED)); + } + + { + auto tx = tx1; + // This metadata is for the share token. + // A hexadecimal string of 1025 bytes. + tx[sfMPTokenMetadata] = std::string(2050, 'B'); + env(tx, ter(temMALFORMED)); + } + }); + + testCase([&](Env& env, + Account const&, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("set negative maximum"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfAssetsMaximum] = negativeAmount(asset).number(); + env(tx, ter{temMALFORMED}); + } + }); + + testCase([&](Env& env, + Account const&, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("invalid deposit amount"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = negativeAmount(asset)}); + env(tx, ter(temBAD_AMOUNT)); + } + + { + auto tx = vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(0)}); + env(tx, ter(temBAD_AMOUNT)); + } + }); + + testCase([&](Env& env, + Account const&, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("invalid set immutable flag"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfFlags] = tfVaultPrivate; + env(tx, ter(temINVALID_FLAG)); + } + }); + + testCase([&](Env& env, + Account const&, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("invalid withdraw amount"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = negativeAmount(asset)}); + env(tx, ter(temBAD_AMOUNT)); + } + + { + auto tx = vault.withdraw( + {.depositor = owner, .id = keylet.key, .amount = asset(0)}); + env(tx, ter(temBAD_AMOUNT)); + } + }); + + testCase([&](Env& env, + Account const& issuer, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("invalid clawback"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = vault.clawback( + {.issuer = owner, + .id = keylet.key, + .holder = issuer, + .amount = asset(50)}); + env(tx, ter(temMALFORMED)); + } + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = negativeAmount(asset)}); + env(tx, ter(temBAD_AMOUNT)); + } + }); + + testCase([&](Env& env, + Account const&, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("invalid create"); + + auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset}); + + { + auto tx = tx1; + tx[sfWithdrawalPolicy] = 0; + env(tx, ter(temMALFORMED)); + } + + { + auto tx = tx1; + tx[sfDomainID] = to_string(base_uint<256>(42ul)); + env(tx, ter{temMALFORMED}); + } + + { + auto tx = tx1; + tx[sfAssetsMaximum] = negativeAmount(asset).number(); + env(tx, ter{temMALFORMED}); + } + + { + auto tx = tx1; + tx[sfFlags] = tfVaultPrivate; + tx[sfDomainID] = "0"; + env(tx, ter{temMALFORMED}); + } + }); + } + + // Test for non-asset specific behaviors. + void + testCreateFailXRP() + { + using namespace test::jtx; + + auto testCase = [this](std::function test) { + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + Vault vault{env}; + Asset asset = xrpIssue(); + + test(env, issuer, owner, depositor, asset, vault); + }; + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault) { + testcase("nothing to set"); + auto tx = vault.set({.owner = owner, .id = keylet::skip().key}); + tx[sfAssetsMaximum] = asset(0).number(); + env(tx, ter(tecNO_ENTRY)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault) { + testcase("nothing to deposit to"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet::skip().key, + .amount = asset(10)}); + env(tx, ter(tecNO_ENTRY)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault) { + testcase("nothing to withdraw from"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet::skip().key, + .amount = asset(10)}); + env(tx, ter(tecNO_ENTRY)); + }); + + 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 = owner, .id = keylet::skip().key}); + env(tx, ter(tecNO_ENTRY)); + }); + + 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}); + 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}); + tx[sfFlags] = tfVaultPrivate; + tx[sfDomainID] = to_string(base_uint<256>(42ul)); + testcase("non-existing domain"); + env(tx, ter{tecOBJECT_NOT_FOUND}); + }); + } + + void + testCreateFailIOU() + { + using namespace test::jtx; + { + { + testcase("IOU fail create frozen"); + Env env{ + *this, supported_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + env.fund(XRP(1000), issuer, owner); + env.close(); + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + Vault vault{env}; + Asset asset = issuer["IOU"]; + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + + env(tx, ter(tecFROZEN)); + env.close(); + } + + { + testcase("IOU fail create no ripling"); + Env env{ + *this, supported_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + env.fund(XRP(1000), issuer, owner); + env.close(); + env(fclear(issuer, asfDefaultRipple)); + env.close(); + + Vault vault{env}; + Asset asset = issuer["IOU"]; + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + env(tx, ter(terNO_RIPPLE)); + env.close(); + } + + { + testcase("IOU no issuer"); + Env env{ + *this, supported_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + env.fund(XRP(1000), owner); + env.close(); + + Vault vault{env}; + Asset asset = issuer["IOU"]; + { + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + env(tx, ter(terNO_ACCOUNT)); + env.close(); + } + } + } + + { + testcase("IOU fail create vault for AMM LPToken"); + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account const gw("gateway"); + Account const alice("alice"); + Account const carol("carol"); + IOU const USD = gw["USD"]; + + auto const [asset1, asset2] = + std::pair(XRP(10000), USD(10000)); + auto tofund = [&](STAmount const& a) -> STAmount { + if (a.native()) + { + auto const defXRP = XRP(30000); + if (a <= defXRP) + return defXRP; + return a + XRP(1000); + } + auto const defIOU = STAmount{a.issue(), 30000}; + if (a <= defIOU) + return defIOU; + return a + STAmount{a.issue(), 1000}; + }; + auto const toFund1 = tofund(asset1); + auto const toFund2 = tofund(asset2); + BEAST_EXPECT(asset1 <= toFund1 && asset2 <= toFund2); + + if (!asset1.native() && !asset2.native()) + fund(env, gw, {alice, carol}, {toFund1, toFund2}, Fund::All); + else if (asset1.native()) + fund(env, gw, {alice, carol}, toFund1, {toFund2}, Fund::All); + else if (asset2.native()) + fund(env, gw, {alice, carol}, toFund2, {toFund1}, Fund::All); + + AMM ammAlice( + env, alice, asset1, asset2, CreateArg{.log = false, .tfee = 0}); + + Account const owner{"owner"}; + env.fund(XRP(1000000), owner); + + Vault vault{env}; + auto [tx, k] = + vault.create({.owner = owner, .asset = ammAlice.lptIssue()}); + env(tx, ter{tecWRONG_ASSET}); + env.close(); + } + } + + void + testCreateFailMPT() + { + using namespace test::jtx; + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + Vault vault{env}; + + 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 + testNonTransferableShares() + { + using namespace test::jtx; + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + + Vault vault{env}; + PrettyAsset asset = issuer["IOU"]; + env.trust(asset(1000), owner); + env(pay(issuer, owner, asset(100))); + env.trust(asset(1000), depositor); + env(pay(issuer, depositor, asset(100))); + env.close(); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + tx[sfFlags] = tfVaultShareNonTransferable; + env(tx); + env.close(); + + { + testcase("nontransferable deposits"); + auto tx1 = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(40)}); + env(tx1); + + auto tx2 = vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(60)}); + env(tx2); + env.close(); + } + + auto const vaultAccount = // + [&env, key = keylet.key, this]() -> AccountID { + auto jvVault = env.rpc("vault_info", strHex(key)); + + BEAST_EXPECT( + jvVault[jss::result][jss::vault][sfAssetsTotal] == "100"); + BEAST_EXPECT( + jvVault[jss::result][jss::vault][jss::shares] + [sfOutstandingAmount] == "100"); + + // Vault pseudo-account + return parseBase58( + jvVault[jss::result][jss::vault][jss::Account] + .asString()) + .value(); + }(); + + auto const MptID = makeMptID(1, vaultAccount); + Asset shares = MptID; + + { + testcase("nontransferable shares cannot be moved"); + env(pay(owner, depositor, shares(10)), ter{tecNO_AUTH}); + env(pay(depositor, owner, shares(10)), ter{tecNO_AUTH}); + } + + { + testcase("nontransferable shares can be used to withdraw"); + auto tx1 = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(20)}); + env(tx1); + + auto tx2 = vault.withdraw( + {.depositor = owner, .id = keylet.key, .amount = asset(30)}); + env(tx2); + env.close(); + } + + { + testcase("nontransferable shares balance check"); + auto jvVault = env.rpc("vault_info", strHex(keylet.key)); + BEAST_EXPECT( + jvVault[jss::result][jss::vault][sfAssetsTotal] == "50"); + BEAST_EXPECT( + jvVault[jss::result][jss::vault][jss::shares] + [sfOutstandingAmount] == "50"); + } + + { + testcase("nontransferable shares withdraw rest"); + auto tx1 = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(20)}); + env(tx1); + + auto tx2 = vault.withdraw( + {.depositor = owner, .id = keylet.key, .amount = asset(30)}); + env(tx2); + env.close(); + } + + { + testcase("nontransferable shares delete empty vault"); + auto tx = vault.del({.owner = owner, .id = keylet.key}); + env(tx); + BEAST_EXPECT(!env.le(keylet)); + } + } + + void + testWithMPT() + { + using namespace test::jtx; + + struct CaseArgs + { + bool enableClawback = true; + }; + + auto testCase = [this]( + std::function test, + CaseArgs args = {}) { + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + Vault vault{env}; + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanTransfer | tfMPTCanLock | + (args.enableClawback ? lsfMPTCanClawback + : LedgerSpecificFlags(0)) | + tfMPTRequireAuth}); + PrettyAsset asset = mptt.issuanceID(); + mptt.authorize({.account = owner}); + mptt.authorize({.account = issuer, .holder = owner}); + mptt.authorize({.account = depositor}); + mptt.authorize({.account = issuer, .holder = 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, + PrettyAsset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT nothing to clawback from"); + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet::skip().key, + .holder = depositor, + .amount = asset(10)}); + env(tx, ter(tecNO_ENTRY)); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT global lock blocks create"); + 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("MPT global lock blocks deposit"); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + mptt.set({.account = issuer, .flags = tfMPTLock}); + env.close(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter{tecLOCKED}); + env.close(); + + // Can delete empty vault, even if global lock + tx = vault.del({.owner = owner, .id = keylet.key}); + env(tx); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT global lock blocks withdrawal"); + 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)[sfShareMPTID]; + auto issuance = env.le(keylet::mptIssuance(share)); + BEAST_EXPECT(issuance); + Number outstandingShares = issuance->at(sfOutstandingAmount); + BEAST_EXPECT(outstandingShares == 100); + + mptt.set({.account = issuer, .flags = tfMPTLock}); + env.close(); + + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter(tecLOCKED)); + + tx[sfDestination] = issuer.human(); + env(tx, ter(tecLOCKED)); + + // Clawback is still permitted, even with global lock + tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = asset(0)}); + env(tx); + env.close(); + + // Can delete empty vault, even if global lock + tx = vault.del({.owner = owner, .id = keylet.key}); + env(tx); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT only issuer can clawback"); + + 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(); + + { + auto tx = vault.clawback( + {.issuer = owner, .id = keylet.key, .holder = depositor}); + env(tx, ter(tecNO_PERMISSION)); + } + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT issuance deleted"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(1000)}); + env(tx); + env.close(); + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = asset(0)}); + env(tx); + } + + mptt.destroy({.issuer = issuer, .id = mptt.issuanceID()}); + env.close(); + + { + auto [tx, keylet] = + vault.create({.owner = depositor, .asset = asset}); + env(tx, ter{tecOBJECT_NOT_FOUND}); + } + + { + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(10)}); + env(tx, ter{tecOBJECT_NOT_FOUND}); + } + + { + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(10)}); + env(tx, ter{tecOBJECT_NOT_FOUND}); + } + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = asset(0)}); + env(tx, ter{tecOBJECT_NOT_FOUND}); + } + + env(vault.del({.owner = owner, .id = keylet.key})); + }); + + testCase( + [this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT clawback disabled"); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(1000)}); + env(tx); + env.close(); + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = asset(0)}); + env(tx, ter{tecNO_PERMISSION}); + } + }, + {.enableClawback = false}); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT un-authorization"); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(1000)}); + env(tx); + env.close(); + + mptt.authorize( + {.account = issuer, + .holder = depositor, + .flags = tfMPTUnauthorize}); + env.close(); + + { + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter(tecNO_AUTH)); + + // Withdrawal to other (authorized) accounts works + tx[sfDestination] = issuer.human(); + env(tx); + tx[sfDestination] = owner.human(); + env(tx); + env.close(); + } + + { + // Cannot deposit some more + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter(tecNO_AUTH)); + } + + // Clawback works + tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = asset(800)}); + env(tx); + + env(vault.del({.owner = owner, .id = keylet.key})); + }); + + testCase([this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + Asset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT lock of vault pseudo-account"); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + auto const vaultAccount = + [&env, keylet = keylet, this]() -> AccountID { + auto const vault = env.le(keylet); + BEAST_EXPECT(vault != nullptr); + return vault->at(sfAccount); + }(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx); + env.close(); + + tx = [&]() { + Json::Value jv; + jv[jss::Account] = issuer.human(); + jv[sfMPTokenIssuanceID] = + to_string(asset.get().getMptID()); + jv[jss::Holder] = toBase58(vaultAccount); + jv[jss::TransactionType] = jss::MPTokenIssuanceSet; + jv[jss::Flags] = tfMPTLock; + return jv; + }(); + env(tx); + env.close(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter(tecLOCKED)); + + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter(tecLOCKED)); + + // Clawback works, even when locked + tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = asset(100)}); + env(tx); + + // Can delete an empty vault even when asset is locked. + tx = vault.del({.owner = owner, .id = keylet.key}); + env(tx); + }); + + { + testcase("MPT shares to a vault"); + + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account owner{"owner"}; + Account issuer{"issuer"}; + env.fund(XRP(1000000), owner, issuer); + env.close(); + Vault vault{env}; + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanTransfer | tfMPTCanLock | lsfMPTCanClawback | + tfMPTRequireAuth}); + mptt.authorize({.account = owner}); + mptt.authorize({.account = issuer, .holder = owner}); + PrettyAsset asset = mptt.issuanceID(); + env(pay(issuer, owner, asset(100))); + auto [tx1, k1] = vault.create({.owner = owner, .asset = asset}); + env(tx1); + env.close(); + + auto const shares = [&env, keylet = k1, this]() -> Asset { + auto const vault = env.le(keylet); + BEAST_EXPECT(vault != nullptr); + return MPTIssue(vault->at(sfShareMPTID)); + }(); + + auto [tx2, k2] = vault.create({.owner = owner, .asset = shares}); + env(tx2, ter{tecWRONG_ASSET}); + env.close(); + } + } + + void + testWithIOU() + { + auto testCase = + [&, this]( + std::function vaultAccount, + Vault& vault, + PrettyAsset const& asset, + std::function issuanceId, + std::function vaultBalance)> + test) { + Env env{ + *this, supported_amendments() | featureSingleAssetVault}; + Account const owner{"owner"}; + Account const issuer{"issuer"}; + Account const charlie{"charlie"}; + Vault vault{env}; + env.fund(XRP(1000), issuer, owner, charlie); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1000), owner); + env(pay(issuer, owner, asset(200))); + env(rate(issuer, 1.25)); + env.close(); + + auto const [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + auto const vaultAccount = + [&env](ripple::Keylet keylet) -> AccountID { + return env.le(keylet)->at(sfAccount); + }; + auto const issuanceId = [&env](ripple::Keylet keylet) -> MPTID { + return env.le(keylet)->at(sfShareMPTID); + }; + auto const vaultBalance = // + [&env, &vaultAccount, issue = asset.raw().get()]( + ripple::Keylet keylet) -> PrettyAmount { + auto const account = vaultAccount(keylet); + auto const sle = env.le(keylet::line(account, issue)); + if (sle == nullptr) + return { + STAmount(issue, 0), + env.lookup(issue.account).name()}; + auto amount = sle->getFieldAmount(sfBalance); + amount.setIssuer(issue.account); + if (account > issue.account) + amount.negate(); + return {amount, env.lookup(issue.account).name()}; + }; + + test( + env, + owner, + issuer, + charlie, + vaultAccount, + vault, + asset, + issuanceId, + vaultBalance); + }; + + testCase([&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const&, + auto vaultAccount, + Vault& vault, + PrettyAsset const& asset, + auto&&...) { + testcase("IOU cannot use different asset"); + PrettyAsset const foo = issuer["FOO"]; + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + { + // Cannot create new trustline to a vault + auto tx = [&, account = vaultAccount(keylet)]() { + Json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + foo(0).value().getJson(JsonOptions::none); + ja[jss::issuer] = toBase58(account); + } + jv[jss::TransactionType] = jss::TrustSet; + jv[jss::Flags] = tfSetFreeze; + return jv; + }(); + env(tx, ter{tecNO_PERMISSION}); + env.close(); + } + + { + auto tx = vault.deposit( + {.depositor = issuer, .id = keylet.key, .amount = foo(20)}); + env(tx, ter{tecWRONG_ASSET}); + env.close(); + } + + { + auto tx = vault.withdraw( + {.depositor = issuer, .id = keylet.key, .amount = foo(20)}); + env(tx, ter{tecWRONG_ASSET}); + env.close(); + } + + env(vault.del({.owner = owner, .id = keylet.key})); + env.close(); + }); + + testCase([&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const& charlie, + auto vaultAccount, + Vault& vault, + PrettyAsset const& asset, + auto issuanceId, + auto) { + testcase("IOU frozen trust line to vault account"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + env(vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); + + Asset const share = Asset(issuanceId(keylet)); + + // Freeze the trustline to the vault + auto trustSet = [&, account = vaultAccount(keylet)]() { + Json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::none); + ja[jss::issuer] = toBase58(account); + } + jv[jss::TransactionType] = jss::TrustSet; + jv[jss::Flags] = tfSetFreeze; + return jv; + }(); + env(trustSet); + env.close(); + + { + // Note, the "frozen" state of the trust line to vault account + // is reported as "locked" state of the vault shares, because + // this state is attached to shares by means of the transitive + // isFrozen. + auto tx = vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(80)}); + env(tx, ter{tecLOCKED}); + } + + { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter{tecLOCKED}); + + // also when trying to withdraw to a 3rd party + tx[sfDestination] = charlie.human(); + env(tx, ter{tecLOCKED}); + env.close(); + } + + { + // Clawback works, even when locked + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(50)}); + env(tx); + env.close(); + } + + // Clear the frozen state + trustSet[jss::Flags] = tfClearFreeze; + env(trustSet); + env.close(); + + env(vault.withdraw( + {.depositor = owner, .id = keylet.key, .amount = share(50)})); + + env(vault.del({.owner = owner, .id = keylet.key})); + env.close(); + }); + + testCase([&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const& charlie, + auto, + Vault& vault, + PrettyAsset const& asset, + auto issuanceId, + auto vaultBalance) { + testcase("IOU transfer fees not applied"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + env(vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); + + auto const issue = asset.raw().get(); + Asset const share = Asset(issuanceId(keylet)); + + // transfer fees ignored on deposit + BEAST_EXPECT(env.balance(owner, issue) == asset(100)); + BEAST_EXPECT(vaultBalance(keylet) == asset(100)); + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(50)}); + env(tx); + env.close(); + } + + // transfer fees ignored on clawback + BEAST_EXPECT(env.balance(owner, issue) == asset(100)); + BEAST_EXPECT(vaultBalance(keylet) == asset(50)); + + env(vault.withdraw( + {.depositor = owner, .id = keylet.key, .amount = share(20)})); + + // transfer fees ignored on withdraw + BEAST_EXPECT(env.balance(owner, issue) == asset(120)); + BEAST_EXPECT(vaultBalance(keylet) == asset(30)); + + { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = share(30)}); + tx[sfDestination] = charlie.human(); + env(tx); + } + + // transfer fees ignored on withdraw to 3rd party + BEAST_EXPECT(env.balance(owner, issue) == asset(120)); + BEAST_EXPECT(env.balance(charlie, issue) == asset(30)); + BEAST_EXPECT(vaultBalance(keylet) == asset(0)); + + env(vault.del({.owner = owner, .id = keylet.key})); + env.close(); + }); + + testCase([&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const& charlie, + auto, + Vault& vault, + PrettyAsset const& asset, + auto&&...) { + testcase("IOU frozen trust line to depositor"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + env(vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); + + // Withdraw to 3rd party works + auto const withdrawToCharlie = [&](ripple::Keylet keylet) { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + tx[sfDestination] = charlie.human(); + return tx; + }(keylet); + env(withdrawToCharlie); + + // Freeze the owner + env(trust(issuer, asset(0), owner, tfSetFreeze)); + env.close(); + + // Cannot withdraw + auto const withdraw = vault.withdraw( + {.depositor = owner, .id = keylet.key, .amount = asset(10)}); + env(withdraw, ter{tecFROZEN}); + + // Cannot withdraw to 3rd party + env(withdrawToCharlie, ter{tecLOCKED}); + env.close(); + + { + // Cannot deposit some more + auto tx = vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + env(tx, ter{tecFROZEN}); + } + + { + // Clawback still works + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(0)}); + env(tx); + env.close(); + } + + env(vault.del({.owner = owner, .id = keylet.key})); + env.close(); + }); + + testCase([&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const& charlie, + auto, + Vault& vault, + PrettyAsset const& asset, + auto&&...) { + testcase("IOU frozen trust line to 3rd party"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + env(vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); + + // Withdraw to 3rd party works + auto const withdrawToCharlie = [&](ripple::Keylet keylet) { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + tx[sfDestination] = charlie.human(); + return tx; + }(keylet); + env(withdrawToCharlie); + + // Freeze the 3rd party + env(trust(issuer, asset(0), charlie, tfSetFreeze)); + env.close(); + + // Can withdraw + auto const withdraw = vault.withdraw( + {.depositor = owner, .id = keylet.key, .amount = asset(10)}); + env(withdraw); + env.close(); + + // Cannot withdraw to 3rd party + env(withdrawToCharlie, ter{tecFROZEN}); + env.close(); + + env(vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(0)})); + env.close(); + + env(vault.del({.owner = owner, .id = keylet.key})); + env.close(); + }); + + testCase([&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const& charlie, + auto, + Vault& vault, + PrettyAsset const& asset, + auto&&...) { + testcase("IOU global freeze"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + env(vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); + + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + { + // Cannot withdraw + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + env(tx, ter{tecFROZEN}); + + // Cannot withdraw to 3rd party + tx[sfDestination] = charlie.human(); + env(tx, ter{tecFROZEN}); + env.close(); + + // Cannot deposit some more + tx = vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + + env(tx, ter{tecFROZEN}); + } + + // Clawback is permitted + env(vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(0)})); + env.close(); + + env(vault.del({.owner = owner, .id = keylet.key})); + env.close(); + }); + } + + void + testWithDomainCheck() + { + testcase("private vault"); + + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + Account charlie{"charlie"}; + Account pdOwner{"pdOwner"}; + Account credIssuer1{"credIssuer1"}; + Account credIssuer2{"credIssuer2"}; + std::string const credType = "credential"; + Vault vault{env}; + env.fund( + XRP(1000), + issuer, + owner, + depositor, + charlie, + pdOwner, + credIssuer1, + credIssuer2); + env.close(); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(issuer, asfAllowTrustLineClawback)); + + PrettyAsset asset = issuer["IOU"]; + env.trust(asset(1000), owner); + env(pay(issuer, owner, asset(500))); + env.trust(asset(1000), depositor); + env(pay(issuer, depositor, asset(500))); + env.close(); + + auto [tx, keylet] = vault.create( + {.owner = owner, .asset = asset, .flags = tfVaultPrivate}); + env(tx); + env.close(); + BEAST_EXPECT(env.le(keylet)); + + { + testcase("private vault owner can deposit"); + auto tx = vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(50)}); + env(tx); + } + + { + testcase("private vault depositor not authorized yet"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx, ter{tecNO_AUTH}); + } + + { + testcase("private vault cannot set non-existing domain"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(base_uint<256>(42ul)); + env(tx, ter{tecOBJECT_NOT_FOUND}); + } + + { + testcase("private vault set domainId"); + + { + pdomain::Credentials const credentials1{ + {.issuer = credIssuer1, .credType = credType}}; + + env(pdomain::setTx(pdOwner, credentials1)); + auto const domainId1 = [&]() { + auto tx = env.tx()->getJson(JsonOptions::none); + return pdomain::getNewDomain(env.meta()); + }(); + + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(domainId1); + env(tx); + env.close(); + + // Update domain second time, should be harmless + env(tx); + env.close(); + } + + { + pdomain::Credentials const credentials{ + {.issuer = credIssuer1, .credType = credType}, + {.issuer = credIssuer2, .credType = credType}}; + + env(pdomain::setTx(pdOwner, credentials)); + auto const domainId = [&]() { + auto tx = env.tx()->getJson(JsonOptions::none); + return pdomain::getNewDomain(env.meta()); + }(); + + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(domainId); + env(tx); + env.close(); + } + } + + { + testcase("private vault depositor still not authorized"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx, ter{tecNO_AUTH}); + env.close(); + } + + auto const credKeylet = + credentials::keylet(depositor, credIssuer1, credType); + { + testcase("private vault depositor now authorized"); + env(credentials::create(depositor, credIssuer1, credType)); + env(credentials::accept(depositor, credIssuer1, credType)); + env(credentials::create(charlie, credIssuer1, credType)); + env(credentials::accept(charlie, credIssuer1, credType)); + env.close(); + auto credSle = env.le(credKeylet); + BEAST_EXPECT(credSle != nullptr); + + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx); + env.close(); + + tx = vault.deposit( + {.depositor = charlie, .id = keylet.key, .amount = asset(50)}); + env(tx, ter{tecINSUFFICIENT_FUNDS}); + env.close(); + } + + { + testcase("private vault depositor lost authorization"); + env(credentials::deleteCred( + credIssuer1, depositor, credIssuer1, credType)); + env.close(); + auto credSle = env.le(credKeylet); + BEAST_EXPECT(credSle == nullptr); + + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx, ter{tecNO_AUTH}); + env.close(); + } + + { + testcase("private vault depositor new authorization"); + env(credentials::create(depositor, credIssuer2, credType)); + env(credentials::accept(depositor, credIssuer2, credType)); + env.close(); + + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx); + env.close(); + } + + { + testcase("private vault reset domainId"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = "0"; + env(tx); + env.close(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx, ter{tecNO_AUTH}); + env.close(); + + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx); + + tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = depositor, + .amount = asset(0)}); + env(tx); + + tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(0)}); + env(tx); + + tx = vault.del({ + .owner = owner, + .id = keylet.key, + }); + env(tx); + } + } + + void + testWithDomainCheckXRP() + { + testcase("private XRP vault"); + + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account owner{"owner"}; + Account depositor{"depositor"}; + Account alice{"charlie"}; + std::string const credType = "credential"; + Vault vault{env}; + env.fund(XRP(100000), owner, depositor, alice); + env.close(); + + PrettyAsset asset = xrpIssue(); + auto [tx, keylet] = vault.create( + {.owner = owner, .asset = asset, .flags = tfVaultPrivate}); + env(tx); + env.close(); + + auto const [vaultAccount, issuanceId] = + [&env, keylet = keylet, this]() -> std::tuple { + auto const vault = env.le(keylet); + BEAST_EXPECT(vault != nullptr); + return {vault->at(sfAccount), vault->at(sfShareMPTID)}; + }(); + BEAST_EXPECT(env.le(keylet::account(vaultAccount))); + BEAST_EXPECT(env.le(keylet::mptIssuance(issuanceId))); + PrettyAsset shares{issuanceId}; + + { + testcase("private XRP vault owner can deposit"); + auto tx = vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(50)}); + env(tx); + } + + { + testcase("private XRP vault cannot pay shares to depositor yet"); + env(pay(owner, depositor, shares(1)), ter{tecNO_AUTH}); + } + + { + testcase("private XRP vault depositor not authorized yet"); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx, ter{tecNO_AUTH}); + } + + { + testcase("private XRP vault set DomainID"); + pdomain::Credentials const credentials{ + {.issuer = owner, .credType = credType}}; + + env(pdomain::setTx(owner, credentials)); + auto const domainId = [&]() { + auto tx = env.tx()->getJson(JsonOptions::none); + return pdomain::getNewDomain(env.meta()); + }(); + + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(domainId); + env(tx); + env.close(); + } + + auto const credKeylet = credentials::keylet(depositor, owner, credType); + { + testcase("private XRP vault depositor now authorized"); + env(credentials::create(depositor, owner, credType)); + env(credentials::accept(depositor, owner, credType)); + env.close(); + + BEAST_EXPECT(env.le(credKeylet)); + auto tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx); + env.close(); + } + + { + testcase("private XRP vault can pay shares to depositor"); + env(pay(owner, depositor, shares(1))); + } + + { + testcase("private XRP vault cannot pay shares to 3rd party"); + Json::Value jv; + jv[sfAccount] = alice.human(); + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfMPTokenIssuanceID] = to_string(issuanceId); + env(jv); + env.close(); + + env(pay(owner, alice, shares(1)), ter{tecNO_AUTH}); + } + } + + void + testFailedPseudoAccount() + { + using namespace test::jtx; + + testcase("failed pseudo-account allocation"); + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account const owner{"owner"}; + Vault vault{env}; + env.fund(XRP(1000), owner); + + auto const keylet = keylet::vault(owner.id(), env.seq(owner)); + for (int i = 0; i < 256; ++i) + { + AccountID const accountId = + ripple::pseudoAccountAddress(*env.current(), keylet.key); + + env(pay(env.master.id(), accountId, XRP(1000)), + seq(autofill), + fee(autofill), + sig(autofill)); + } + + auto [tx, keylet1] = + vault.create({.owner = owner, .asset = xrpIssue()}); + BEAST_EXPECT(keylet.key == keylet1.key); + env(tx, ter{terADDRESS_COLLISION}); + } + + void + testRPC() + { + testcase("RPC"); + Env env{*this, supported_amendments() | featureSingleAssetVault}; + Account const owner{"owner"}; + Account const issuer{"issuer"}; + Vault vault{env}; + env.fund(XRP(1000), issuer, owner); + env.close(); + + PrettyAsset asset = issuer["IOU"]; + env.trust(asset(1000), owner); + env(pay(issuer, owner, asset(200))); + env.close(); + + auto const sequence = env.seq(owner); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + // Set some fields + { + auto tx1 = vault.deposit( + {.depositor = owner, .id = keylet.key, .amount = asset(50)}); + env(tx1); + + auto tx2 = vault.set({.owner = owner, .id = keylet.key}); + tx2[sfAssetsMaximum] = asset(1000).number(); + env(tx2); + env.close(); + } + + auto const sleVault = [&env, keylet = keylet, this]() { + auto const vault = env.le(keylet); + BEAST_EXPECT(vault != nullptr); + return vault; + }(); + + auto const check = [&, keylet = keylet, sle = sleVault, this]( + Json::Value const& vault, + Json::Value const& issuance = Json::nullValue) { + BEAST_EXPECT(vault.isObject()); + + constexpr auto checkString = + [](auto& node, SField const& field, std::string v) -> bool { + return node.isMember(field.fieldName) && + node[field.fieldName].isString() && + node[field.fieldName] == v; + }; + constexpr auto checkObject = + [](auto& node, SField const& field, Json::Value v) -> bool { + return node.isMember(field.fieldName) && + node[field.fieldName].isObject() && + node[field.fieldName] == v; + }; + constexpr auto checkInt = + [](auto& node, SField const& field, int v) -> bool { + return node.isMember(field.fieldName) && + ((node[field.fieldName].isInt() && + node[field.fieldName] == Json::Int(v)) || + (node[field.fieldName].isUInt() && + node[field.fieldName] == Json::UInt(v))); + }; + + BEAST_EXPECT(vault["LedgerEntryType"].asString() == "Vault"); + BEAST_EXPECT(vault[jss::index].asString() == strHex(keylet.key)); + BEAST_EXPECT(checkInt(vault, sfFlags, 0)); + // Ignore all other standard fields, this test doesn't care + + BEAST_EXPECT( + checkString(vault, sfAccount, toBase58(sle->at(sfAccount)))); + BEAST_EXPECT( + checkObject(vault, sfAsset, to_json(sle->at(sfAsset)))); + BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50")); + BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000")); + BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50")); + BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0")); + + auto const strShareID = strHex(sle->at(sfShareMPTID)); + BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID)); + BEAST_EXPECT(checkString(vault, sfOwner, toBase58(owner.id()))); + BEAST_EXPECT(checkInt(vault, sfSequence, sequence)); + BEAST_EXPECT(checkInt( + vault, sfWithdrawalPolicy, vaultStrategyFirstComeFirstServe)); + + if (issuance.isObject()) + { + BEAST_EXPECT( + issuance["LedgerEntryType"].asString() == + "MPTokenIssuance"); + BEAST_EXPECT( + issuance[jss::mpt_issuance_id].asString() == strShareID); + BEAST_EXPECT(checkInt(issuance, sfSequence, 1)); + BEAST_EXPECT(checkInt( + issuance, + sfFlags, + int(lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer))); + BEAST_EXPECT(checkString(issuance, sfOutstandingAmount, "50")); + } + }; + + { + testcase("RPC ledger_entry selected by key"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault] = strHex(keylet.key); + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + + BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error)); + BEAST_EXPECT(jvVault[jss::result].isMember(jss::node)); + check(jvVault[jss::result][jss::node]); + } + + { + testcase("RPC ledger_entry selected by owner and seq"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault][jss::owner] = owner.human(); + jvParams[jss::vault][jss::seq] = sequence; + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + + BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error)); + BEAST_EXPECT(jvVault[jss::result].isMember(jss::node)); + check(jvVault[jss::result][jss::node]); + } + + { + testcase("RPC ledger_entry cannot find vault by key"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault] = to_string(uint256(42)); + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT( + jvVault[jss::result][jss::error].asString() == "entryNotFound"); + } + + { + testcase("RPC ledger_entry cannot find vault by owner and seq"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault][jss::owner] = issuer.human(); + jvParams[jss::vault][jss::seq] = 1'000'000; + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT( + jvVault[jss::result][jss::error].asString() == "entryNotFound"); + } + + { + testcase("RPC ledger_entry malformed key"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault] = 42; + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT( + jvVault[jss::result][jss::error].asString() == + "malformedRequest"); + } + + { + testcase("RPC ledger_entry malformed owner"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault][jss::owner] = 42; + jvParams[jss::vault][jss::seq] = sequence; + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT( + jvVault[jss::result][jss::error].asString() == + "malformedOwner"); + } + + { + testcase("RPC ledger_entry malformed seq"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault][jss::owner] = issuer.human(); + jvParams[jss::vault][jss::seq] = "foo"; + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT( + jvVault[jss::result][jss::error].asString() == + "malformedRequest"); + } + + { + testcase("RPC ledger_entry zero seq"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault][jss::owner] = issuer.human(); + jvParams[jss::vault][jss::seq] = 0; + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT( + jvVault[jss::result][jss::error].asString() == + "malformedRequest"); + } + + { + testcase("RPC ledger_entry negative seq"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault][jss::owner] = issuer.human(); + jvParams[jss::vault][jss::seq] = -1; + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT( + jvVault[jss::result][jss::error].asString() == + "malformedRequest"); + } + + { + testcase("RPC ledger_entry oversized seq"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault][jss::owner] = issuer.human(); + jvParams[jss::vault][jss::seq] = 1e20; + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT( + jvVault[jss::result][jss::error].asString() == + "malformedRequest"); + } + + { + testcase("RPC ledger_entry bool seq"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault][jss::owner] = issuer.human(); + jvParams[jss::vault][jss::seq] = true; + auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); + BEAST_EXPECT( + jvVault[jss::result][jss::error].asString() == + "malformedRequest"); + } + + { + testcase("RPC account_objects"); + + Json::Value jvParams; + jvParams[jss::account] = owner.human(); + jvParams[jss::type] = jss::vault; + auto jv = env.rpc( + "json", "account_objects", to_string(jvParams))[jss::result]; + + BEAST_EXPECT(jv[jss::account_objects].size() == 1); + check(jv[jss::account_objects][0u]); + } + + { + testcase("RPC ledger_data"); + + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::binary] = false; + jvParams[jss::type] = jss::vault; + Json::Value jv = + env.rpc("json", "ledger_data", to_string(jvParams)); + BEAST_EXPECT(jv[jss::result][jss::state].size() == 1); + check(jv[jss::result][jss::state][0u]); + } + + { + testcase("RPC vault_info command line"); + Json::Value jv = + env.rpc("vault_info", strHex(keylet.key), "validated"); + + BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); + BEAST_EXPECT(jv[jss::result].isMember(jss::vault)); + check( + jv[jss::result][jss::vault], + jv[jss::result][jss::vault][jss::shares]); + } + + { + testcase("RPC vault_info json"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault_id] = strHex(keylet.key); + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + + BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); + BEAST_EXPECT(jv[jss::result].isMember(jss::vault)); + check( + jv[jss::result][jss::vault], + jv[jss::result][jss::vault][jss::shares]); + } + + { + testcase("RPC vault_info invalid vault_id"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault_id] = "foobar"; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json invalid index"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault_id] = 0; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json by owner and sequence"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::owner] = owner.human(); + jvParams[jss::seq] = sequence; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + + BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); + BEAST_EXPECT(jv[jss::result].isMember(jss::vault)); + check( + jv[jss::result][jss::vault], + jv[jss::result][jss::vault][jss::shares]); + } + + { + testcase("RPC vault_info json malformed sequence"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::owner] = owner.human(); + jvParams[jss::seq] = "foobar"; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json invalid sequence"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::owner] = owner.human(); + jvParams[jss::seq] = 0; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json negative sequence"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::owner] = owner.human(); + jvParams[jss::seq] = -1; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json oversized sequence"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::owner] = owner.human(); + jvParams[jss::seq] = 1e20; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json bool sequence"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::owner] = owner.human(); + jvParams[jss::seq] = true; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json malformed owner"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::owner] = "foobar"; + jvParams[jss::seq] = sequence; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json invalid combination only owner"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::owner] = owner.human(); + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json invalid combination only seq"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::seq] = sequence; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json invalid combination seq vault_id"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault_id] = strHex(keylet.key); + jvParams[jss::seq] = sequence; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json invalid combination owner vault_id"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault_id] = strHex(keylet.key); + jvParams[jss::owner] = owner.human(); + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase( + "RPC vault_info json invalid combination owner seq " + "vault_id"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::vault_id] = strHex(keylet.key); + jvParams[jss::seq] = sequence; + jvParams[jss::owner] = owner.human(); + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info json no input"); + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + auto jv = env.rpc("json", "vault_info", to_string(jvParams)); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info command line invalid index"); + Json::Value jv = env.rpc("vault_info", "foobar", "validated"); + BEAST_EXPECT(jv[jss::error].asString() == "invalidParams"); + } + + { + testcase("RPC vault_info command line invalid index"); + Json::Value jv = env.rpc("vault_info", "0", "validated"); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "malformedRequest"); + } + + { + testcase("RPC vault_info command line invalid index"); + Json::Value jv = + env.rpc("vault_info", strHex(uint256(42)), "validated"); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "entryNotFound"); + } + + { + testcase("RPC vault_info command line invalid ledger"); + Json::Value jv = env.rpc("vault_info", strHex(keylet.key), "0"); + BEAST_EXPECT( + jv[jss::result][jss::error].asString() == "lgrNotFound"); + } + } + +public: + void + run() override + { + testSequences(); + testPreflight(); + testCreateFailXRP(); + testCreateFailIOU(); + testCreateFailMPT(); + testWithMPT(); + testWithIOU(); + testWithDomainCheck(); + testWithDomainCheckXRP(); + testNonTransferableShares(); + testFailedPseudoAccount(); + testRPC(); + } +}; + +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 306953d5ab..6ba1cfd6f1 100644 --- a/src/test/basics/IOUAmount_test.cpp +++ b/src/test/basics/IOUAmount_test.cpp @@ -44,6 +44,11 @@ public: 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 ef26ebf2ee..de6b83362d 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 589347b12c..344a2ab73c 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -21,7 +21,6 @@ #define RIPPLE_TEST_JTX_AMOUNT_H_INCLUDED #include -#include #include #include @@ -128,12 +127,23 @@ public: 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 @@ -151,6 +161,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 742b8744ef..a8daafba41 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/credentials.h b/src/test/jtx/credentials.h index 9161b7241d..1a72e2360d 100644 --- a/src/test/jtx/credentials.h +++ b/src/test/jtx/credentials.h @@ -30,6 +30,16 @@ namespace jtx { namespace credentials { +inline Keylet +keylet( + test::jtx::Account const& subject, + test::jtx::Account const& issuer, + std::string_view credType) +{ + return keylet::credential( + subject.id(), issuer.id(), Slice(credType.data(), credType.size())); +} + // Sets the optional URI. class uri { diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index ecb2d62f43..ac00d3eed1 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -603,6 +603,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 0000000000..663c42c6ee --- /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[sfVaultID] = 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[sfVaultID] = 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[sfVaultID] = 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[sfVaultID] = 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[sfVaultID] = 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 950ab0d409..52ade92323 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -88,11 +88,12 @@ public: 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 0000000000..74c482bf17 --- /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 18b037cbbe..7ceb76504d 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -78,8 +78,8 @@ class Invariants_test : public beast::unit_test::suite Preclose const& preclose = {}) { using namespace test::jtx; - FeatureBitset amendments = - supported_amendments() | featureInvariantsV1_1; + FeatureBitset amendments = supported_amendments() | + featureInvariantsV1_1 | featureSingleAssetVault; Env env{*this, amendments}; Account const A1{"A1"}; @@ -116,12 +116,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(); + } } } } @@ -784,7 +786,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. @@ -827,6 +829,74 @@ class Invariants_test : public beast::unit_test::suite }, XRPAmount{}, STTx{ttPAYMENT, [](STObject& tx) {}}); + + doInvariantCheck( + {{"pseudo-account created by a wrong transaction type"}}, + [](Account const&, Account const&, ApplyContext& ac) { + Account const A3{"A3"}; + Keylet const acctKeylet = keylet::account(A3); + auto const sleNew = std::make_shared(acctKeylet); + sleNew->setFieldU32(sfSequence, 0); + sleNew->setFieldH256(sfAMMID, uint256(1)); + sleNew->setFieldU32( + sfFlags, + lsfDisableMaster | lsfDefaultRipple | lsfDefaultRipple); + ac.view().insert(sleNew); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account created with wrong starting sequence number"}}, + [](Account const&, Account const&, ApplyContext& ac) { + Account const A3{"A3"}; + Keylet const acctKeylet = keylet::account(A3); + auto const sleNew = std::make_shared(acctKeylet); + sleNew->setFieldU32(sfSequence, ac.view().seq()); + sleNew->setFieldH256(sfAMMID, uint256(1)); + sleNew->setFieldU32( + sfFlags, + lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + ac.view().insert(sleNew); + return true; + }, + XRPAmount{}, + STTx{ttAMM_CREATE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"pseudo-account created with wrong flags"}}, + [](Account const&, Account const&, ApplyContext& ac) { + Account const A3{"A3"}; + Keylet const acctKeylet = keylet::account(A3); + auto const sleNew = std::make_shared(acctKeylet); + sleNew->setFieldU32(sfSequence, 0); + sleNew->setFieldH256(sfAMMID, uint256(1)); + sleNew->setFieldU32( + sfFlags, lsfDisableMaster | lsfDefaultRipple); + ac.view().insert(sleNew); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"pseudo-account created with wrong flags"}}, + [](Account const&, Account const&, ApplyContext& ac) { + Account const A3{"A3"}; + Keylet const acctKeylet = keylet::account(A3); + auto const sleNew = std::make_shared(acctKeylet); + sleNew->setFieldU32(sfSequence, 0); + sleNew->setFieldH256(sfAMMID, uint256(1)); + sleNew->setFieldU32( + sfFlags, + lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth | + lsfRequireDestTag); + ac.view().insert(sleNew); + return true; + }, + XRPAmount{}, + STTx{ttAMM_CREATE, [](STObject& tx) {}}); } void diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp index ed255e32f1..6f2c57ecb0 100644 --- a/src/test/protocol/STNumber_test.cpp +++ b/src/test/protocol/STNumber_test.cpp @@ -18,12 +18,15 @@ //============================================================================== #include +#include +#include #include #include #include #include #include +#include namespace ripple { @@ -78,6 +81,197 @@ struct STNumber_test : public beast::unit_test::suite STAmount const totalAmount{totalValue, strikePrice.issue()}; BEAST_EXPECT(totalAmount == Number{10'000}); } + + { + BEAST_EXPECT( + numberFromJson(sfNumber, Json::Value(42)) == + STNumber(sfNumber, 42)); + BEAST_EXPECT( + numberFromJson(sfNumber, Json::Value(-42)) == + STNumber(sfNumber, -42)); + + BEAST_EXPECT( + numberFromJson(sfNumber, Json::UInt(42)) == + STNumber(sfNumber, 42)); + + BEAST_EXPECT( + numberFromJson(sfNumber, "-123") == STNumber(sfNumber, -123)); + + BEAST_EXPECT( + numberFromJson(sfNumber, "123") == STNumber(sfNumber, 123)); + BEAST_EXPECT( + numberFromJson(sfNumber, "-123") == STNumber(sfNumber, -123)); + + BEAST_EXPECT( + numberFromJson(sfNumber, "3.14") == + STNumber(sfNumber, Number(314, -2))); + BEAST_EXPECT( + numberFromJson(sfNumber, "-3.14") == + STNumber(sfNumber, -Number(314, -2))); + BEAST_EXPECT( + numberFromJson(sfNumber, "3.14e2") == STNumber(sfNumber, 314)); + BEAST_EXPECT( + numberFromJson(sfNumber, "-3.14e2") == + STNumber(sfNumber, -314)); + + BEAST_EXPECT( + numberFromJson(sfNumber, "1000e-2") == STNumber(sfNumber, 10)); + BEAST_EXPECT( + numberFromJson(sfNumber, "-1000e-2") == + STNumber(sfNumber, -10)); + + BEAST_EXPECT( + numberFromJson(sfNumber, "0") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "0.0") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "0.000") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "-0") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "-0.0") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "-0.000") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "0e6") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "0.0e6") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "0.000e6") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "-0e6") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "-0.0e6") == STNumber(sfNumber, 0)); + BEAST_EXPECT( + numberFromJson(sfNumber, "-0.000e6") == STNumber(sfNumber, 0)); + + // Obvious non-numbers tested here + try + { + auto _ = numberFromJson(sfNumber, ""); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "'' is not a number"; + BEAST_EXPECT(e.what() == expected); + } + + try + { + auto _ = numberFromJson(sfNumber, "e"); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "'e' is not a number"; + BEAST_EXPECT(e.what() == expected); + } + + try + { + auto _ = numberFromJson(sfNumber, "1e"); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "'1e' is not a number"; + BEAST_EXPECT(e.what() == expected); + } + + try + { + auto _ = numberFromJson(sfNumber, "e2"); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "'e2' is not a number"; + BEAST_EXPECT(e.what() == expected); + } + + try + { + auto _ = numberFromJson(sfNumber, Json::Value()); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "not a number"; + BEAST_EXPECT(e.what() == expected); + } + + try + { + auto _ = numberFromJson( + sfNumber, + "1234567890123456789012345678901234567890123456789012345678" + "9012345678901234567890123456789012345678901234567890123456" + "78901234567890123456789012345678901234567890"); + BEAST_EXPECT(false); + } + catch (std::bad_cast const& e) + { + BEAST_EXPECT(true); + } + + // We do not handle leading zeros + try + { + auto _ = numberFromJson(sfNumber, "001"); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "'001' is not a number"; + BEAST_EXPECT(e.what() == expected); + } + + try + { + auto _ = numberFromJson(sfNumber, "000.0"); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "'000.0' is not a number"; + BEAST_EXPECT(e.what() == expected); + } + + // We do not handle dangling dot + try + { + auto _ = numberFromJson(sfNumber, ".1"); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "'.1' is not a number"; + BEAST_EXPECT(e.what() == expected); + } + + try + { + auto _ = numberFromJson(sfNumber, "1."); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "'1.' is not a number"; + BEAST_EXPECT(e.what() == expected); + } + + try + { + auto _ = numberFromJson(sfNumber, "1.e3"); + BEAST_EXPECT(false); + } + catch (std::runtime_error const& e) + { + std::string const expected = "'1.e3' is not a number"; + BEAST_EXPECT(e.what() == expected); + } + } } }; diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index 577f731200..724a3a0517 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -355,8 +355,7 @@ class Transaction_test : public beast::unit_test::suite } auto const tx = env.jt(noop(alice), seq(env.seq(alice))).stx; - auto const ctid = - *RPC::encodeCTID(endLegSeq, tx->getSeqProxy().value(), netID); + auto const ctid = *RPC::encodeCTID(endLegSeq, tx->getSeqValue(), netID); for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq) { auto const result = env.rpc( diff --git a/src/xrpld/app/misc/CredentialHelpers.cpp b/src/xrpld/app/misc/CredentialHelpers.cpp index dcc5975b34..03ad1f9c80 100644 --- a/src/xrpld/app/misc/CredentialHelpers.cpp +++ b/src/xrpld/app/misc/CredentialHelpers.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -39,12 +40,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. @@ -78,19 +78,19 @@ deleteSLE( AccountID const& account, SField const& node, bool isOwner) -> TER { auto const sleAccount = view.peek(keylet::account(account)); if (!sleAccount) - { + { // LCOV_EXCL_START JLOG(j.fatal()) << "Internal error: can't retrieve Owner account."; return tecINTERNAL; - } + } // LCOV_EXCL_STOP // Remove object from owner directory std::uint64_t const page = sleCredential->getFieldU64(node); if (!view.dirRemove( keylet::ownerDir(account), page, sleCredential->key(), false)) - { + { // LCOV_EXCL_START JLOG(j.fatal()) << "Unable to delete Credential from owner."; return tefBAD_LEDGER; - } + } // LCOV_EXCL_STOP if (isOwner) adjustOwnerCount(view, sleAccount, -1, j); @@ -186,30 +186,69 @@ 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 tecOBJECT_NOT_FOUND; + + auto const closeTime = view.info().parentCloseTime; + bool foundExpired = false; + for (auto const& h : slePD->getFieldArray(sfAcceptedCredentials)) + { + auto const issuer = h.getAccountID(sfIssuer); + auto const type = h.getFieldVL(sfCredentialType); + auto const keyletCredential = + keylet::credential(subject, issuer, makeSlice(type)); + auto const sleCredential = view.read(keyletCredential); + + // 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_AUTH; +} + +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)); - if (!sleCred) // already checked in preclaim - return tefINTERNAL; + auto sleCred = view.read(keylet::credential(h)); + if (!sleCred) // already checked in preclaim + return tefINTERNAL; // LCOV_EXCL_LINE auto [it, ins] = sorted.emplace((*sleCred)[sfIssuer], (*sleCred)[sfCredentialType]); if (!ins) - return tefINTERNAL; + return tefINTERNAL; // LCOV_EXCL_LINE 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; } @@ -273,6 +312,46 @@ 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) + return tecOBJECT_NOT_FOUND; + + // 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)) + { + auto const issuer = h.getAccountID(sfIssuer); + auto const type = h.getFieldVL(sfCredentialType); + auto const keyletCredential = + keylet::credential(account, issuer, makeSlice(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, @@ -289,7 +368,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)) @@ -297,8 +377,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 8b52acf54e..162ddd6515 100644 --- a/src/xrpld/app/misc/CredentialHelpers.h +++ b/src/xrpld/app/misc/CredentialHelpers.h @@ -35,9 +35,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 @@ -50,14 +50,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> @@ -70,6 +81,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/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 6f29f79384..d87dea3c52 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -3711,7 +3711,7 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo) { auto stx = tx->getSTransaction(); if (stx->getAccountID(sfAccount) == accountId && - stx->getSeqProxy().value() == 1) + stx->getSeqValue() == 1) return true; } diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index deafa6f27a..95cb5bf2e6 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -183,6 +183,14 @@ AMMCreate::preclaim(PreclaimContext const& ctx) return tecAMM_INVALID_TOKENS; } + if (ctx.view.rules().enabled(featureSingleAssetVault)) + { + if (auto const accountId = pseudoAccountAddress( + ctx.view, keylet::amm(amount.issue(), amount2.issue()).key); + accountId == beast::zero) + return terADDRESS_COLLISION; + } + // If featureAMMClawback is enabled, allow AMMCreate without checking // if the issuer has clawback enabled if (ctx.view.rules().enabled(featureAMMClawback)) @@ -219,64 +227,37 @@ 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, sfAMMID); // 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}); @@ -286,22 +267,15 @@ 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}; + return {ter, false}; } 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; @@ -312,7 +286,7 @@ applyCreate( if (auto const res = accountSend( sb, account_, - *ammAccount, + accountId, amount, ctx_.journal, WaiveTransferFee::Yes)) @@ -321,7 +295,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 @@ -350,7 +324,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 468adbd209..cccda83a68 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/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index e58faf2286..41ab1256fb 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -207,7 +207,12 @@ Clawback::preclaim(PreclaimContext const& ctx) if (!sleIssuer || !sleHolder) return terNO_ACCOUNT; - if (sleHolder->isFieldPresent(sfAMMID)) + // Note the order of checks - when SAV is active, this check here will make + // the one which follows `sleHolder->isFieldPresent(sfAMMID)` redundant. + if (ctx.view.rules().enabled(featureSingleAssetVault) && + isPseudoAccount(sleHolder)) + return tecPSEUDO_ACCOUNT; + else if (sleHolder->isFieldPresent(sfAMMID)) return tecAMM_ACCOUNT; return std::visit( diff --git a/src/xrpld/app/tx/detail/CreateCheck.cpp b/src/xrpld/app/tx/detail/CreateCheck.cpp index 19ef28b843..9baceef944 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.cpp +++ b/src/xrpld/app/tx/detail/CreateCheck.cpp @@ -97,8 +97,11 @@ CreateCheck::preclaim(PreclaimContext const& ctx) (flags & lsfDisallowIncomingCheck)) return tecNO_PERMISSION; - // AMM can not cash the check - if (sleDst->isFieldPresent(sfAMMID)) + // Pseudo-accounts cannot cash checks. Note, this is not amendment-gated + // because all writes to pseudo-account discriminator fields **are** + // amendment gated, hence the behaviour of this check will always match the + // currently active amendments. + if (isPseudoAccount(sleDst)) return tecNO_PERMISSION; if ((flags & lsfRequireDestTag) && !ctx.tx.isFieldPresent(sfDestinationTag)) @@ -184,7 +187,7 @@ CreateCheck::doApply() // Note that we use the value from the sequence or ticket as the // Check sequence. For more explanation see comments in SeqProxy.h. - std::uint32_t const seq = ctx_.tx.getSeqProxy().value(); + std::uint32_t const seq = ctx_.tx.getSeqValue(); Keylet const checkKeylet = keylet::check(account_, seq); auto sleCheck = std::make_shared(checkKeylet); diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 92ba54f077..d9bd57ec3c 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -969,7 +969,7 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) // Note that we we use the value from the sequence or ticket as the // offer sequence. For more explanation see comments in SeqProxy.h. - auto const offerSequence = ctx_.tx.getSeqProxy().value(); + auto const offerSequence = ctx_.tx.getSeqValue(); // This is the original rate of the offer, and is the rate at which // it will be placed, even if crossing offers change the amounts that diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index bc9ad0a11f..0b58957fcf 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -148,7 +148,12 @@ EscrowCreate::preclaim(PreclaimContext const& ctx) auto const sled = ctx.view.read(keylet::account(ctx.tx[sfDestination])); if (!sled) return tecNO_DST; - if (sled->isFieldPresent(sfAMMID)) + + // Pseudo-accounts cannot receive escrow. Note, this is not amendment-gated + // because all writes to pseudo-account discriminator fields **are** + // amendment gated, hence the behaviour of this check will always match the + // currently active amendments. + if (isPseudoAccount(sled)) return tecNO_PERMISSION; return tesSUCCESS; @@ -228,8 +233,7 @@ EscrowCreate::doApply() // Create escrow in ledger. Note that we we use the value from the // sequence or ticket. For more explanation see comments in SeqProxy.h. - Keylet const escrowKeylet = - keylet::escrow(account, ctx_.tx.getSeqProxy().value()); + Keylet const escrowKeylet = keylet::escrow(account, ctx_.tx.getSeqValue()); auto const slep = std::make_shared(escrowKeylet); (*slep)[sfAmount] = ctx_.tx[sfAmount]; (*slep)[sfAccount] = account; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 2441cb040a..aa1464ec2a 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) @@ -490,6 +491,7 @@ LedgerEntryTypesMatch::visitEntry( case ltMPTOKEN: case ltCREDENTIAL: case ltPERMISSIONED_DOMAIN: + case ltVAULT: break; default: invalidTypeAdded_ = true; @@ -884,6 +886,8 @@ ValidNewAccountRoot::visitEntry( { accountsCreated_++; accountSeq_ = (*after)[sfSequence]; + pseudoAccount_ = isPseudoAccount(after); + flags_ = after->getFlags(); } } @@ -907,12 +911,28 @@ 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) { - std::uint32_t const startingSeq{ - view.rules().enabled(featureDeletableAccounts) ? view.seq() : 1}; + bool const pseudoAccount = + (pseudoAccount_ && view.rules().enabled(featureSingleAssetVault)); + + if (pseudoAccount && tx.getTxnType() != ttAMM_CREATE && + tx.getTxnType() != ttVAULT_CREATE) + { + JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a " + "wrong transaction type"; + return false; + } + + std::uint32_t const startingSeq = // + pseudoAccount // + ? 0 // + : view.rules().enabled(featureDeletableAccounts) // + ? view.seq() // + : 1; if (accountSeq_ != startingSeq) { @@ -920,12 +940,24 @@ ValidNewAccountRoot::finalize( "wrong starting sequence number"; return false; } + + if (pseudoAccount) + { + std::uint32_t const expected = + (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + if (flags_ != expected) + { + JLOG(j.fatal()) + << "Invariant failed: pseudo-account created with " + "wrong flags"; + return false; + } + } + 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; } @@ -1313,28 +1345,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) { @@ -1355,7 +1389,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); @@ -1381,7 +1416,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/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index cb06b0fb05..6819780114 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -438,6 +438,8 @@ class ValidNewAccountRoot { std::uint32_t accountsCreated_ = 0; std::uint32_t accountSeq_ = 0; + bool pseudoAccount_ = false; + std::uint32_t flags_ = 0; public: void diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index cfc098ab0f..748c05869f 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -83,6 +83,10 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tecHAS_OBLIGATIONS; } + if (ctx.view.rules().enabled(featureSingleAssetVault) && + sleMpt->isFlag(lsfMPTLocked)) + return tecNO_PERMISSION; + return tesSUCCESS; } @@ -160,14 +164,14 @@ MPTokenAuthorize::authorize( keylet::mptoken(args.mptIssuanceID, args.account); auto const sleMpt = view.peek(mptokenKey); if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0) - return tecINTERNAL; + return tecINTERNAL; // LCOV_EXCL_LINE if (!view.dirRemove( keylet::ownerDir(args.account), (*sleMpt)[sfOwnerNode], sleMpt->key(), false)) - return tecINTERNAL; + return tecINTERNAL; // LCOV_EXCL_LINE adjustOwnerCount(view, sleAcct, -1, journal); @@ -194,20 +198,13 @@ MPTokenAuthorize::authorize( 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); + if (auto ter = dirLink(view, args.account, mptoken)) + return ter; // LCOV_EXCL_LINE + (*mptoken)[sfAccount] = args.account; (*mptoken)[sfMPTokenIssuanceID] = args.mptIssuanceID; (*mptoken)[sfFlags] = 0; - (*mptoken)[sfOwnerNode] = *ownerNode; view.insert(mptoken); // Update owner count. diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 79dc1734b5..e2b135a22a 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; + MPTID const& mptIssuanceID; AccountID const& account; - std::uint32_t flags; - std::optional holderID; + 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 1bd3fcadd7..1b96b27f24 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,15 @@ MPTokenIssuanceCreate::create( { auto const acct = view.peek(keylet::account(args.account)); if (!acct) - return tecINTERNAL; + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE - if (args.priorBalance < - view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) - return tecINSUFFICIENT_RESERVE; + if (args.priorBalance && + *(args.priorBalance) < + view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) + return Unexpected(tecINSUFFICIENT_RESERVE); - auto const mptIssuanceKeylet = - keylet::mptIssuance(args.sequence, args.account); + auto const mptId = makeMptID(args.sequence, args.account); + auto const mptIssuanceKeylet = keylet::mptIssuance(mptId); // create the MPTokenIssuance { @@ -92,7 +93,7 @@ MPTokenIssuanceCreate::create( describeOwnerDir(args.account)); if (!ownerNode) - return tecDIR_FULL; + return Unexpected(tecDIR_FULL); // LCOV_EXCL_LINE auto mptIssuance = std::make_shared(mptIssuanceKeylet); (*mptIssuance)[sfFlags] = args.flags & ~tfUniversal; @@ -113,30 +114,36 @@ 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_, - .sequence = tx.getSeqProxy().value(), - .flags = tx.getFlags(), - .maxAmount = tx[~sfMaximumAmount], - .assetScale = tx[~sfAssetScale], - .transferFee = tx[~sfTransferFee], - .metadata = tx[~sfMPTokenMetadata]}); + auto const result = create( + view(), + j_, + { + .priorBalance = mPriorBalance, + .account = account_, + .sequence = tx.getSeqValue(), + .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 1346c3e31d..ea01908dff 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -22,18 +22,22 @@ #include +#include +#include + namespace ripple { struct MPTCreateArgs { - XRPAmount const& priorBalance; + std::optional 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 +55,7 @@ public: 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 ed5d3c4f96..d06ea3473e 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -55,7 +55,7 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; // ensure it has no outstanding balances - if ((*sleMPT)[~sfOutstandingAmount] != 0) + if ((*sleMPT)[sfOutstandingAmount] != 0) return tecHAS_OBLIGATIONS; return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index 25cdd0e69a..a42902f6ac 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -237,7 +237,13 @@ PayChanCreate::preclaim(PreclaimContext const& ctx) (flags & lsfDisallowXRP)) return tecNO_TARGET; - if (sled->isFieldPresent(sfAMMID)) + // Pseudo-accounts cannot receive payment channels, other than native + // to their underlying ledger object - implemented in their respective + // transaction types. Note, this is not amendment-gated because all + // writes to pseudo-account discriminator fields **are** amendment + // gated, hence the behaviour of this check will always match the + // currently active amendments. + if (isPseudoAccount(sled)) return tecNO_PERMISSION; } @@ -266,7 +272,7 @@ PayChanCreate::doApply() // Note that we we use the value from the sequence or ticket as the // payChan sequence. For more explanation see comments in SeqProxy.h. Keylet const payChanKeylet = - keylet::payChan(account, dst, ctx_.tx.getSeqProxy().value()); + keylet::payChan(account, dst, ctx_.tx.getSeqValue()); auto const slep = std::make_shared(payChanKeylet); // Funds held in this channel diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index f2f4ac4f7c..a97e472841 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -520,8 +520,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. @@ -591,9 +590,12 @@ Payment::doApply() return tecUNFUNDED_PAYMENT; } - // AMMs can never receive an XRP payment. - // Must use AMMDeposit transaction instead. - if (sleDst->isFieldPresent(sfAMMID)) + // Pseudo-accounts cannot receive payments, other than these native to + // their underlying ledger object - implemented in their respective + // transaction types. Note, this is not amendment-gated because all writes + // to pseudo-account discriminator fields **are** amendment gated, hence the + // behaviour of this check will always match the active amendments. + if (isPseudoAccount(sleDst)) return tecNO_PERMISSION; // The source account does have enough money. Make sure the diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 9fe267b8e1..5e83c201fa 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include namespace { @@ -241,14 +243,16 @@ SetTrust::preclaim(PreclaimContext const& ctx) // This might be nullptr auto const sleDst = ctx.view.read(keylet::account(uDstAccountID)); + if ((ctx.view.rules().enabled(featureDisallowIncoming) || + ammEnabled(ctx.view.rules()) || + ctx.view.rules().enabled(featureSingleAssetVault)) && + sleDst == nullptr) + return tecNO_DST; // If the destination has opted to disallow incoming trustlines // then honour that flag if (ctx.view.rules().enabled(featureDisallowIncoming)) { - if (!sleDst) - return tecNO_DST; - if (sleDst->getFlags() & lsfDisallowIncomingTrustline) { // The original implementation of featureDisallowIncoming was @@ -266,18 +270,22 @@ SetTrust::preclaim(PreclaimContext const& ctx) } } - // If destination is AMM and the trustline doesn't exist then only - // allow SetTrust if the asset is AMM LP token and AMM is not - // in empty state. - if (ammEnabled(ctx.view.rules())) + // In general, trust lines to pseudo accounts are not permitted, unless + // enabled in the code section below, for specific cases. This block is not + // amendment-gated because sleDst will not have a pseudo-account designator + // field populated, unless the appropriate amendment was already enabled. + if (sleDst && isPseudoAccount(sleDst)) { - if (!sleDst) - return tecNO_DST; - - if (sleDst->isFieldPresent(sfAMMID) && - !ctx.view.read(keylet::line(id, uDstAccountID, currency))) + // If destination is AMM and the trustline doesn't exist then only allow + // SetTrust if the asset is AMM LP token and AMM is not in empty state. + if (sleDst->isFieldPresent(sfAMMID)) { - if (auto const ammSle = + if (ctx.view.exists(keylet::line(id, uDstAccountID, currency))) + { + // pass + } + else if ( + auto const ammSle = ctx.view.read({ltAMM, sleDst->getFieldH256(sfAMMID)})) { if (auto const lpTokens = @@ -288,8 +296,16 @@ SetTrust::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } else - return tecINTERNAL; + return tecINTERNAL; // LCOV_EXCL_LINE } + else if (sleDst->isFieldPresent(sfVaultID)) + { + if (!ctx.view.exists(keylet::line(id, uDstAccountID, currency))) + return tecNO_PERMISSION; + // else pass + } + else + return tecPSEUDO_ACCOUNT; } // Checking all freeze/deep freeze flag invariants. diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp new file mode 100644 index 0000000000..f9bd0c7629 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -0,0 +1,239 @@ +//------------------------------------------------------------------------------ +/* + 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 + +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; + + if (ctx.tx[sfVaultID] == beast::zero) + { + JLOG(ctx.j.debug()) << "VaultClawback: zero/empty vault ID."; + return temMALFORMED; + } + + AccountID const issuer = ctx.tx[sfAccount]; + AccountID const holder = ctx.tx[sfHolder]; + + if (issuer == holder) + { + JLOG(ctx.j.debug()) << "VaultClawback: issuer cannot be 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())) + { + JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP."; + return temMALFORMED; + } + else if (amount->asset().getIssuer() != issuer) + { + JLOG(ctx.j.debug()) + << "VaultClawback: only asset issuer can clawback."; + 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 tecNO_ENTRY; + + auto account = ctx.tx[sfAccount]; + auto const issuer = ctx.view.read(keylet::account(account)); + if (!issuer) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) << "VaultClawback: missing issuer account."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + Asset const vaultAsset = vault->at(sfAsset); + if (auto const amount = ctx.tx[~sfAmount]; + amount && vaultAsset != amount->asset()) + return tecWRONG_ASSET; + + if (vaultAsset.native()) + { + JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP."; + return tecNO_PERMISSION; // Cannot clawback XRP. + } + else if (vaultAsset.getIssuer() != account) + { + JLOG(ctx.j.debug()) << "VaultClawback: only asset issuer can clawback."; + return tecNO_PERMISSION; // Only issuers can clawback. + } + + if (vaultAsset.holds()) + { + auto const mpt = vaultAsset.get(); + auto const mptIssue = + ctx.view.read(keylet::mptIssuance(mpt.getMptID())); + if (mptIssue == nullptr) + return tecOBJECT_NOT_FOUND; + + std::uint32_t const issueFlags = mptIssue->getFieldU32(sfFlags); + if (!(issueFlags & lsfMPTCanClawback)) + { + JLOG(ctx.j.debug()) + << "VaultClawback: cannot clawback MPT vault asset."; + return tecNO_PERMISSION; + } + } + else if (vaultAsset.holds()) + { + std::uint32_t const issuerFlags = issuer->getFieldU32(sfFlags); + if (!(issuerFlags & lsfAllowTrustLineClawback) || + (issuerFlags & lsfNoFreeze)) + { + JLOG(ctx.j.debug()) + << "VaultClawback: cannot clawback IOU vault asset."; + 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 tefINTERNAL; // LCOV_EXCL_LINE + + auto const mptIssuanceID = (*vault)[sfShareMPTID]; + auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultClawback: missing issuance of vault shares."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + 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)[sfShareMPTID]; + 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(sfAssetsAvailable); + if (assets > maxAssets) + { + assets = maxAssets; + shares = assetsToSharesWithdraw(vault, sleIssuance, assets); + } + + if (shares == beast::zero) + return tecINSUFFICIENT_FUNDS; + + vault->at(sfAssetsTotal) -= assets; + vault->at(sfAssetsAvailable) -= 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_, WaiveTransferFee::Yes)) + return ter; + + // Transfer assets from vault to issuer. + if (auto ter = accountSend( + view(), vaultAccount, account_, assets, j_, WaiveTransferFee::Yes)) + return ter; + + // Sanity check + if (accountHolds( + view(), + vaultAccount, + assets.asset(), + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_) < beast::zero) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultClawback: negative balance of vault assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + 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 0000000000..65f0164686 --- /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 0000000000..cb6a994e7e --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -0,0 +1,244 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +namespace ripple { + +NotTEC +VaultCreate::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureSingleAssetVault) || + !ctx.rules.enabled(featureMPTokensV1)) + 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 withdrawalPolicy = ctx.tx[~sfWithdrawalPolicy]) + { + // Enforce valid withdrawal policy + if (*withdrawalPolicy != vaultStrategyFirstComeFirstServe) + return temMALFORMED; + } + + if (auto const domain = ctx.tx[~sfDomainID]) + { + if (*domain == beast::zero) + return temMALFORMED; + else if ((ctx.tx.getFlags() & tfVaultPrivate) == 0) + return temMALFORMED; // DomainID only allowed on private vaults + } + + if (auto const assetMax = ctx.tx[~sfAssetsMaximum]) + { + if (*assetMax < beast::zero) + return temMALFORMED; + } + + 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 vaultAsset = ctx.tx[sfAsset]; + auto account = ctx.tx[sfAccount]; + + if (vaultAsset.native()) + ; // No special checks for XRP + else if (vaultAsset.holds()) + { + auto mptID = vaultAsset.get().getMptID(); + auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; + if (!issuance->isFlag(lsfMPTCanTransfer)) + { + // NOTE: flag lsfMPTCanTransfer is immutable, so this is debug in + // VaultCreate only; in other vault function it's an error. + JLOG(ctx.j.debug()) + << "VaultCreate: vault assets are non-transferable."; + return tecNO_AUTH; + } + } + else if (vaultAsset.holds()) + { + auto const issuer = + ctx.view.read(keylet::account(vaultAsset.getIssuer())); + if (!issuer) + return terNO_ACCOUNT; + else if (!issuer->isFlag(lsfDefaultRipple)) + return terNO_RIPPLE; + } + + // Check for pseudo-account issuers - we do not want a vault to hold such + // assets (e.g. MPT shares to other vaults or AMM LPTokens) as they would be + // impossible to clawback (should the need arise) + if (!vaultAsset.native()) + { + if (isPseudoAccount(ctx.view, vaultAsset.getIssuer())) + return tecWRONG_ASSET; + } + + // Cannot create Vault for an Asset frozen for the vault owner + if (isFrozen(ctx.view, account, vaultAsset)) + return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + + if (auto const domain = ctx.tx[~sfDomainID]) + { + auto const sleDomain = + ctx.view.read(keylet::permissionedDomain(*domain)); + if (!sleDomain) + return tecOBJECT_NOT_FOUND; + } + + auto sequence = ctx.tx.getSeqValue(); + if (auto const accountId = pseudoAccountAddress( + ctx.view, keylet::vault(account, sequence).key); + accountId == beast::zero) + return terADDRESS_COLLISION; + + 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 sequence = tx.getSeqValue(); + auto owner = view().peek(keylet::account(account_)); + if (owner == nullptr) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto vault = std::make_shared(keylet::vault(account_, sequence)); + + if (auto ter = dirLink(view(), account_, vault)) + return ter; + 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(), sfVaultID); + if (!maybePseudo) + return maybePseudo.error(); // LCOV_EXCL_LINE + auto& pseudo = *maybePseudo; + auto pseudoId = pseudo->at(sfAccount); + auto asset = tx[sfAsset]; + + if (auto ter = addEmptyHolding(view(), pseudoId, mPriorBalance, asset, j_); + !isTesSuccess(ter)) + return ter; + + auto txFlags = tx.getFlags(); + std::uint32_t mptFlags = 0; + if ((txFlags & tfVaultShareNonTransferable) == 0) + mptFlags |= (lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer); + if (txFlags & tfVaultPrivate) + mptFlags |= lsfMPTRequireAuth; + + // Note, here we are **not** creating an MPToken for the assets held in + // the vault. That MPToken or TrustLine/RippleState is created above, in + // addEmptyHolding. Here we are creating MPTokenIssuance for the shares + // in the vault + auto maybeShare = MPTokenIssuanceCreate::create( + view(), + j_, + { + .priorBalance = std::nullopt, + .account = pseudoId->value(), + .sequence = 1, + .flags = mptFlags, + .metadata = tx[~sfMPTokenMetadata], + .domainId = tx[~sfDomainID], + }); + if (!maybeShare) + return maybeShare.error(); // LCOV_EXCL_LINE + auto& share = *maybeShare; + + vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset}); + vault->at(sfFlags) = txFlags & tfVaultPrivate; + vault->at(sfSequence) = sequence; + vault->at(sfOwner) = account_; + vault->at(sfAccount) = pseudoId; + vault->at(sfAssetsTotal) = Number(0); + vault->at(sfAssetsAvailable) = Number(0); + vault->at(sfLossUnrealized) = Number(0); + // Leave default values for AssetTotal and AssetAvailable, both zero. + if (auto value = tx[~sfAssetsMaximum]) + vault->at(sfAssetsMaximum) = *value; + vault->at(sfShareMPTID) = 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 0000000000..5555644629 --- /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 0000000000..7861e9e9b6 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultDelete.cpp @@ -0,0 +1,189 @@ +//------------------------------------------------------------------------------ +/* + 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; + + if (ctx.tx[sfVaultID] == beast::zero) + { + JLOG(ctx.j.debug()) << "VaultDelete: zero/empty vault ID."; + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +VaultDelete::preclaim(PreclaimContext const& ctx) +{ + auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID])); + if (!vault) + return tecNO_ENTRY; + + if (vault->at(sfOwner) != ctx.tx[sfAccount]) + { + JLOG(ctx.j.debug()) << "VaultDelete: account is not an owner."; + return tecNO_PERMISSION; + } + + if (vault->at(sfAssetsAvailable) != 0) + { + JLOG(ctx.j.debug()) << "VaultDelete: nonzero assets available."; + return tecHAS_OBLIGATIONS; + } + + if (vault->at(sfAssetsTotal) != 0) + { + JLOG(ctx.j.debug()) << "VaultDelete: nonzero assets total."; + return tecHAS_OBLIGATIONS; + } + + // Verify we can destroy MPTokenIssuance + auto const sleMPT = + ctx.view.read(keylet::mptIssuance(vault->at(sfShareMPTID))); + + if (!sleMPT) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultDeposit: missing issuance of vault shares."; + return tecOBJECT_NOT_FOUND; + // LCOV_EXCL_STOP + } + + if (sleMPT->at(sfIssuer) != vault->getAccountID(sfAccount)) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) << "VaultDeposit: invalid owner of vault shares."; + return tecNO_PERMISSION; + // LCOV_EXCL_STOP + } + + if (sleMPT->at(sfOutstandingAmount) != 0) + { + JLOG(ctx.j.debug()) << "VaultDelete: nonzero outstanding shares."; + return tecHAS_OBLIGATIONS; + } + + return tesSUCCESS; +} + +TER +VaultDelete::doApply() +{ + auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + if (!vault) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Destroy the asset holding. + auto asset = vault->at(sfAsset); + if (auto ter = removeEmptyHolding(view(), vault->at(sfAccount), asset, j_); + !isTesSuccess(ter)) + return ter; + + auto const& pseudoID = vault->at(sfAccount); + auto const pseudoAcct = view().peek(keylet::account(pseudoID)); + if (!pseudoAcct) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultDelete: missing vault pseudo-account."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + // Destroy the share issuance. Do not use MPTokenIssuanceDestroy for this, + // no special logic needed. First run few checks, duplicated from preclaim. + auto const mpt = view().peek(keylet::mptIssuance(vault->at(sfShareMPTID))); + if (!mpt) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultDelete: missing issuance of vault shares."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + if (!view().dirRemove( + keylet::ownerDir(pseudoID), (*mpt)[sfOwnerNode], mpt->key(), false)) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultDelete: failed to delete issuance object."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + adjustOwnerCount(view(), pseudoAcct, -1, j_); + + view().erase(mpt); + + // The pseudo-account's directory should have been deleted already. + if (view().peek(keylet::ownerDir(pseudoID))) + return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE + + // 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)) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultDelete: failed to delete vault object."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + auto const owner = view().peek(keylet::account(ownerID)); + if (!owner) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultDelete: missing vault owner account."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + 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 0000000000..2b77e84469 --- /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 0000000000..0efddb0ff7 --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -0,0 +1,283 @@ +//------------------------------------------------------------------------------ +/* + 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[sfVaultID] == beast::zero) + { + JLOG(ctx.j.debug()) << "VaultDeposit: zero/empty vault ID."; + return temMALFORMED; + } + + 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 tecNO_ENTRY; + + auto const account = ctx.tx[sfAccount]; + auto const assets = ctx.tx[sfAmount]; + auto const vaultAsset = vault->at(sfAsset); + if (assets.asset() != vaultAsset) + return tecWRONG_ASSET; + + if (vaultAsset.native()) + ; // No special checks for XRP + else if (vaultAsset.holds()) + { + auto mptID = vaultAsset.get().getMptID(); + auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; + if (!issuance->isFlag(lsfMPTCanTransfer)) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultDeposit: vault assets are non-transferable."; + return tecNO_AUTH; + // LCOV_EXCL_STOP + } + } + else if (vaultAsset.holds()) + { + auto const issuer = + ctx.view.read(keylet::account(vaultAsset.getIssuer())); + if (!issuer) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultDeposit: missing issuer of vault assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + } + + auto const mptIssuanceID = vault->at(sfShareMPTID); + auto const vaultShare = MPTIssue(mptIssuanceID); + if (vaultShare == assets.asset()) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultDeposit: vault shares and assets cannot be same."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultDeposit: missing issuance of vault shares."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + if (sleIssuance->isFlag(lsfMPTLocked)) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultDeposit: issuance of vault shares is locked."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + // Cannot deposit inside Vault an Asset frozen for the depositor + if (isFrozen(ctx.view, account, vaultAsset)) + return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + + // Cannot deposit if the shares of the vault are frozen + if (isFrozen(ctx.view, account, vaultShare)) + return tecLOCKED; + + if (vault->isFlag(tfVaultPrivate) && account != vault->at(sfOwner)) + { + auto const maybeDomainID = sleIssuance->at(~sfDomainID); + // Since this is a private vault and the account is not its owner, we + // perform authorization check based on DomainID read from sleIssuance. + // Had the vault shares been a regular MPToken, we would allow + // authorization granted by the Issuer explicitly, but Vault uses Issuer + // pseudo-account, which cannot grant an authorization. + if (maybeDomainID) + { + // As per validDomain documentation, we suppress tecEXPIRED error + // here, so we can delete any expired credentials inside doApply. + if (auto const err = + credentials::validDomain(ctx.view, *maybeDomainID, account); + !isTesSuccess(err) && err != tecEXPIRED) + return err; + } + else + return tecNO_AUTH; + } + + // Source MPToken must exist (if asset is an MPT) + if (auto const ter = requireAuth(ctx.view, vaultAsset, account); + !isTesSuccess(ter)) + return ter; + + if (accountHolds( + ctx.view, + account, + vaultAsset, + FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahZERO_IF_UNAUTHORIZED, + ctx.j) < assets) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +TER +VaultDeposit::doApply() +{ + auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + if (!vault) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const assets = ctx_.tx[sfAmount]; + // Make sure the depositor can hold shares. + auto const mptIssuanceID = (*vault)[sfShareMPTID]; + auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultDeposit: missing issuance of vault shares."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + auto const& vaultAccount = vault->at(sfAccount); + // Note, vault owner is always authorized + if ((vault->getFlags() & tfVaultPrivate) && account_ != vault->at(sfOwner)) + { + if (auto const err = enforceMPTokenAuthorization( + ctx_.view(), mptIssuanceID, 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) + { + if (auto const err = MPTokenAuthorize::authorize( + view(), + ctx_.journal, + {.priorBalance = mPriorBalance, + .mptIssuanceID = mptIssuanceID->value(), + .account = account_}); + !isTesSuccess(err)) + return err; + } + + // If the vault is private, set the authorized flag for the vault owner + if (vault->isFlag(tfVaultPrivate)) + { + if (auto const err = MPTokenAuthorize::authorize( + view(), + ctx_.journal, + { + .priorBalance = mPriorBalance, + .mptIssuanceID = mptIssuanceID->value(), + .account = sleIssuance->at(sfIssuer), + .holderID = 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(sfAssetsTotal) += assets; + vault->at(sfAssetsAvailable) += assets; + view().update(vault); + + // A deposit must not push the vault over its limit. + auto const maximum = *vault->at(sfAssetsMaximum); + if (maximum != 0 && *vault->at(sfAssetsTotal) > maximum) + return tecLIMIT_EXCEEDED; + + // Transfer assets from depositor to vault. + if (auto ter = accountSend( + view(), account_, vaultAccount, assets, j_, WaiveTransferFee::Yes)) + return ter; + + // Sanity check + if (accountHolds( + view(), + account_, + assets.asset(), + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_) < beast::zero) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultDeposit: negative balance of account assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + // Transfer shares from vault to depositor. + if (auto ter = accountSend( + view(), vaultAccount, account_, shares, j_, WaiveTransferFee::Yes)) + 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 0000000000..50515ce3d8 --- /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 0000000000..a13ce6d10e --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -0,0 +1,197 @@ +//------------------------------------------------------------------------------ +/* + 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 + +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[sfVaultID] == beast::zero) + { + JLOG(ctx.j.debug()) << "VaultSet: zero/empty vault ID."; + return temMALFORMED; + } + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + if (auto const data = ctx.tx[~sfData]) + { + if (data->empty() || data->length() > maxDataPayloadLength) + { + JLOG(ctx.j.debug()) << "VaultSet: invalid data payload size."; + return temMALFORMED; + } + } + + if (auto const assetMax = ctx.tx[~sfAssetsMaximum]) + { + if (*assetMax < beast::zero) + { + JLOG(ctx.j.debug()) << "VaultSet: invalid max assets."; + return temMALFORMED; + } + } + + if (!ctx.tx.isFieldPresent(sfDomainID) && + !ctx.tx.isFieldPresent(sfAssetsMaximum) && + !ctx.tx.isFieldPresent(sfData)) + { + JLOG(ctx.j.debug()) << "VaultSet: nothing is being updated."; + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +VaultSet::preclaim(PreclaimContext const& ctx) +{ + auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID])); + if (!vault) + return tecNO_ENTRY; + + // Assert that submitter is the Owner. + if (ctx.tx[sfAccount] != vault->at(sfOwner)) + { + JLOG(ctx.j.debug()) << "VaultSet: account is not an owner."; + return tecNO_PERMISSION; + } + + auto const mptIssuanceID = (*vault)[sfShareMPTID]; + auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) << "VaultSet: missing issuance of vault shares."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + if (auto const domain = ctx.tx[~sfDomainID]) + { + // We can only set domain if private flag was originally set + if ((vault->getFlags() & tfVaultPrivate) == 0) + { + JLOG(ctx.j.debug()) << "VaultSet: vault is not private"; + return tecNO_PERMISSION; + } + + if (*domain != beast::zero) + { + auto const sleDomain = + ctx.view.read(keylet::permissionedDomain(*domain)); + if (!sleDomain) + return tecOBJECT_NOT_FOUND; + } + + // Sanity check only, this should be enforced by VaultCreate + if ((sleIssuance->getFlags() & lsfMPTRequireAuth) == 0) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultSet: issuance of vault shares is not private."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + } + + 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(keylet::vault(tx[sfVaultID])); + if (!vault) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const mptIssuanceID = (*vault)[sfShareMPTID]; + auto const sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultSet: missing issuance of vault shares."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + // Update mutable flags and fields if given. + if (tx.isFieldPresent(sfData)) + vault->at(sfData) = tx[sfData]; + if (tx.isFieldPresent(sfAssetsMaximum)) + { + if (tx[sfAssetsMaximum] != 0 && + tx[sfAssetsMaximum] < *vault->at(sfAssetsTotal)) + return tecLIMIT_EXCEEDED; + vault->at(sfAssetsMaximum) = tx[sfAssetsMaximum]; + } + + if (auto const domainId = tx[~sfDomainID]; domainId) + { + if (*domainId != beast::zero) + { + // 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). The + // sfDomainID flag must be set in the MPTokenIssuance object and can + // be freely updated. + sleIssuance->setFieldH256(sfDomainID, *domainId); + } + else if (sleIssuance->isFieldPresent(sfDomainID)) + { + sleIssuance->makeFieldAbsent(sfDomainID); + } + view().update(sleIssuance); + } + + 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 0000000000..f16aa6c284 --- /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 0000000000..7a8605cdbd --- /dev/null +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -0,0 +1,258 @@ +//------------------------------------------------------------------------------ +/* + 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 + +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[sfVaultID] == beast::zero) + { + JLOG(ctx.j.debug()) << "VaultWithdraw: zero/empty vault ID."; + return temMALFORMED; + } + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + if (auto const destination = ctx.tx[~sfDestination]; + destination && *destination == beast::zero) + { + JLOG(ctx.j.debug()) << "VaultWithdraw: zero/empty destination account."; + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +VaultWithdraw::preclaim(PreclaimContext const& ctx) +{ + auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID])); + if (!vault) + return tecNO_ENTRY; + + auto const assets = ctx.tx[sfAmount]; + auto const vaultAsset = vault->at(sfAsset); + auto const vaultShare = vault->at(sfShareMPTID); + if (assets.asset() != vaultAsset && assets.asset() != vaultShare) + return tecWRONG_ASSET; + + if (vaultAsset.native()) + ; // No special checks for XRP + else if (vaultAsset.holds()) + { + auto mptID = vaultAsset.get().getMptID(); + auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; + if (!issuance->isFlag(lsfMPTCanTransfer)) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultWithdraw: vault assets are non-transferable."; + return tecNO_AUTH; + // LCOV_EXCL_STOP + } + } + else if (vaultAsset.holds()) + { + auto const issuer = + ctx.view.read(keylet::account(vaultAsset.getIssuer())); + if (!issuer) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultWithdraw: missing issuer of vault assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + } + + // Enforce valid withdrawal policy + if (vault->at(sfWithdrawalPolicy) != vaultStrategyFirstComeFirstServe) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) << "VaultWithdraw: invalid withdrawal policy."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + auto const account = ctx.tx[sfAccount]; + auto const dstAcct = [&]() -> AccountID { + if (ctx.tx.isFieldPresent(sfDestination)) + return ctx.tx.getAccountID(sfDestination); + return account; + }(); + + // Withdrawal to a 3rd party destination account is essentially a transfer, + // via shares in the vault. Enforce all the usual asset transfer checks. + if (account != dstAcct) + { + auto const sleDst = ctx.view.read(keylet::account(dstAcct)); + if (sleDst == nullptr) + return tecNO_DST; + + if (sleDst->getFlags() & lsfRequireDestTag) + return tecDST_TAG_NEEDED; // Cannot send without a tag + + if (sleDst->getFlags() & lsfDepositAuth) + { + if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account))) + return tecNO_PERMISSION; + } + } + + // Destination MPToken must exist (if asset is an MPT) + if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct); + !isTesSuccess(ter)) + return ter; + + // Cannot withdraw from a Vault an Asset frozen for the destination account + if (isFrozen(ctx.view, dstAcct, vaultAsset)) + return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + + if (isFrozen(ctx.view, account, vaultShare)) + return tecLOCKED; + + return tesSUCCESS; +} + +TER +VaultWithdraw::doApply() +{ + auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID])); + if (!vault) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const mptIssuanceID = (*vault)[sfShareMPTID]; + auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultWithdraw: missing issuance of vault shares."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + // 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; // LCOV_EXCL_LINE + + if (accountHolds( + view(), + account_, + share, + FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahIGNORE_AUTH, + j_) < shares) + { + JLOG(j_.debug()) << "VaultWithdraw: account doesn't hold enough 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(sfAssetsAvailable) < assets) + { + JLOG(j_.debug()) << "VaultWithdraw: vault doesn't hold enough assets"; + return tecINSUFFICIENT_FUNDS; + } + + vault->at(sfAssetsTotal) -= assets; + vault->at(sfAssetsAvailable) -= 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_, WaiveTransferFee::Yes)) + return ter; + + auto const dstAcct = [&]() -> AccountID { + if (ctx_.tx.isFieldPresent(sfDestination)) + return ctx_.tx.getAccountID(sfDestination); + return account_; + }(); + + // Transfer assets from vault to depositor or destination account. + if (auto ter = accountSend( + view(), vaultAccount, dstAcct, assets, j_, WaiveTransferFee::Yes)) + return ter; + + // Sanity check + if (accountHolds( + view(), + vaultAccount, + assets.asset(), + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_) < beast::zero) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultWithdraw: negative balance of vault assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + 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 0000000000..0b713d403b --- /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 b20b5a29f6..5e8c125e83 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -61,6 +61,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 4c70f5dc48..387aedecfc 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -34,6 +35,7 @@ #include #include +#include #include #include @@ -87,6 +89,14 @@ isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); [[nodiscard]] bool isGlobalFrozen(ReadView const& view, Asset const& asset); +// Note, depth parameter is used to limit the recursion depth +[[nodiscard]] bool +isVaultPseudoAccountFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptShare, + int depth); + [[nodiscard]] bool isIndividualFrozen( ReadView const& view, @@ -130,7 +140,11 @@ isFrozen( AccountID const& issuer); [[nodiscard]] inline bool -isFrozen(ReadView const& view, AccountID const& account, Issue const& issue) +isFrozen( + ReadView const& view, + AccountID const& account, + Issue const& issue, + int = 0 /*ignored*/) { return isFrozen(view, account, issue.currency, issue.account); } @@ -139,13 +153,63 @@ isFrozen(ReadView const& view, AccountID const& account, Issue const& issue) isFrozen( ReadView const& view, AccountID const& account, - MPTIssue const& mptIssue); + MPTIssue const& mptIssue, + int depth = 0); +/** + * isFrozen check is recursive for MPT shares in a vault, descending to + * assets in the vault, up to maxAssetCheckDepth recursion depth. This is + * purely defensive, as we currently do not allow such vaults to be created. + */ [[nodiscard]] inline bool -isFrozen(ReadView const& view, AccountID const& account, Asset const& asset) +isFrozen( + ReadView const& view, + AccountID const& account, + Asset const& asset, + int depth = 0) { return std::visit( - [&](auto const& issue) { return isFrozen(view, account, issue); }, + [&](auto const& issue) { + return isFrozen(view, account, issue, depth); + }, + asset.value()); +} + +[[nodiscard]] bool +isAnyFrozen( + ReadView const& view, + std::initializer_list const& accounts, + MPTIssue const& mptIssue, + int depth = 0); + +[[nodiscard]] inline bool +isAnyFrozen( + ReadView const& view, + std::initializer_list const& accounts, + Issue const& issue) +{ + for (auto const& account : accounts) + { + if (isFrozen(view, account, issue.currency, issue.account)) + return true; + } + return false; +} + +[[nodiscard]] inline bool +isAnyFrozen( + ReadView const& view, + std::initializer_list const& accounts, + Asset const& asset, + int depth = 0) +{ + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return isAnyFrozen(view, accounts, issue); + else + return isAnyFrozen(view, accounts, issue, depth); + }, asset.value()); } @@ -192,6 +256,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 @@ -430,6 +503,73 @@ dirNext( [[nodiscard]] std::function describeOwnerDir(AccountID const& account); +[[nodiscard]] TER +dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr& object); + +AccountID +pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey); + +/** + * + * Create pseudo-account, storing pseudoOwnerKey into ownerField. + * + * The list of valid ownerField is maintained in View.cpp and the caller to + * this function must perform necessary amendment check(s) before using a + * field. The amendment check is **not** performed in createPseudoAccount. + */ +[[nodiscard]] Expected, TER> +createPseudoAccount( + ApplyView& view, + uint256 const& pseudoOwnerKey, + SField const& ownerField); + +// Returns true iff sleAcct is a pseudo-account. +// +// Returns false if sleAcct is +// * NOT a pseudo-account OR +// * NOT a ltACCOUNT_ROOT OR +// * null pointer +[[nodiscard]] bool +isPseudoAccount(std::shared_ptr sleAcct); + +[[nodiscard]] inline bool +isPseudoAccount(ReadView const& view, AccountID accountId) +{ + return isPseudoAccount(view.read(keylet::account(accountId))); +} + +[[nodiscard]] TER +addEmptyHolding( + ApplyView& view, + AccountID const& accountID, + XRPAmount priorBalance, + Issue const& issue, + beast::Journal journal); + +[[nodiscard]] TER +addEmptyHolding( + ApplyView& view, + AccountID const& accountID, + XRPAmount priorBalance, + MPTIssue const& mptIssue, + beast::Journal journal); + +[[nodiscard]] inline TER +addEmptyHolding( + ApplyView& view, + AccountID const& accountID, + XRPAmount priorBalance, + Asset const& asset, + beast::Journal journal) +{ + return std::visit( + [&](TIss const& issue) -> TER { + return addEmptyHolding( + view, accountID, priorBalance, issue, journal); + }, + asset.value()); +} + // VFALCO NOTE Both STAmount parameters should just // be "Amount", a unit-less number. // @@ -457,6 +597,34 @@ trustCreate( std::uint32_t uSrcQualityOut, beast::Journal j); +[[nodiscard]] TER +removeEmptyHolding( + ApplyView& view, + AccountID const& accountID, + Issue const& issue, + beast::Journal journal); + +[[nodiscard]] TER +removeEmptyHolding( + ApplyView& view, + AccountID const& accountID, + MPTIssue const& mptIssue, + beast::Journal journal); + +[[nodiscard]] inline TER +removeEmptyHolding( + ApplyView& view, + AccountID const& accountID, + Asset const& asset, + beast::Journal journal) +{ + return std::visit( + [&](TIss const& issue) -> TER { + return removeEmptyHolding(view, accountID, issue, journal); + }, + asset.value()); +} + [[nodiscard]] TER trustDelete( ApplyView& view, @@ -535,17 +703,92 @@ transferXRP( STAmount const& amount, beast::Journal j); +/* Check if MPToken exists: + * - StrongAuth - before checking lsfMPTRequireAuth is set + * - WeakAuth - after checking if lsfMPTRequireAuth is set + */ +enum class MPTAuthType : bool { StrongAuth = true, WeakAuth = false }; + /** 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 enforceMPTokenAuthorization inside + * doApply. This will ensure that any expired credentials are deleted. + * + * requireAuth check is recursive for MPT shares in a vault, descending to + * assets in the vault, up to maxAssetCheckDepth recursion depth. This is + * purely defensive, as we currently do not allow such vaults to be created. + * + * If StrongAuth then return tecNO_AUTH if MPToken doesn't exist or + * lsfMPTRequireAuth is set and MPToken is not authorized. If WeakAuth then + * return tecNO_AUTH if lsfMPTRequireAuth is set and MPToken doesn't exist or is + * not authorized (explicitly or via credentials, if DomainID is set in + * MPTokenIssuance). Consequently, if WeakAuth and lsfMPTRequireAuth is *not* + * set, this function will return true even if MPToken does *not* exist. + */ [[nodiscard]] TER requireAuth( ReadView const& view, MPTIssue const& mptIssue, - AccountID const& account); + AccountID const& account, + MPTAuthType authType = MPTAuthType::StrongAuth, + int depth = 0); + +[[nodiscard]] TER inline requireAuth( + ReadView const& view, + Asset const& asset, + AccountID const& account, + MPTAuthType authType = MPTAuthType::StrongAuth) +{ + return std::visit( + [&](TIss const& issue_) { + if constexpr (std::is_same_v) + return requireAuth(view, issue_, account); + else + return requireAuth(view, issue_, account, authType); + }, + asset.value()); +} + +/** Enforce account has MPToken to match its authorization. + * + * Called from doApply - it will check for expired (and delete if found any) + * credentials matching DomainID set in MPTokenIssuance. Must be called if + * requireAuth(...MPTIssue...) returned tesSUCCESS or tecEXPIRED in preclaim, + * which implies that preclaim should replace `tecEXPIRED` with `tesSUCCESS` + * in order for the transactor to proceed to doApply. + * + * This function will create MPToken (if needed) on the basis of any + * non-expired credentials and will delete any expired credentials, indirectly + * via verifyValidDomain, as per DomainID (if set in MPTokenIssuance). + * + * The caller does NOT need to ensure that DomainID is actually set - this + * function handles gracefully both cases when DomainID is set and when not. + * + * The caller does NOT need to look for existing MPToken to match + * mptIssue/account - this function checks lsfMPTAuthorized of an existing + * MPToken iff DomainID is not set. + * + * Do not use for accounts which hold implied permission e.g. object owners or + * if MPTokenIssuance does not require authorization. In both cases use + * MPTokenAuthorize::authorize if MPToken does not yet exist. + */ +[[nodiscard]] TER +enforceMPTokenAuthorization( + ApplyView& view, + MPTID const& mptIssuanceID, + 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 @@ -592,6 +835,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); + /** Has the specified time passed? @param now the current time diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index af81a6b7bb..d248d37e18 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -17,18 +17,29 @@ */ //============================================================================== +#include +#include #include #include +#include #include #include #include #include +#include +#include +#include #include #include +#include +#include +#include #include #include +#include +#include namespace ripple { @@ -184,7 +195,7 @@ bool isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue) { if (auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID()))) - return sle->getFlags() & lsfMPTLocked; + return sle->isFlag(lsfMPTLocked); return false; } @@ -229,7 +240,7 @@ isIndividualFrozen( { if (auto const sle = view.read(keylet::mptoken(mptIssue.getMptID(), account))) - return sle->getFlags() & lsfMPTLocked; + return sle->isFlag(lsfMPTLocked); return false; } @@ -262,10 +273,77 @@ bool isFrozen( ReadView const& view, AccountID const& account, - MPTIssue const& mptIssue) + MPTIssue const& mptIssue, + int depth) { return isGlobalFrozen(view, mptIssue) || - isIndividualFrozen(view, account, mptIssue); + isIndividualFrozen(view, account, mptIssue) || + isVaultPseudoAccountFrozen(view, account, mptIssue, depth); +} + +[[nodiscard]] bool +isAnyFrozen( + ReadView const& view, + std::initializer_list const& accounts, + MPTIssue const& mptIssue, + int depth) +{ + if (isGlobalFrozen(view, mptIssue)) + return true; + + for (auto const& account : accounts) + { + if (isIndividualFrozen(view, account, mptIssue)) + return true; + } + + for (auto const& account : accounts) + { + if (isVaultPseudoAccountFrozen(view, account, mptIssue, depth)) + return true; + } + + return false; +} + +bool +isVaultPseudoAccountFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptShare, + int depth) +{ + if (!view.rules().enabled(featureSingleAssetVault)) + return false; + + if (depth >= maxAssetCheckDepth) + return true; // LCOV_EXCL_LINE + + auto const mptIssuance = + view.read(keylet::mptIssuance(mptShare.getMptID())); + if (mptIssuance == nullptr) + return false; // zero MPToken won't block deletion of MPTokenIssuance + + auto const issuer = mptIssuance->getAccountID(sfIssuer); + auto const mptIssuer = view.read(keylet::account(issuer)); + if (mptIssuer == nullptr) + { // LCOV_EXCL_START + UNREACHABLE("ripple::isVaultPseudoAccountFrozen : null MPToken issuer"); + return false; + } // LCOV_EXCL_STOP + + if (!mptIssuer->isFieldPresent(sfVaultID)) + return false; // not a Vault pseudo-account, common case + + auto const vault = + view.read(keylet::vault(mptIssuer->getFieldH256(sfVaultID))); + if (vault == nullptr) + { // LCOV_EXCL_START + UNREACHABLE("ripple::isVaultPseudoAccountFrozen : null vault"); + return false; + } // LCOV_EXCL_STOP + + return isAnyFrozen(view, {issuer, account}, vault->at(sfAsset), depth + 1); } bool @@ -413,6 +491,7 @@ accountHolds( auto const sleMpt = view.read(keylet::mptoken(mptIssue.getMptID(), account)); + if (!sleMpt) amount.clear(mptIssue); else if ( @@ -422,9 +501,17 @@ 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, MPTAuthType::StrongAuth); + !isTesSuccess(err)) + amount.clear(mptIssue); + } + else if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED) { auto const sleIssuance = view.read(keylet::mptIssuance(mptIssue.getMptID())); @@ -440,6 +527,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, @@ -931,6 +1041,176 @@ 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; // LCOV_EXCL_LINE + object->setFieldU64(sfOwnerNode, *page); + return tesSUCCESS; +} + +AccountID +pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey) +{ + // This number must not be changed without an amendment + constexpr int maxAccountAttempts = 256; + for (auto i = 0; i < maxAccountAttempts; ++i) + { + ripesha_hasher rsh; + auto const hash = sha512Half(i, view.info().parentHash, pseudoOwnerKey); + rsh(hash.data(), hash.size()); + AccountID const ret{static_cast(rsh)}; + if (!view.read(keylet::account(ret))) + return ret; + } + return beast::zero; +} + +// Note, the list of the pseudo-account designator fields below MUST be +// maintained but it does NOT need to be amendment-gated, since a +// non-active amendment will not set any field, by definition. Specific +// properties of a pseudo-account are NOT checked here, that's what +// InvariantCheck is for. +static std::array const pseudoAccountOwnerFields = { + &sfAMMID, // + &sfVaultID, // +}; + +Expected, TER> +createPseudoAccount( + ApplyView& view, + uint256 const& pseudoOwnerKey, + SField const& ownerField) +{ + XRPL_ASSERT( + std::count_if( + pseudoAccountOwnerFields.begin(), + pseudoAccountOwnerFields.end(), + [&ownerField](SField const* sf) -> bool { + return *sf == ownerField; + }) == 1, + "ripple::createPseudoAccount : valid owner field"); + + auto const accountId = pseudoAccountAddress(view, pseudoOwnerKey); + if (accountId == beast::zero) + return Unexpected(tecDUPLICATE); + + // Create pseudo-account. + auto account = std::make_shared(keylet::account(accountId)); + account->setAccountID(sfAccount, accountId); + account->setFieldAmount(sfBalance, STAmount{}); + + // Pseudo-accounts can't submit transactions, so set the sequence number + // to 0 to make them easier to spot and verify, and add an extra level + // of protection. + std::uint32_t const seqno = // + view.rules().enabled(featureSingleAssetVault) // + ? 0 // + : view.seq(); + 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. + account->setFieldH256(ownerField, pseudoOwnerKey); + + view.insert(account); + + return account; +} + +[[nodiscard]] bool +isPseudoAccount(std::shared_ptr sleAcct) +{ + // Intentionally use defensive coding here because it's cheap and makes the + // semantics of true return value clean. + return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT && + std::count_if( + pseudoAccountOwnerFields.begin(), + pseudoAccountOwnerFields.end(), + [&sleAcct](SField const* sf) -> bool { + return sleAcct->isFieldPresent(*sf); + }) > 0; +} + +[[nodiscard]] TER +addEmptyHolding( + ApplyView& view, + AccountID const& accountID, + XRPAmount priorBalance, + Issue const& issue, + beast::Journal journal) +{ + // 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; // LCOV_EXCL_LINE + + auto const& srcId = issuerId; + auto const& dstId = accountID; + auto const high = srcId > dstId; + auto const index = keylet::line(srcId, dstId, currency); + auto const sleSrc = view.peek(keylet::account(srcId)); + auto const sleDst = view.peek(keylet::account(dstId)); + if (!sleDst || !sleSrc) + return tefINTERNAL; // LCOV_EXCL_LINE + if (!sleSrc->isFlag(lsfDefaultRipple)) + return tecINTERNAL; // LCOV_EXCL_LINE + // If the line already exists, don't create it again. + if (view.read(index)) + return tecDUPLICATE; + return trustCreate( + view, + high, + srcId, + dstId, + index.key, + sleDst, + /*auth=*/false, + /*noRipple=*/true, + /*freeze=*/false, + /*deepFreeze*/ false, + /*balance=*/STAmount{Issue{currency, noAccount()}}, + /*limit=*/STAmount{Issue{currency, dstId}}, + /*qualityIn=*/0, + /*qualityOut=*/0, + journal); +} + +[[nodiscard]] TER +addEmptyHolding( + ApplyView& view, + AccountID const& accountID, + XRPAmount priorBalance, + MPTIssue const& mptIssue, + beast::Journal journal) +{ + auto const& mptID = mptIssue.getMptID(); + auto const mpt = view.peek(keylet::mptIssuance(mptID)); + if (!mpt) + return tefINTERNAL; // LCOV_EXCL_LINE + if (mpt->isFlag(lsfMPTLocked)) + return tefINTERNAL; // LCOV_EXCL_LINE + if (view.peek(keylet::mptoken(mptID, accountID))) + return tecDUPLICATE; + + return MPTokenAuthorize::authorize( + view, + journal, + {.priorBalance = priorBalance, + .mptIssuanceID = mptID, + .account = accountID}); +} + TER trustCreate( ApplyView& view, @@ -1050,6 +1330,91 @@ trustCreate( return tesSUCCESS; } +[[nodiscard]] TER +removeEmptyHolding( + ApplyView& view, + AccountID const& accountID, + Issue const& issue, + beast::Journal journal) +{ + 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; + + // Adjust the owner count(s) + if (line->isFlag(lsfLowReserve)) + { + // Clear reserve for low account. + auto sleLowAccount = + view.peek(keylet::account(line->at(sfLowLimit)->getIssuer())); + if (!sleLowAccount) + return tecINTERNAL; + adjustOwnerCount(view, sleLowAccount, -1, journal); + // It's not really necessary to clear the reserve flag, since the line + // is about to be deleted, but this will make the metadata reflect an + // accurate state at the time of deletion. + line->clearFlag(lsfLowReserve); + } + + if (line->isFlag(lsfHighReserve)) + { + // Clear reserve for high account. + auto sleHighAccount = + view.peek(keylet::account(line->at(sfHighLimit)->getIssuer())); + if (!sleHighAccount) + return tecINTERNAL; + adjustOwnerCount(view, sleHighAccount, -1, journal); + // It's not really necessary to clear the reserve flag, since the line + // is about to be deleted, but this will make the metadata reflect an + // accurate state at the time of deletion. + line->clearFlag(lsfHighReserve); + } + + return trustDelete( + view, + line, + line->at(sfLowLimit)->getIssuer(), + line->at(sfHighLimit)->getIssuer(), + journal); +} + +[[nodiscard]] TER +removeEmptyHolding( + ApplyView& view, + AccountID const& accountID, + MPTIssue const& mptIssue, + beast::Journal journal) +{ + auto const& mptID = mptIssue.getMptID(); + auto const mptoken = view.peek(keylet::mptoken(mptID, accountID)); + if (!mptoken) + return tecOBJECT_NOT_FOUND; + if (mptoken->at(sfMPTAmount) != 0) + return tecHAS_OBLIGATIONS; + + return MPTokenAuthorize::authorize( + view, + journal, + {.priorBalance = {}, + .mptIssuanceID = mptID, + .account = accountID, + .flags = tfMPTUnauthorize}); +} + TER trustDelete( ApplyView& view, @@ -1464,6 +1829,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); @@ -1513,6 +1879,7 @@ rippleCreditMPT( else return tecNO_AUTH; } + return tesSUCCESS; } @@ -1921,35 +2288,179 @@ TER requireAuth( ReadView const& view, MPTIssue const& mptIssue, - AccountID const& account) + AccountID const& account, + MPTAuthType authType, + int depth) { 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; + if (view.rules().enabled(featureSingleAssetVault)) + { + if (depth >= maxAssetCheckDepth) + return tecINTERNAL; // LCOV_EXCL_LINE + + // requireAuth is recursive if the issuer is a vault pseudo-account + auto const sleIssuer = view.read(keylet::account(mptIssuer)); + if (!sleIssuer) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (sleIssuer->isFieldPresent(sfVaultID)) + { + auto const sleVault = + view.read(keylet::vault(sleIssuer->getFieldH256(sfVaultID))); + if (!sleVault) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const asset = sleVault->at(sfAsset); + if (auto const err = std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return requireAuth(view, issue, account); + else + return requireAuth( + view, issue, account, authType, depth + 1); + }, + asset.value()); + !isTesSuccess(err)) + return err; + } + } + auto const mptokenID = keylet::mptoken(mptID.key, account); auto const sleToken = view.read(mptokenID); // if account has no MPToken, fail - if (!sleToken) + if (!sleToken && authType == MPTAuthType::StrongAuth) return tecNO_AUTH; + // Note, this check is not amendment-gated because DomainID will be always + // empty **unless** writing to it has been enabled by an amendment + auto const maybeDomainID = sleIssuance->at(~sfDomainID); + if (maybeDomainID) + { + XRPL_ASSERT( + sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth, + "ripple::requireAuth : issuance requires authorization"); + // ter = tefINTERNAL | tecOBJECT_NOT_FOUND | tecNO_AUTH | tecEXPIRED + if (auto const ter = + credentials::validDomain(view, *maybeDomainID, account); + isTesSuccess(ter)) + return ter; // Note: sleToken might be null + else if (!sleToken) + return ter; + // We ignore error from validDomain if we found sleToken, as it could + // belong to someone who is explicitly authorized e.g. a vault owner. + } + // mptoken must be authorized if issuance enabled requireAuth - if (sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth && - !(sleToken->getFlags() & lsfMPTAuthorized)) + if (sleIssuance->isFlag(lsfMPTRequireAuth) && + (!sleToken || !sleToken->isFlag(lsfMPTAuthorized))) return tecNO_AUTH; - return tesSUCCESS; + return tesSUCCESS; // Note: sleToken might be null } +[[nodiscard]] TER +enforceMPTokenAuthorization( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + XRPAmount const& priorBalance, // for MPToken authorization + beast::Journal j) +{ + auto const sleIssuance = view.read(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + return tefINTERNAL; // LCOV_EXCL_LINE + + XRPL_ASSERT( + sleIssuance->isFlag(lsfMPTRequireAuth), + "ripple::enforceMPTokenAuthorization : authorization required"); + + if (account == sleIssuance->at(sfIssuer)) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const keylet = keylet::mptoken(mptIssuanceID, account); + auto const sleToken = view.read(keylet); // NOTE: might be null + auto const maybeDomainID = sleIssuance->at(~sfDomainID); + bool const authorizedByDomain = maybeDomainID.has_value() && + verifyValidDomain(view, account, *maybeDomainID, j) == tesSUCCESS; + + if (!authorizedByDomain && sleToken == nullptr) + { + // Could not find MPToken and won't create one, could be either of: + // + // 1. Field sfDomainID not set in MPTokenIssuance or + // 2. Account has no matching and accepted credentials or + // 3. Account has all expired credentials (deleted in verifyValidDomain) + // + // Either way, return tecNO_AUTH and there is nothing else to do + return tecNO_AUTH; + } + else if (!authorizedByDomain && maybeDomainID.has_value()) + { + // Found an MPToken but the account is not authorized and we expect + // it to have been authorized by the domain. This could be because the + // credentials used to create the MPToken have expired or been deleted. + return tecNO_AUTH; + } + else if (!authorizedByDomain) + { + // We found an MPToken, but sfDomainID is not set, so this is a classic + // MPToken which requires authorization by the token issuer. + XRPL_ASSERT( + sleToken != nullptr && !maybeDomainID.has_value(), + "ripple::enforceMPTokenAuthorization : found MPToken"); + if (sleToken->isFlag(lsfMPTAuthorized)) + return tesSUCCESS; + + return tecNO_AUTH; + } + else if (authorizedByDomain && sleToken != nullptr) + { + // Found an MPToken, authorized by the domain. Ignore authorization flag + // lsfMPTAuthorized because it is meaningless. Return tesSUCCESS + XRPL_ASSERT( + maybeDomainID.has_value(), + "ripple::enforceMPTokenAuthorization : found MPToken for domain"); + return tesSUCCESS; + } + else if (authorizedByDomain) + { + // Could not find MPToken but there should be one because we are + // authorized by domain. Proceed to create it, then return tesSUCCESS + XRPL_ASSERT( + maybeDomainID.has_value() && sleToken == nullptr, + "ripple::enforceMPTokenAuthorization : new MPToken for domain"); + if (auto const err = MPTokenAuthorize::authorize( + view, + j, + { + .priorBalance = priorBalance, + .mptIssuanceID = mptIssuanceID, + .account = account, + .flags = 0, + }); + !isTesSuccess(err)) + return err; + + return tesSUCCESS; + } + + // LCOV_EXCL_START + UNREACHABLE( + "ripple::enforceMPTokenAuthorization : condition list is incomplete"); + return tefINTERNAL; +} // LCOV_EXCL_STOP + TER canTransfer( ReadView const& view, @@ -2125,6 +2636,62 @@ 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(sfAssetsTotal); + STAmount shares{vault->at(sfShareMPTID), 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(sfAssetsTotal); + assetTotal -= vault->at(sfLossUnrealized); + STAmount shares{vault->at(sfShareMPTID)}; + 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(sfShareMPTID), + "ripple::sharesToAssetsWithdraw : shares and vault match"); + Number assetTotal = vault->at(sfAssetsTotal); + 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; +} + bool after(NetClock::time_point now, std::uint32_t mark) { diff --git a/src/xrpld/net/detail/RPCCall.cpp b/src/xrpld/net/detail/RPCCall.cpp index dd1208aa24..0cc3cb6618 100644 --- a/src/xrpld/net/detail/RPCCall.cpp +++ b/src/xrpld/net/detail/RPCCall.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -862,6 +863,23 @@ private: return jvRequest; } + Json::Value + parseVault(Json::Value const& jvParams) + { + std::string strVaultID = jvParams[0u].asString(); + uint256 id = beast::zero; + if (!id.parseHex(strVaultID)) + return rpcError(rpcINVALID_PARAMS); + + Json::Value jvRequest(Json::objectValue); + jvRequest[jss::vault_id] = strVaultID; + + if (jvParams.size() > 1) + jvParseLedger(jvRequest, jvParams[1u].asString()); + + return jvRequest; + } + // peer_reservations_add [] Json::Value parsePeerReservationsAdd(Json::Value const& jvParams) @@ -1208,6 +1226,7 @@ public: {"account_offers", &RPCParser::parseAccountItems, 1, 4}, {"account_tx", &RPCParser::parseAccountTransactions, 1, 8}, {"amm_info", &RPCParser::parseAsIs, 1, 2}, + {"vault_info", &RPCParser::parseVault, 1, 2}, {"book_changes", &RPCParser::parseLedgerId, 1, 1}, {"book_offers", &RPCParser::parseBookOffers, 2, 7}, {"can_delete", &RPCParser::parseCanDelete, 0, 1}, diff --git a/src/xrpld/rpc/detail/Handler.cpp b/src/xrpld/rpc/detail/Handler.cpp index dd670529a5..3b32524ee2 100644 --- a/src/xrpld/rpc/detail/Handler.cpp +++ b/src/xrpld/rpc/detail/Handler.cpp @@ -188,6 +188,7 @@ Handler const handlerArray[]{ Role::ADMIN, NO_CONDITION}, {"validator_info", byRef(&doValidatorInfo), Role::ADMIN, NO_CONDITION}, + {"vault_info", byRef(&doVaultInfo), Role::USER, NO_CONDITION}, {"wallet_propose", byRef(&doWalletPropose), Role::ADMIN, NO_CONDITION}, // Evented methods {"subscribe", byRef(&doSubscribe), Role::USER, NO_CONDITION}, diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 347a984d15..b98f31340a 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -1140,5 +1140,6 @@ getLedgerByContext(RPC::JsonContext& context) return RPC::make_error( rpcNOT_READY, "findCreate failed to return an inbound ledger"); } + } // namespace RPC } // namespace ripple diff --git a/src/xrpld/rpc/detail/RPCHelpers.h b/src/xrpld/rpc/detail/RPCHelpers.h index 31b9761058..1d33d69459 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.h +++ b/src/xrpld/rpc/detail/RPCHelpers.h @@ -269,7 +269,9 @@ keypairForSignature( Json::Value const& params, Json::Value& error, unsigned int apiVersion = apiVersionIfUnspecified); + } // namespace RPC + } // namespace ripple #endif diff --git a/src/xrpld/rpc/handlers/AccountObjects.cpp b/src/xrpld/rpc/handlers/AccountObjects.cpp index 03ea427b12..2b2496a1dd 100644 --- a/src/xrpld/rpc/handlers/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/AccountObjects.cpp @@ -224,7 +224,9 @@ doAccountObjects(RPC::JsonContext& context) {jss::bridge, ltBRIDGE}, {jss::mpt_issuance, ltMPTOKEN_ISSUANCE}, {jss::mptoken, ltMPTOKEN}, - {jss::permissioned_domain, ltPERMISSIONED_DOMAIN}}; + {jss::permissioned_domain, ltPERMISSIONED_DOMAIN}, + {jss::vault, ltVAULT}, + }; typeFilter.emplace(); typeFilter->reserve(std::size(deletionBlockers)); diff --git a/src/xrpld/rpc/handlers/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h index 12e493576b..b76cbea8cd 100644 --- a/src/xrpld/rpc/handlers/Handlers.h +++ b/src/xrpld/rpc/handlers/Handlers.h @@ -166,6 +166,8 @@ Json::Value doValidatorListSites(RPC::JsonContext&); Json::Value doValidatorInfo(RPC::JsonContext&); +Json::Value +doVaultInfo(RPC::JsonContext&); } // namespace ripple #endif diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index d2f188aef3..fb82788907 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -744,6 +745,39 @@ parseTicket(Json::Value const& params, Json::Value& jvResult) return getTicketIndex(*id, params[jss::ticket_seq].asUInt()); } +static std::optional +parseVault(Json::Value const& params, Json::Value& jvResult) +{ + if (!params.isObject()) + { + uint256 uNodeIndex; + if (!uNodeIndex.parseHex(params.asString())) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return uNodeIndex; + } + + if (!params.isMember(jss::owner) || !params.isMember(jss::seq) || + !(params[jss::seq].isInt() || params[jss::seq].isUInt()) || + params[jss::seq].asDouble() <= 0.0 || + params[jss::seq].asDouble() > double(Json::Value::maxUInt)) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + + auto const id = parseBase58(params[jss::owner].asString()); + if (!id) + { + jvResult[jss::error] = "malformedOwner"; + return std::nullopt; + } + + return keylet::vault(*id, params[jss::seq].asUInt()).key; +} + static std::optional parseXChainOwnedClaimID(Json::Value const& claim_id, Json::Value& jvResult) { @@ -951,6 +985,7 @@ doLedgerEntry(RPC::JsonContext& context) {jss::xchain_owned_create_account_claim_id, parseXChainOwnedCreateAccountClaimID, ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, + {jss::vault, parseVault, ltVAULT}, }); uint256 uNodeIndex; diff --git a/src/xrpld/rpc/handlers/VaultInfo.cpp b/src/xrpld/rpc/handlers/VaultInfo.cpp new file mode 100644 index 0000000000..417bbd38e3 --- /dev/null +++ b/src/xrpld/rpc/handlers/VaultInfo.cpp @@ -0,0 +1,114 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +static std::optional +parseVault(Json::Value const& params, Json::Value& jvResult) +{ + auto const hasVaultId = params.isMember(jss::vault_id); + auto const hasOwner = params.isMember(jss::owner); + auto const hasSeq = params.isMember(jss::seq); + + uint256 uNodeIndex = beast::zero; + if (hasVaultId && !hasOwner && !hasSeq) + { + if (!uNodeIndex.parseHex(params[jss::vault_id].asString())) + { + RPC::inject_error(rpcINVALID_PARAMS, jvResult); + return std::nullopt; + } + // else uNodeIndex holds the value we need + } + else if (!hasVaultId && hasOwner && hasSeq) + { + auto const id = parseBase58(params[jss::owner].asString()); + if (!id) + { + RPC::inject_error(rpcACT_MALFORMED, jvResult); + return std::nullopt; + } + else if ( + !(params[jss::seq].isInt() || params[jss::seq].isUInt()) || + params[jss::seq].asDouble() <= 0.0 || + params[jss::seq].asDouble() > double(Json::Value::maxUInt)) + { + RPC::inject_error(rpcINVALID_PARAMS, jvResult); + return std::nullopt; + } + + uNodeIndex = keylet::vault(*id, params[jss::seq].asUInt()).key; + } + else + { + // Invalid combination of fields vault_id/owner/seq + RPC::inject_error(rpcINVALID_PARAMS, jvResult); + return std::nullopt; + } + + return uNodeIndex; +} + +Json::Value +doVaultInfo(RPC::JsonContext& context) +{ + std::shared_ptr lpLedger; + auto jvResult = RPC::lookupLedger(lpLedger, context); + + if (!lpLedger) + return jvResult; + + auto const uNodeIndex = + parseVault(context.params, jvResult).value_or(beast::zero); + if (uNodeIndex == beast::zero) + { + jvResult[jss::error] = "malformedRequest"; + return jvResult; + } + + auto const sleVault = lpLedger->read(keylet::vault(uNodeIndex)); + auto const sleIssuance = sleVault == nullptr // + ? nullptr + : lpLedger->read(keylet::mptIssuance(sleVault->at(sfShareMPTID))); + if (!sleVault || !sleIssuance) + { + jvResult[jss::error] = "entryNotFound"; + return jvResult; + } + + Json::Value& vault = jvResult[jss::vault]; + vault = sleVault->getJson(JsonOptions::none); + auto& share = vault[jss::shares]; + share = sleIssuance->getJson(JsonOptions::none); + + jvResult[jss::vault] = vault; + return jvResult; +} + +} // namespace ripple