diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index ab487280b9..f51e71a495 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -14,126 +14,196 @@ namespace xrpl { -//------------------------------------------------------------------------------ -// -// Freeze checking (MPT-specific) -// -//------------------------------------------------------------------------------ +class MPToken : public virtual TokenBase +{ +public: + MPToken(ReadView const& view, MPTIssue const& mptIssue) + : ReadOnlySLE(view.read(keylet::mptIssuance(mptIssue.getMptID())), view) + , TokenBase(view, view.read(keylet::mptIssuance(mptIssue.getMptID()))) + , mptID_(mptIssue.getMptID()) + , mptIssue_(mptIssue) + { + } -[[nodiscard]] bool -isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); + MPToken(ReadView const& view, MPTID const& mptID) + : ReadOnlySLE(view.read(keylet::mptIssuance(mptID)), view) + , TokenBase(view, view.read(keylet::mptIssuance(mptID))) + , mptID_(mptID) + , mptIssue_(MPTIssue(mptID_)) + { + } -[[nodiscard]] bool -isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); + MPTID const& + getMptID() const + { + return mptID_; + } -[[nodiscard]] bool -isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, int depth = 0); + MPTIssue const& + getMptIssue() const + { + return mptIssue_; + } -[[nodiscard]] bool -isAnyFrozen( - ReadView const& view, - std::initializer_list const& accounts, - MPTIssue const& mptIssue, - int depth = 0); + AccountID const& + getIssuer() const + { + return mptIssue_.getIssuer(); + } -//------------------------------------------------------------------------------ -// -// Transfer rate (MPT-specific) -// -//------------------------------------------------------------------------------ + //------------------------------------------------------------------------------ + // + // Freeze checking (MPT-specific) + // + //------------------------------------------------------------------------------ -/** Returns MPT transfer fee as Rate. Rate specifies - * the fee as fractions of 1 billion. For example, 1% transfer rate - * is represented as 1,010,000,000. - * @param issuanceID MPTokenIssuanceID of MPTTokenIssuance object - */ -[[nodiscard]] Rate -transferRate(ReadView const& view, MPTID const& issuanceID); + [[nodiscard]] bool + isGlobalFrozen() const override; -//------------------------------------------------------------------------------ -// -// Holding checks (MPT-specific) -// -//------------------------------------------------------------------------------ + [[nodiscard]] bool + isIndividualFrozen(AccountID const& account) const override; -[[nodiscard]] TER -canAddHolding(ReadView const& view, MPTIssue const& mptIssue); + [[nodiscard]] bool + isFrozen(AccountID const& account, int depth = 0) const override; -//------------------------------------------------------------------------------ -// -// Authorization (MPT-specific) -// -//------------------------------------------------------------------------------ + [[nodiscard]] TER + checkFrozen(AccountID const& account) const override; -[[nodiscard]] TER -authorizeMPToken( - ApplyView& view, - XRPAmount const& priorBalance, - MPTID const& mptIssuanceID, - AccountID const& account, - beast::Journal journal, - std::uint32_t flags = 0, - std::optional holderID = std::nullopt); + [[nodiscard]] bool + isAnyFrozen(std::initializer_list const& accounts, int depth = 0) const override; -/** Check if the account lacks required authorization for MPT. - * - * 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. - */ -[[nodiscard]] TER -requireAuth( - ReadView const& view, - MPTIssue const& mptIssue, - AccountID const& account, - AuthType authType = AuthType::Legacy, - int depth = 0); + [[nodiscard]] bool + isDeepFrozen(AccountID const& account, int depth = 0) const override; -/** 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. - */ -[[nodiscard]] TER -enforceMPTokenAuthorization( - ApplyView& view, - MPTID const& mptIssuanceID, - AccountID const& account, - XRPAmount const& priorBalance, - beast::Journal j); + [[nodiscard]] TER + checkDeepFrozen(AccountID const& account) const override; -/** Check if the destination account is allowed - * to receive MPT. Return tecNO_AUTH if it doesn't - * and tesSUCCESS otherwise. - */ -[[nodiscard]] TER -canTransfer( - ReadView const& view, - MPTIssue const& mptIssue, - AccountID const& from, - AccountID const& to); + //------------------------------------------------------------------------------ + // + // Transfer rate (MPT-specific) + // + //------------------------------------------------------------------------------ -//------------------------------------------------------------------------------ -// -// Empty holding operations (MPT-specific) -// -//------------------------------------------------------------------------------ + /** Returns MPT transfer fee as Rate. Rate specifies + * the fee as fractions of 1 billion. For example, 1% transfer rate + * is represented as 1,010,000,000. + * @param issuanceID MPTokenIssuanceID of MPTTokenIssuance object + */ + [[nodiscard]] Rate + transferRate() const override; -[[nodiscard]] TER -addEmptyHolding( - ApplyView& view, - AccountID const& accountID, - XRPAmount priorBalance, - MPTIssue const& mptIssue, - beast::Journal journal); + //------------------------------------------------------------------------------ + // + // Holding checks (MPT-specific) + // + //------------------------------------------------------------------------------ -[[nodiscard]] TER -removeEmptyHolding( - ApplyView& view, - AccountID const& accountID, - MPTIssue const& mptIssue, - beast::Journal journal); + [[nodiscard]] TER + canAddHolding() const override; + + /** Check if the account lacks required authorization for MPT. + * + * 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. + */ + [[nodiscard]] TER + requireAuth(AccountID const& account, AuthType authType = AuthType::Legacy, int depth = 0) + const override; + + /** Check if the destination account is allowed + * to receive MPT. Return tecNO_AUTH if it doesn't + * and tesSUCCESS otherwise. + */ + [[nodiscard]] TER + canTransfer(AccountID const& from, AccountID const& to) const override; + + STAmount + accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE) const override; + + [[nodiscard]] STAmount + accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE) const override; + +protected: + MPTID const mptID_; + MPTIssue const mptIssue_; +}; + +class WritableMPToken : public virtual WritableTokenBase, public virtual MPToken +{ +public: + WritableMPToken(ApplyView& view, MPTIssue const& mptIssue) + : ReadOnlySLE(view.peek(keylet::mptIssuance(mptIssue.getMptID())), view) + , TokenBase(view, view.peek(keylet::mptIssuance(mptIssue.getMptID()))) + , WritableSLE(view.peek(keylet::mptIssuance(mptIssue.getMptID())), view) + , WritableTokenBase(view, view.peek(keylet::mptIssuance(mptIssue.getMptID()))) + , MPToken(view, mptIssue) + { + } + + WritableMPToken(ApplyView& view, MPTID const& mptID) + : ReadOnlySLE(view.peek(keylet::mptIssuance(mptID)), view) + , TokenBase(view, view.peek(keylet::mptIssuance(mptID))) + , WritableSLE(view.peek(keylet::mptIssuance(mptID)), view) + , WritableTokenBase(view, view.peek(keylet::mptIssuance(mptID))) + , MPToken(view, mptID) + { + } + + // Resolve ambiguity: use writable operator-> for non-const, read-only for const + using WritableSLE::operator->; + using MPToken::operator->; + using WritableSLE::operator*; + using MPToken::operator*; + + //------------------------------------------------------------------------------ + // + // Authorization (MPT-specific) + // + //------------------------------------------------------------------------------ + + [[nodiscard]] TER + authorizeMPToken( + XRPAmount const& priorBalance, + AccountID const& account, + beast::Journal journal, + std::uint32_t flags = 0, + std::optional holderID = std::nullopt); + + /** 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. + */ + [[nodiscard]] TER + enforceMPTokenAuthorization( + AccountID const& account, + XRPAmount const& priorBalance, + beast::Journal j); + + //------------------------------------------------------------------------------ + // + // Empty holding operations (MPT-specific) + // + //------------------------------------------------------------------------------ + + [[nodiscard]] TER + addEmptyHolding(AccountID const& accountID, XRPAmount priorBalance, beast::Journal journal) + override; + + [[nodiscard]] TER + removeEmptyHolding(AccountID const& accountID, beast::Journal journal) override; +}; //------------------------------------------------------------------------------ // diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h index 9973524db4..83a9f8abc2 100644 --- a/include/xrpl/ledger/helpers/RippleStateHelpers.h +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h @@ -19,6 +19,126 @@ namespace xrpl { +class IOUToken : public virtual TokenBase +{ +public: + IOUToken(ReadView const& view, Issue const& issue) + : ReadOnlySLE(view.read(keylet::account(issue.getIssuer())), view) + , TokenBase(view, view.read(keylet::account(issue.getIssuer()))) + , issue_(issue) + , issuer_(issue.getIssuer()) + , issuerAccount_(issuer_, view) + , currency_(issue.currency) + { + } + + IOUToken(ReadView const& view, AccountID const& issuer, Currency const& currency) + : IOUToken(view, Issue{currency, issuer}) + { + } + + [[nodiscard]] bool + isGlobalFrozen() const override + { + return issuerAccount_.isGlobalFrozen(); + } + + [[nodiscard]] bool + isIndividualFrozen(AccountID const& account) const override; + + [[nodiscard]] bool + isFrozen(AccountID const& account, int depth = 0) const override; + + [[nodiscard]] TER + checkFrozen(AccountID const& account) const override; + + [[nodiscard]] bool + isAnyFrozen(std::initializer_list const& accounts, int depth = 0) const override; + + [[nodiscard]] bool + isDeepFrozen(AccountID const& account, int depth = 0) const override; + + [[nodiscard]] TER + checkDeepFrozen(AccountID const& account) const override; + + [[nodiscard]] Rate + transferRate() const override; + + STAmount + accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE) const override; + + [[nodiscard]] STAmount + accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE) const override; + + [[nodiscard]] TER + canAddHolding() const override; + + //------------------------------------------------------------------------------ + // + // Authorization and transfer checks (Asset-based dispatchers) + // + //------------------------------------------------------------------------------ + + [[nodiscard]] TER + requireAuth(AccountID const& account, AuthType authType = AuthType::Legacy, int depth = 0) + const override; + + [[nodiscard]] TER + canTransfer(AccountID const& from, AccountID const& to) const override; + +protected: + Issue const issue_; + AccountID const issuer_; + AccountRoot const issuerAccount_; + Currency const currency_; +}; + +class WritableIOUToken : public virtual WritableTokenBase, public virtual IOUToken +{ +public: + WritableIOUToken(ApplyView& view, Issue const& issue) + : ReadOnlySLE(view.peek(keylet::account(issue.getIssuer())), view) + , TokenBase(view, view.peek(keylet::account(issue.getIssuer()))) + , WritableSLE(view.peek(keylet::account(issue.getIssuer())), view) + , WritableTokenBase(view, view.peek(keylet::account(issue.getIssuer()))) + , IOUToken(view, issue) + { + } + + WritableIOUToken(ApplyView& view, AccountID const& issuer, Currency const& currency) + : WritableIOUToken(view, Issue{currency, issuer}) + { + } + + // Resolve ambiguity: use writable operator-> for non-const, read-only for const + using WritableSLE::operator->; + using IOUToken::operator->; + using WritableSLE::operator*; + using IOUToken::operator*; + + //------------------------------------------------------------------------------ + // + // Holding management (WritableTokenBase interface) + // + //------------------------------------------------------------------------------ + + [[nodiscard]] TER + addEmptyHolding(AccountID const& accountID, XRPAmount priorBalance, beast::Journal journal) + override; + + [[nodiscard]] TER + removeEmptyHolding(AccountID const& accountID, beast::Journal journal) override; +}; + //------------------------------------------------------------------------------ // // Credit functions (from Credit.h) @@ -59,69 +179,6 @@ creditBalance( Currency const& currency); /** @} */ -//------------------------------------------------------------------------------ -// -// Freeze checking (IOU-specific) -// -//------------------------------------------------------------------------------ - -[[nodiscard]] bool -isIndividualFrozen( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer); - -[[nodiscard]] inline bool -isIndividualFrozen(ReadView const& view, AccountID const& account, Issue const& issue) -{ - return isIndividualFrozen(view, account, issue.currency, issue.account); -} - -[[nodiscard]] bool -isFrozen( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer); - -[[nodiscard]] inline bool -isFrozen(ReadView const& view, AccountID const& account, Issue const& issue) -{ - return isFrozen(view, account, issue.currency, issue.account); -} - -// Overload with depth parameter for uniformity with MPTIssue version. -// The depth parameter is ignored for IOUs since they don't have vault recursion. -[[nodiscard]] inline bool -isFrozen(ReadView const& view, AccountID const& account, Issue const& issue, int /*depth*/) -{ - return isFrozen(view, account, issue); -} - -[[nodiscard]] bool -isDeepFrozen( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer); - -[[nodiscard]] inline bool -isDeepFrozen( - ReadView const& view, - AccountID const& account, - Issue const& issue, - int = 0 /*ignored*/) -{ - return isDeepFrozen(view, account, issue.currency, issue.account); -} - -[[nodiscard]] inline TER -checkDeepFrozen(ReadView const& view, AccountID const& account, Issue const& issue) -{ - return isDeepFrozen(view, account, issue) ? (TER)tecFROZEN : (TER)tesSUCCESS; -} - //------------------------------------------------------------------------------ // // Trust line operations @@ -182,43 +239,6 @@ redeemIOU( Issue const& issue, beast::Journal j); -//------------------------------------------------------------------------------ -// -// Authorization and transfer checks (IOU-specific) -// -//------------------------------------------------------------------------------ - -/** Check if the account lacks required authorization. - * - * Return tecNO_AUTH or tecNO_LINE if it does - * and tesSUCCESS otherwise. - * - * If StrongAuth then return tecNO_LINE if the RippleState doesn't exist. Return - * tecNO_AUTH if lsfRequireAuth is set on the issuer's AccountRoot, and the - * RippleState does exist, and the RippleState is not authorized. - * - * If WeakAuth then return tecNO_AUTH if lsfRequireAuth is set, and the - * RippleState exists, and is not authorized. Return tecNO_LINE if - * lsfRequireAuth is set and the RippleState doesn't exist. Consequently, if - * WeakAuth and lsfRequireAuth is *not* set, this function will return - * tesSUCCESS even if RippleState does *not* exist. - * - * The default "Legacy" auth type is equivalent to WeakAuth. - */ -[[nodiscard]] TER -requireAuth( - ReadView const& view, - Issue const& issue, - AccountID const& account, - AuthType authType = AuthType::Legacy); - -/** Check if the destination account is allowed - * to receive IOU. Return terNO_RIPPLE if rippling is - * disabled on both sides and tesSUCCESS otherwise. - */ -[[nodiscard]] TER -canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, AccountID const& to); - //------------------------------------------------------------------------------ // // Empty holding operations (IOU-specific) diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index 41d8c4cc28..e880ecb299 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -10,6 +11,7 @@ #include #include +#include #include namespace xrpl { @@ -48,82 +50,136 @@ enum class AuthType { StrongAuth, WeakAuth, Legacy }; // //------------------------------------------------------------------------------ -[[nodiscard]] bool -isGlobalFrozen(ReadView const& view, Asset const& asset); +class TokenBase : public virtual ReadOnlySLE +{ +public: + [[nodiscard]] virtual bool + isGlobalFrozen() const = 0; -[[nodiscard]] bool -isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset); + [[nodiscard]] virtual bool + isIndividualFrozen(AccountID const& account) const = 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]] bool -isFrozen(ReadView const& view, AccountID const& account, Asset const& asset, 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]] virtual bool + isFrozen(AccountID const& account, int depth = 0) const; -[[nodiscard]] TER -checkFrozen(ReadView const& view, AccountID const& account, Issue const& issue); + [[nodiscard]] virtual TER + checkFrozen(AccountID const& account) const = 0; -[[nodiscard]] TER -checkFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); + [[nodiscard]] virtual bool + isAnyFrozen(std::initializer_list const& accounts, int depth = 0) const = 0; -[[nodiscard]] TER -checkFrozen(ReadView const& view, AccountID const& account, Asset const& asset); + /** + * 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]] virtual bool + isDeepFrozen(AccountID const& account, int depth = 0) const = 0; -[[nodiscard]] bool -isAnyFrozen( - ReadView const& view, - std::initializer_list const& accounts, - Issue const& issue); + [[nodiscard]] virtual TER + checkDeepFrozen(AccountID const& account) const = 0; -[[nodiscard]] bool -isAnyFrozen( - ReadView const& view, - std::initializer_list const& accounts, - Asset const& asset, - int depth = 0); + /** Returns the transfer fee as Rate based on the type of token + * @param view The ledger view + * @param amount The amount to transfer + */ + [[nodiscard]] virtual Rate + transferRate() const = 0; + //------------------------------------------------------------------------------ + // + // Account balance functions (Asset-based dispatchers) + // + //------------------------------------------------------------------------------ -[[nodiscard]] bool -isDeepFrozen( - ReadView const& view, - AccountID const& account, - MPTIssue const& mptIssue, - int depth = 0); + // Returns the amount an account can spend. + // + // If shSIMPLE_BALANCE is specified, this is the amount the account can spend + // without going into debt. + // + // If shFULL_BALANCE is specified, this is the amount the account can spend + // total. Specifically: + // * The account can go into debt if using a trust line, and the other side has + // a non-zero limit. + // * If the account is the asset issuer the limit is defined by the asset / + // issuance. + // + // <-- saAmount: amount of currency held by account. May be negative. + virtual STAmount + accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE) const = 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]] bool -isDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth = 0); + [[nodiscard]] virtual STAmount + accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j, + SpendableHandling includeFullBalance = shSIMPLE_BALANCE) const = 0; -[[nodiscard]] TER -checkDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); + [[nodiscard]] virtual TER + canAddHolding() const = 0; -[[nodiscard]] TER -checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset); + //------------------------------------------------------------------------------ + // + // Authorization and transfer checks (Asset-based dispatchers) + // + //------------------------------------------------------------------------------ -//------------------------------------------------------------------------------ -// -// Account balance functions (Asset-based dispatchers) -// -//------------------------------------------------------------------------------ + [[nodiscard]] virtual TER + requireAuth(AccountID const& account, AuthType authType = AuthType::Legacy, int depth = 0) + const = 0; -// Returns the amount an account can spend. -// -// If shSIMPLE_BALANCE is specified, this is the amount the account can spend -// without going into debt. -// -// If shFULL_BALANCE is specified, this is the amount the account can spend -// total. Specifically: -// * The account can go into debt if using a trust line, and the other side has -// a non-zero limit. -// * If the account is the asset issuer the limit is defined by the asset / -// issuance. -// -// <-- saAmount: amount of currency held by account. May be negative. + [[nodiscard]] virtual TER + canTransfer(AccountID const& from, AccountID const& to) const = 0; + +protected: + TokenBase(ReadView const& view, std::shared_ptr sle) : ReadOnlySLE(sle, view) + { + } +}; + +class WritableTokenBase : public virtual TokenBase, public virtual WritableSLE +{ +public: + //------------------------------------------------------------------------------ + // + // Holding operations (Asset-based dispatchers) + // + //------------------------------------------------------------------------------ + + [[nodiscard]] virtual TER + addEmptyHolding(AccountID const& accountID, XRPAmount priorBalance, beast::Journal journal) = 0; + + [[nodiscard]] virtual TER + removeEmptyHolding(AccountID const& accountID, beast::Journal journal) = 0; + +protected: + WritableTokenBase(ApplyView& view, std::shared_ptr sle) + : TokenBase(view, sle), WritableSLE(sle, view) + { + } +}; + +std::unique_ptr +makeTokenBase(ReadView const& view, Asset const& asset); + +std::unique_ptr +makeWritableTokenBase(ApplyView& view, Asset const& asset); + +// Helper function to get transfer rate from an STAmount +[[nodiscard]] Rate +transferRate(ReadView const& view, STAmount const& amount); + +// Returns the amount the specified account can spend. +// Supports both IOU and MPT via Currency/AccountID parameters. [[nodiscard]] STAmount accountHolds( ReadView const& view, @@ -134,25 +190,8 @@ accountHolds( beast::Journal j, SpendableHandling includeFullBalance = shSIMPLE_BALANCE); -[[nodiscard]] STAmount -accountHolds( - ReadView const& view, - AccountID const& account, - Issue const& issue, - FreezeHandling zeroIfFrozen, - beast::Journal j, - SpendableHandling includeFullBalance = shSIMPLE_BALANCE); - -[[nodiscard]] STAmount -accountHolds( - ReadView const& view, - AccountID const& account, - MPTIssue const& mptIssue, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j, - SpendableHandling includeFullBalance = shSIMPLE_BALANCE); - +// Returns the amount the specified account can spend for a given Asset. +// Dispatches to appropriate token wrapper based on Asset type. [[nodiscard]] STAmount accountHolds( ReadView const& view, @@ -163,66 +202,6 @@ accountHolds( beast::Journal j, SpendableHandling includeFullBalance = shSIMPLE_BALANCE); -// 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 -// an account can spend while also allowing currency issuers to spend -// unlimited amounts of their own currency (since they can always issue more). -[[nodiscard]] STAmount -accountFunds( - ReadView const& view, - AccountID const& id, - STAmount const& saDefault, - FreezeHandling freezeHandling, - beast::Journal j); - -/** Returns the transfer fee as Rate based on the type of token - * @param view The ledger view - * @param amount The amount to transfer - */ -[[nodiscard]] Rate -transferRate(ReadView const& view, STAmount const& amount); - -//------------------------------------------------------------------------------ -// -// Holding operations (Asset-based dispatchers) -// -//------------------------------------------------------------------------------ - -[[nodiscard]] TER -canAddHolding(ReadView const& view, Asset const& asset); - -[[nodiscard]] TER -addEmptyHolding( - ApplyView& view, - AccountID const& accountID, - XRPAmount priorBalance, - Asset const& asset, - beast::Journal journal); - -[[nodiscard]] TER -removeEmptyHolding( - ApplyView& view, - AccountID const& accountID, - Asset const& asset, - beast::Journal journal); - -//------------------------------------------------------------------------------ -// -// Authorization and transfer checks (Asset-based dispatchers) -// -//------------------------------------------------------------------------------ - -[[nodiscard]] TER -requireAuth( - ReadView const& view, - Asset const& asset, - AccountID const& account, - AuthType authType = AuthType::Legacy); - -[[nodiscard]] TER -canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, AccountID const& to); - //------------------------------------------------------------------------------ // // Money Transfers (Asset-based dispatchers) diff --git a/src/libxrpl/ledger/AcceptedLedgerTx.cpp b/src/libxrpl/ledger/AcceptedLedgerTx.cpp index f0d243f9b6..1ac2ce291f 100644 --- a/src/libxrpl/ledger/AcceptedLedgerTx.cpp +++ b/src/libxrpl/ledger/AcceptedLedgerTx.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -44,12 +45,10 @@ AcceptedLedgerTx::AcceptedLedgerTx( // If the offer create is not self funded then add the owner balance if (account != amount.issue().account) { - auto const ownerFunds = accountFunds( - *ledger, - account, - amount, - fhIGNORE_FREEZE, - beast::Journal{beast::Journal::getNullSink()}); + auto const ownerFunds = + makeTokenBase(*ledger, amount.asset()) + ->accountHolds( + account, fhIGNORE_FREEZE, beast::Journal{beast::Journal::getNullSink()}); mJson[jss::transaction][jss::owner_funds] = ownerFunds.getText(); } } diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 8224edf2d0..be1b6ad32a 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -49,8 +49,8 @@ isVaultPseudoAccountFrozen( if (depth >= maxAssetCheckDepth) return true; // LCOV_EXCL_LINE - auto const mptIssuance = view.read(keylet::mptIssuance(mptShare.getMptID())); - if (mptIssuance == nullptr) + auto mptIssuance = MPToken(view, mptShare); + if (!mptIssuance.exists()) return false; // zero MPToken won't block deletion of MPTokenIssuance auto const issuer = mptIssuance->getAccountID(sfIssuer); @@ -74,7 +74,7 @@ isVaultPseudoAccountFrozen( // LCOV_EXCL_STOP } - return isAnyFrozen(view, {issuer, account}, vault->at(sfAsset), depth + 1); + return mptIssuance.isAnyFrozen({issuer, account}, depth + 1); } bool @@ -84,8 +84,9 @@ isLPTokenFrozen( Issue const& asset, Issue const& asset2) { - return isFrozen(view, account, asset.currency, asset.account) || - isFrozen(view, account, asset2.currency, asset2.account); + auto assetToken = IOUToken(view, asset); + auto asset2Token = IOUToken(view, asset2); + return assetToken.isFrozen(account) || asset2Token.isFrozen(account); } bool @@ -406,10 +407,11 @@ doWithdraw( STAmount const& amount, beast::Journal j) { + auto const asset = makeWritableTokenBase(view, amount.asset()); // Create trust line or MPToken for the receiving account if (dstAcct == senderAcct) { - if (auto const ter = addEmptyHolding(view, senderAcct, priorBalance, amount.asset(), j); + if (auto const ter = asset->addEmptyHolding(senderAcct, priorBalance, j); !isTesSuccess(ter) && ter != tecDUPLICATE) return ter; } @@ -421,13 +423,8 @@ doWithdraw( } // Sanity check - if (accountHolds( - view, - sourceAcct, - amount.asset(), - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j) < amount) + if (asset->accountHolds( + sourceAcct, FreezeHandling::fhIGNORE_FREEZE, AuthHandling::ahIGNORE_AUTH, j) < amount) { // LCOV_EXCL_START JLOG(j.error()) << "LoanBrokerCoverWithdraw: negative balance of " diff --git a/src/libxrpl/ledger/entries/MPTokenHelpers.cpp b/src/libxrpl/ledger/entries/MPTokenHelpers.cpp new file mode 100644 index 0000000000..e847110195 --- /dev/null +++ b/src/libxrpl/ledger/entries/MPTokenHelpers.cpp @@ -0,0 +1,811 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +// Forward declarations for functions that remain in View.h/cpp +bool +isVaultPseudoAccountFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptShare, + int depth); + +[[nodiscard]] TER +dirLink( + ApplyView& view, + AccountID const& owner, + std::shared_ptr& object, + SF_UINT64 const& node = sfOwnerNode); + +bool +MPTokenIssuance::isGlobalFrozen() const +{ + if (sle_) + return sle_->isFlag(lsfMPTLocked); + return false; +} + +bool +MPTokenIssuance::isIndividualFrozen(AccountID const& account) const +{ + if (auto const sle = readView_.read(keylet::mptoken(mptID_, account))) + return sle->isFlag(lsfMPTLocked); + return false; +} + +bool +MPTokenIssuance::isFrozen(AccountID const& account, int depth) const +{ + return isGlobalFrozen() || isIndividualFrozen(account) || + isVaultPseudoAccountFrozen(readView_, account, mptIssue_, depth); +} + +[[nodiscard]] bool +MPTokenIssuance::isAnyFrozen(std::initializer_list const& accounts, int depth) const +{ + if (isGlobalFrozen()) + return true; + + for (auto const& account : accounts) + { + if (isIndividualFrozen(account)) + return true; + } + + for (auto const& account : accounts) + { + if (isVaultPseudoAccountFrozen(readView_, account, mptIssue_, depth)) + return true; + } + + return false; +} + +TER +MPTokenIssuance::checkFrozen(AccountID const& account) const +{ + return isFrozen(account) ? TER{tecLOCKED} : TER{tesSUCCESS}; +} + +bool +MPTokenIssuance::isDeepFrozen(AccountID const& account, int depth) const +{ + return isFrozen(account, depth); +} + +TER +MPTokenIssuance::checkDeepFrozen(AccountID const& account) const +{ + return isDeepFrozen(account) ? TER{tecLOCKED} : TER{tesSUCCESS}; +} + +Rate +MPTokenIssuance::transferRate() const +{ + // fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000 + // For example, if transfer fee is 50% then 10,000 * 50,000 = 500,000 + // which represents 50% of 1,000,000,000 + if (sle_ && sle_->isFieldPresent(sfTransferFee)) + return Rate{1'000'000'000u + 10'000 * sle_->getFieldU16(sfTransferFee)}; + + return parityRate; +} + +[[nodiscard]] TER +MPTokenIssuance::canAddHolding() const +{ + if (!sle_) + { + return tecOBJECT_NOT_FOUND; + } + if (!sle_->isFlag(lsfMPTCanTransfer)) + { + return tecNO_AUTH; + } + + return tesSUCCESS; +} + +[[nodiscard]] TER +WritableMPTokenIssuance::addEmptyHolding( + AccountID const& accountID, + XRPAmount priorBalance, + beast::Journal journal) +{ + if (!mutableSle_) + return tefINTERNAL; // LCOV_EXCL_LINE + if (mutableSle_->isFlag(lsfMPTLocked)) + return tefINTERNAL; // LCOV_EXCL_LINE + if (applyView_.peek(keylet::mptoken(mptID_, accountID))) + return tecDUPLICATE; + if (accountID == mptIssue_.getIssuer()) + return tesSUCCESS; + + return authorizeMPToken(priorBalance, accountID, journal); +} + +[[nodiscard]] TER +WritableMPTokenIssuance::authorizeMPToken( + XRPAmount const& priorBalance, + AccountID const& account, + beast::Journal journal, + std::uint32_t flags, + std::optional holderID) +{ + WritableAccountRoot wrappedAcct(account, applyView_); + if (!wrappedAcct) + return tecINTERNAL; // LCOV_EXCL_LINE + + // If the account that submitted the tx is a holder + // Note: `account_` is holder's account + // `holderID` is NOT used + if (!holderID) + { + // When a holder wants to unauthorize/delete a MPT, the ledger must + // - delete mptokenKey from owner directory + // - delete the MPToken + if (flags & tfMPTUnauthorize) + { + auto const mptokenKey = keylet::mptoken(mptID_, account); + auto const sleMpt = applyView_.peek(mptokenKey); + if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!applyView_.dirRemove( + keylet::ownerDir(account), (*sleMpt)[sfOwnerNode], sleMpt->key(), false)) + return tecINTERNAL; // LCOV_EXCL_LINE + + wrappedAcct.adjustOwnerCount(-1, journal); + + applyView_.erase(sleMpt); + return tesSUCCESS; + } + + // A potential holder wants to authorize/hold a mpt, the ledger must: + // - add the new mptokenKey to the owner directory + // - create the MPToken object for the holder + + // The reserve that is required to create the MPToken. Note + // that although the reserve increases with every item + // an account owns, in the case of MPTokens we only + // *enforce* a reserve if the user owns more than two + // items. This is similar to the reserve requirements of trust lines. + std::uint32_t const uOwnerCount = wrappedAcct->getFieldU32(sfOwnerCount); + XRPAmount const reserveCreate( + (uOwnerCount < 2) ? XRPAmount(beast::zero) + : applyView_.fees().accountReserve(uOwnerCount + 1)); + + if (priorBalance < reserveCreate) + return tecINSUFFICIENT_RESERVE; + + // Defensive check before we attempt to create MPToken for the issuer + if (!mutableSle_ || mutableSle_->getAccountID(sfIssuer) == account) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::authorizeMPToken : invalid issuance or issuers token"); + if (applyView_.rules().enabled(featureLendingProtocol)) + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + auto const mptokenKey = keylet::mptoken(mptID_, account); + auto mptoken = std::make_shared(mptokenKey); + if (auto ter = dirLink(applyView_, account, mptoken)) + return ter; // LCOV_EXCL_LINE + + (*mptoken)[sfAccount] = account; + (*mptoken)[sfMPTokenIssuanceID] = mptID_; + (*mptoken)[sfFlags] = 0; + applyView_.insert(mptoken); + + // Update owner count. + wrappedAcct.adjustOwnerCount(1, journal); + + return tesSUCCESS; + } + + if (!mutableSle_) + return tecINTERNAL; // LCOV_EXCL_LINE + + // If the account that submitted this tx is the issuer of the MPT + // Note: `account_` is issuer's account + // `holderID` is holder's account + if (account != (*mutableSle_)[sfIssuer]) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sleMpt = applyView_.peek(keylet::mptoken(mptID_, *holderID)); + if (!sleMpt) + return tecINTERNAL; // LCOV_EXCL_LINE + + std::uint32_t const flagsIn = sleMpt->getFieldU32(sfFlags); + std::uint32_t flagsOut = flagsIn; + + // Issuer wants to unauthorize the holder, unset lsfMPTAuthorized on + // their MPToken + if (flags & tfMPTUnauthorize) + { + flagsOut &= ~lsfMPTAuthorized; + } + // Issuer wants to authorize a holder, set lsfMPTAuthorized on their + // MPToken + else + { + flagsOut |= lsfMPTAuthorized; + } + + if (flagsIn != flagsOut) + sleMpt->setFieldU32(sfFlags, flagsOut); + + applyView_.update(sleMpt); + return tesSUCCESS; +} + +[[nodiscard]] TER +WritableMPTokenIssuance::removeEmptyHolding(AccountID const& accountID, beast::Journal journal) +{ + // If the account is the issuer, then no token should exist. MPTs do not + // have the legacy ability to create such a situation, but check anyway. If + // a token does exist, it will get deleted. If not, return success. + bool const accountIsIssuer = accountID == mptIssue_.getIssuer(); + auto const mptoken = applyView_.peek(keylet::mptoken(mptID_, accountID)); + if (!mptoken) + return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; + // Unlike a trust line, if the account is the issuer, and the token has a + // balance, it can not just be deleted, because that will throw the issuance + // accounting out of balance, so fail. Since this should be impossible + // anyway, I'm not going to put any effort into it. + if (mptoken->at(sfMPTAmount) != 0) + return tecHAS_OBLIGATIONS; + + return authorizeMPToken( + {}, // priorBalance + accountID, + journal, + tfMPTUnauthorize // flags + ); +} + +[[nodiscard]] TER +MPTokenIssuance::requireAuth(AccountID const& account, AuthType authType, int depth) const +{ + if (!sle_) + return tecOBJECT_NOT_FOUND; + + auto const mptIssuer = AccountRoot(sle_->getAccountID(sfIssuer), readView_); + + // issuer is always "authorized" + if (mptIssuer == account) // Issuer won't have MPToken + return tesSUCCESS; + + bool const featureSAVEnabled = readView_.rules().enabled(featureSingleAssetVault); + + if (featureSAVEnabled) + { + if (depth >= maxAssetCheckDepth) + return tecINTERNAL; // LCOV_EXCL_LINE + + // requireAuth is recursive if the issuer is a vault pseudo-account + if (!mptIssuer.exists()) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (mptIssuer->isFieldPresent(sfVaultID)) + { + auto const sleVault = readView_.read(keylet::vault(mptIssuer->getFieldH256(sfVaultID))); + if (!sleVault) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const asset = sleVault->at(sfAsset); + if (auto const err = + makeTokenBase(readView_, asset)->requireAuth(account, authType, depth + 1); + !isTesSuccess(err)) + return err; + } + } + + auto const sleToken = readView_.read(keylet::mptoken(mptID_, account)); + + // if account has no MPToken, fail + if (!sleToken && (authType == AuthType::StrongAuth || authType == AuthType::Legacy)) + 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 = sle_->at(~sfDomainID); + if (maybeDomainID) + { + XRPL_ASSERT( + sle_->getFieldU32(sfFlags) & lsfMPTRequireAuth, + "xrpl::requireAuth : issuance requires authorization"); + // ter = tefINTERNAL | tecOBJECT_NOT_FOUND | tecNO_AUTH | tecEXPIRED + auto const ter = credentials::validDomain(readView_, *maybeDomainID, account); + if (isTesSuccess(ter)) + { + return ter; // Note: sleToken might be null + } + 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. + } + + if (featureSAVEnabled) + { + // Implicitly authorize Vault and LoanBroker pseudo-accounts + if (isPseudoAccount(readView_, account, {&sfVaultID, &sfLoanBrokerID})) + return tesSUCCESS; + } + + // mptoken must be authorized if issuance enabled requireAuth + if (sle_->isFlag(lsfMPTRequireAuth) && (!sleToken || !sleToken->isFlag(lsfMPTAuthorized))) + return tecNO_AUTH; + + return tesSUCCESS; // Note: sleToken might be null +} + +[[nodiscard]] TER +WritableMPTokenIssuance::enforceMPTokenAuthorization( + AccountID const& account, + XRPAmount const& priorBalance, // for MPToken authorization + beast::Journal j) +{ + if (!mutableSle_) + return tefINTERNAL; // LCOV_EXCL_LINE + + XRPL_ASSERT( + mutableSle_->isFlag(lsfMPTRequireAuth), + "xrpl::enforceMPTokenAuthorization : authorization required"); + + if (account == mutableSle_->at(sfIssuer)) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const keylet = keylet::mptoken(mptID_, account); + auto const sleToken = readView_.read(keylet); // NOTE: might be null + auto const maybeDomainID = mutableSle_->at(~sfDomainID); + bool expired = false; + bool const authorizedByDomain = [&]() -> bool { + // NOTE: defensive here, should be checked in preclaim + if (!maybeDomainID) + return false; // LCOV_EXCL_LINE + + auto const ter = verifyValidDomain(applyView(), account, *maybeDomainID, j); + if (isTesSuccess(ter)) + return true; + if (ter == tecEXPIRED) + expired = true; + return false; + }(); + + 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 expired ? tecEXPIRED : tecNO_AUTH; + } + if (!authorizedByDomain && maybeDomainID) + { + // 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 expired ? tecEXPIRED : tecNO_AUTH; + } + 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, + "xrpl::enforceMPTokenAuthorization : found MPToken"); + if (sleToken->isFlag(lsfMPTAuthorized)) + return tesSUCCESS; + + return tecNO_AUTH; + } + if (authorizedByDomain && sleToken != nullptr) + { + // Found an MPToken, authorized by the domain. Ignore authorization flag + // lsfMPTAuthorized because it is meaningless. Return tesSUCCESS + XRPL_ASSERT(maybeDomainID, "xrpl::enforceMPTokenAuthorization : found MPToken for domain"); + return tesSUCCESS; + } + 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 && sleToken == nullptr, + "xrpl::enforceMPTokenAuthorization : new MPToken for domain"); + if (auto const err = authorizeMPToken( + priorBalance, // priorBalance + account, // account + j); + !isTesSuccess(err)) + return err; + + return tesSUCCESS; + } + + // LCOV_EXCL_START + UNREACHABLE("xrpl::enforceMPTokenAuthorization : condition list is incomplete"); + return tefINTERNAL; + // LCOV_EXCL_STOP +} + +TER +MPTokenIssuance::canTransfer(AccountID const& from, AccountID const& to) const +{ + if (!sle_) + return tecOBJECT_NOT_FOUND; + + if (!(sle_->getFieldU32(sfFlags) & lsfMPTCanTransfer)) + { + if (from != (*sle_)[sfIssuer] && to != (*sle_)[sfIssuer]) + return TER{tecNO_AUTH}; + } + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ +// +// Token capability checks (MPT-specific) +// +//------------------------------------------------------------------------------ + +bool +MPTokenIssuance::canClawback() const +{ + if (!sle_) + return false; + return sle_->isFlag(lsfMPTCanClawback); +} + +bool +MPTokenIssuance::requiresAuth() const +{ + if (!sle_) + return false; + return sle_->isFlag(lsfMPTRequireAuth); +} + +TER +rippleLockEscrowMPT( + ApplyView& view, + AccountID const& sender, + STAmount const& amount, + beast::Journal j) +{ + auto const mptIssue = amount.get(); + auto mptIssuance = WritableMPTokenIssuance(view, mptIssue); + if (!mptIssuance.exists()) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleLockEscrowMPT: MPT issuance not found for " + << mptIssue.getMptID(); + return tecOBJECT_NOT_FOUND; + } // LCOV_EXCL_STOP + + if (mptIssuance.getIssuer() == sender) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleLockEscrowMPT: sender is the issuer, cannot lock MPTs."; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + // 1. Decrease the MPT Holder MPTAmount + // 2. Increase the MPT Holder EscrowedAmount + { + auto const mptokenID = keylet::mptoken(mptIssuance.getMptID(), sender); + auto sle = view.peek(mptokenID); + if (!sle) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleLockEscrowMPT: MPToken not found for " << sender; + return tecOBJECT_NOT_FOUND; + } // LCOV_EXCL_STOP + + auto const amt = sle->getFieldU64(sfMPTAmount); + auto const pay = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, amt), STAmount(mptIssue, pay))) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleLockEscrowMPT: insufficient MPTAmount for " + << to_string(sender) << ": " << amt << " < " << pay; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + (*sle)[sfMPTAmount] = amt - pay; + + // Overflow check for addition + uint64_t const locked = (*sle)[~sfLockedAmount].value_or(0); + + if (!canAdd(STAmount(mptIssue, locked), STAmount(mptIssue, pay))) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleLockEscrowMPT: overflow on locked amount for " + << to_string(sender) << ": " << locked << " + " << pay; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + if (sle->isFieldPresent(sfLockedAmount)) + { + (*sle)[sfLockedAmount] += pay; + } + else + { + sle->setFieldU64(sfLockedAmount, pay); + } + + view.update(sle); + } + + // 1. Increase the Issuance EscrowedAmount + // 2. DO NOT change the Issuance OutstandingAmount + { + uint64_t const issuanceEscrowed = (*mptIssuance)[~sfLockedAmount].value_or(0); + auto const pay = amount.mpt().value(); + + // Overflow check for addition + if (!canAdd(STAmount(mptIssue, issuanceEscrowed), STAmount(mptIssue, pay))) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleLockEscrowMPT: overflow on issuance " + "locked amount for " + << mptIssue.getMptID() << ": " << issuanceEscrowed << " + " << pay; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + if (mptIssuance->isFieldPresent(sfLockedAmount)) + { + (*mptIssuance)[sfLockedAmount] += pay; + } + else + { + mptIssuance->setFieldU64(sfLockedAmount, pay); + } + + mptIssuance.update(); + } + return tesSUCCESS; +} + +TER +rippleUnlockEscrowMPT( + ApplyView& view, + AccountID const& sender, + AccountID const& receiver, + STAmount const& netAmount, + STAmount const& grossAmount, + beast::Journal j) +{ + if (!view.rules().enabled(fixTokenEscrowV1)) + { + XRPL_ASSERT( + netAmount == grossAmount, "xrpl::rippleUnlockEscrowMPT : netAmount == grossAmount"); + } + + auto const& issuer = netAmount.getIssuer(); + auto const& mptIssue = netAmount.get(); + auto mptIssuance = WritableMPTokenIssuance(view, mptIssue); + if (!mptIssuance) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: MPT issuance not found for " + << mptIssue.getMptID(); + return tecOBJECT_NOT_FOUND; + } // LCOV_EXCL_STOP + + // Decrease the Issuance EscrowedAmount + { + if (!mptIssuance->isFieldPresent(sfLockedAmount)) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: no locked amount in issuance for " + << mptIssue.getMptID(); + return tecINTERNAL; + } // LCOV_EXCL_STOP + + auto const locked = mptIssuance->getFieldU64(sfLockedAmount); + auto const redeem = grossAmount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, redeem))) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: insufficient locked amount for " + << mptIssue.getMptID() << ": " << locked << " < " << redeem; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + auto const newLocked = locked - redeem; + if (newLocked == 0) + { + mptIssuance->makeFieldAbsent(sfLockedAmount); + } + else + { + mptIssuance->setFieldU64(sfLockedAmount, newLocked); + } + mptIssuance.update(); + } + + if (issuer != receiver) + { + // Increase the MPT Holder MPTAmount + auto const mptokenID = keylet::mptoken(mptIssue.getMptID(), receiver); + auto sle = view.peek(mptokenID); + if (!sle) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: MPToken not found for " << receiver; + return tecOBJECT_NOT_FOUND; + } // LCOV_EXCL_STOP + + auto current = sle->getFieldU64(sfMPTAmount); + auto delta = netAmount.mpt().value(); + + // Overflow check for addition + if (!canAdd(STAmount(mptIssue, current), STAmount(mptIssue, delta))) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: overflow on MPTAmount for " + << to_string(receiver) << ": " << current << " + " << delta; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + (*sle)[sfMPTAmount] += delta; + view.update(sle); + } + else + { + // Decrease the Issuance OutstandingAmount + auto const outstanding = mptIssuance->getFieldU64(sfOutstandingAmount); + auto const redeem = netAmount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, outstanding), STAmount(mptIssue, redeem))) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: insufficient outstanding amount for " + << mptIssue.getMptID() << ": " << outstanding << " < " << redeem; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + mptIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); + mptIssuance.update(); + } + + if (issuer == sender) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: sender is the issuer, " + "cannot unlock MPTs."; + return tecINTERNAL; + } // LCOV_EXCL_STOP + // Decrease the MPT Holder EscrowedAmount + auto const mptokenID = keylet::mptoken(mptIssue.getMptID(), sender); + auto sle = view.peek(mptokenID); + if (!sle) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: MPToken not found for " << sender; + return tecOBJECT_NOT_FOUND; + } // LCOV_EXCL_STOP + + if (!sle->isFieldPresent(sfLockedAmount)) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: no locked amount in MPToken for " + << to_string(sender); + return tecINTERNAL; + } // LCOV_EXCL_STOP + + auto const locked = sle->getFieldU64(sfLockedAmount); + auto const delta = grossAmount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, delta))) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: insufficient locked amount for " + << to_string(sender) << ": " << locked << " < " << delta; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + auto const newLocked = locked - delta; + if (newLocked == 0) + sle->makeFieldAbsent(sfLockedAmount); + else + sle->setFieldU64(sfLockedAmount, newLocked); + view.update(sle); + + // Note: The gross amount is the amount that was locked, the net + // amount is the amount that is being unlocked. The difference is the fee + // that was charged for the transfer. If this difference is greater than + // zero, we need to update the outstanding amount. + auto const diff = grossAmount.mpt().value() - netAmount.mpt().value(); + if (diff != 0) + { + auto const outstanding = mptIssuance->getFieldU64(sfOutstandingAmount); + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, outstanding), STAmount(mptIssue, diff))) + { // LCOV_EXCL_START + JLOG(j.error()) << "rippleUnlockEscrowMPT: insufficient outstanding amount for " + << mptIssue.getMptID() << ": " << outstanding << " < " << diff; + return tecINTERNAL; + } // LCOV_EXCL_STOP + + mptIssuance->setFieldU64(sfOutstandingAmount, outstanding - diff); + mptIssuance.update(); + } + return tesSUCCESS; +} + +STAmount +MPTokenIssuance::accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + beast::Journal j, + SpendableHandling includeFullBalance) const +{ + return accountHolds(account, zeroIfFrozen, ahIGNORE_AUTH, j, includeFullBalance); +} + +STAmount +MPTokenIssuance::accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j, + SpendableHandling includeFullBalance) const +{ + bool const returnSpendable = (includeFullBalance == shFULL_BALANCE); + + if (returnSpendable && account == mptIssue_.getIssuer()) + { + // if the account is the issuer, and the issuance exists, their limit is + // the issuance limit minus the outstanding value + + if (!sle_) + { + return STAmount{mptIssue_}; + } + return STAmount{ + mptIssue_, + sle_->at(~sfMaximumAmount).value_or(maxMPTokenAmount) - sle_->at(sfOutstandingAmount)}; + } + + STAmount amount; + + auto const sleMpt = readView_.read(keylet::mptoken(mptID_, account)); + + if (!sleMpt) + amount.clear(mptIssue_); + else if (zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(account)) + amount.clear(mptIssue_); + else + { + amount = STAmount{mptIssue_, sleMpt->getFieldU64(sfMPTAmount)}; + + // 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 && + readView_.rules().enabled(featureSingleAssetVault)) + { + if (auto const err = requireAuth(account, AuthType::StrongAuth); !isTesSuccess(err)) + amount.clear(mptIssue_); + } + else if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED) + { + // if auth is enabled on the issuance and mpt is not authorized, + // clear amount + if (sle_ && sle_->isFlag(lsfMPTRequireAuth) && !sleMpt->isFlag(lsfMPTAuthorized)) + amount.clear(mptIssue_); + } + } + + return amount; +} + +} // namespace xrpl diff --git a/src/libxrpl/ledger/entries/RippleStateHelpers.cpp b/src/libxrpl/ledger/entries/RippleStateHelpers.cpp new file mode 100644 index 0000000000..e2cacd3ab2 --- /dev/null +++ b/src/libxrpl/ledger/entries/RippleStateHelpers.cpp @@ -0,0 +1,925 @@ +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +//------------------------------------------------------------------------------ +// +// Credit functions (from Credit.cpp) +// +//------------------------------------------------------------------------------ + +STAmount +creditLimit( + ReadView const& readView_, + AccountID const& account, + AccountID const& issuer, + Currency const& currency) +{ + STAmount result(Issue{currency, account}); + + auto sleRippleState = readView_.read(keylet::line(account, issuer, currency)); + + if (sleRippleState) + { + result = sleRippleState->getFieldAmount(account < issuer ? sfLowLimit : sfHighLimit); + result.setIssuer(account); + } + + XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditLimit : result issuer match"); + XRPL_ASSERT(result.getCurrency() == currency, "xrpl::creditLimit : result currency match"); + return result; +} + +IOUAmount +creditLimit2(ReadView const& v, AccountID const& acc, AccountID const& iss, Currency const& cur) +{ + return toAmount(creditLimit(v, acc, iss, cur)); +} + +STAmount +creditBalance( + ReadView const& readView_, + AccountID const& account, + AccountID const& issuer, + Currency const& currency) +{ + STAmount result(Issue{currency, account}); + + auto sleRippleState = readView_.read(keylet::line(account, issuer, currency)); + + if (sleRippleState) + { + result = sleRippleState->getFieldAmount(sfBalance); + if (account < issuer) + result.negate(); + result.setIssuer(account); + } + + XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditBalance : result issuer match"); + XRPL_ASSERT(result.getCurrency() == currency, "xrpl::creditBalance : result currency match"); + return result; +} + +//------------------------------------------------------------------------------ +// +// Freeze checking (IOU-specific) +// +//------------------------------------------------------------------------------ + +bool +IOUToken::isIndividualFrozen(AccountID const& account) const +{ + if (isXRP(currency_)) + return false; + if (issuer_ != account) + { + // Check if the issuer froze the line + auto const sle = readView_.read(keylet::line(account, issuer_, currency_)); + if (sle && sle->isFlag((issuer_ > account) ? lsfHighFreeze : lsfLowFreeze)) + return true; + } + return false; +} + +// Can the specified account spend the specified currency issued by +// the specified issuer or does the freeze flag prohibit it? +bool +IOUToken::isFrozen(AccountID const& account, int depth) const +{ + // NOTE: depth is ignored here because it's only relevant for MPTs + if (isXRP(currency_)) + return false; + if (issuerAccount_.exists() && issuerAccount_->isFlag(lsfGlobalFreeze)) + return true; + if (issuer_ != account) + { + // Check if the issuer froze the line + auto const sleLine = readView_.read(keylet::line(account, issuer_, currency_)); + if (sleLine && sleLine->isFlag((issuer_ > account) ? lsfHighFreeze : lsfLowFreeze)) + return true; + } + return false; +} + +bool +IOUToken::isDeepFrozen(AccountID const& account, int depth) const +{ + // NOTE: depth is ignored here because it's only relevant for MPTs + if (isXRP(currency_)) + { + return false; + } + + if (issuer_ == account) + { + return false; + } + + auto const sle = readView_.read(keylet::line(account, issuer_, currency_)); + if (!sle) + { + return false; + } + + return sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze); +} + +TER +IOUToken::checkFrozen(AccountID const& account) const +{ + return isFrozen(account) ? TER{tecFROZEN} : TER{tesSUCCESS}; +} + +TER +IOUToken::checkDeepFrozen(AccountID const& account) const +{ + return isDeepFrozen(account) ? TER{tecFROZEN} : TER{tesSUCCESS}; +} + +bool +IOUToken::isAnyFrozen(std::initializer_list const& accounts, int depth) const +{ + // NOTE: depth is ignored here because it's only relevant for MPTs + if (isGlobalFrozen()) + return true; + + for (auto const& account : accounts) + { + if (isFrozen(account, depth)) + return true; + } + + return false; +} + +STAmount +IOUToken::accountFunds( + AccountID const& id, + STAmount const& saDefault, + FreezeHandling freezeHandling, + beast::Journal j) const +{ + if (!saDefault.native() && saDefault.getIssuer() == id) + return saDefault; + + return accountHolds(id, freezeHandling, j); +} + +STAmount +IOUToken::accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + beast::Journal j, + SpendableHandling includeFullBalance) const +{ + return accountHolds(account, zeroIfFrozen, ahIGNORE_AUTH, j, includeFullBalance); +} + +STAmount +IOUToken::accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j, + SpendableHandling includeFullBalance) const +{ + if (isXRP(currency_)) + { + AccountRoot accountRoot(account, readView_); + return {accountRoot.xrpLiquid(0, j)}; + } + + bool const returnSpendable = (includeFullBalance == shFULL_BALANCE); + if (returnSpendable && account == issuer_) + // If the account is the issuer, then their limit is effectively + // infinite + return STAmount{issue_, STAmount::cMaxValue, STAmount::cMaxOffset}; + + // IOU: Return balance on trust line modulo freeze + // Check if line exists and is usable (mirrors old getLineIfUsable) + SLE::const_pointer sle = readView_.read(keylet::line(account, issuer_, currency_)); + + if (sle && zeroIfFrozen == fhZERO_IF_FROZEN) + { + if (isFrozen(account) || isDeepFrozen(account)) + { + sle = nullptr; + } + + // when fixFrozenLPTokenTransfer is enabled, if currency is lptoken, + // we need to check if the associated assets have been frozen + if (sle && readView_.rules().enabled(fixFrozenLPTokenTransfer)) + { + auto const sleIssuer = readView_.read(keylet::account(issuer_)); + if (!sleIssuer) + { + sle = nullptr; // LCOV_EXCL_LINE + } + else if (sleIssuer->isFieldPresent(sfAMMID)) + { + auto const sleAmm = readView_.read(keylet::amm((*sleIssuer)[sfAMMID])); + + if (!sleAmm || + isLPTokenFrozen( + readView_, + account, + (*sleAmm)[sfAsset].get(), + (*sleAmm)[sfAsset2].get())) + { + sle = nullptr; + } + } + } + } + + // Extract balance (mirrors old getTrustLineBalance) + STAmount amount; + if (sle) + { + amount = sle->getFieldAmount(sfBalance); + bool const accountHigh = account > issuer_; + auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit; + if (accountHigh) + { + // Put balance in account terms. + amount.negate(); + } + if (returnSpendable) + { + amount += sle->getFieldAmount(oppositeField); + } + amount.setIssuer(issuer_); + } + else + { + amount.clear(Issue{currency_, issuer_}); + } + + JLOG(j.trace()) << "IOUToken::accountHolds:" << " account=" << to_string(account) + << " amount=" << amount.getFullText(); + + return readView_.balanceHook(account, issuer_, amount); +} + +TER +IOUToken::canAddHolding() const +{ + if (isXRP(issue_)) + return tesSUCCESS; + + if (!issuerAccount_.exists()) + return terNO_ACCOUNT; + + if (!issuerAccount_->isFlag(lsfDefaultRipple)) + return terNO_RIPPLE; + + return tesSUCCESS; +} + +Rate +IOUToken::transferRate() const +{ + return issuerAccount_.transferRate(); +} + +//------------------------------------------------------------------------------ +// +// Trust line operations +// +//------------------------------------------------------------------------------ + +TER +trustCreate( + ApplyView& view, + bool const bSrcHigh, + AccountID const& uSrcAccountID, + AccountID const& uDstAccountID, + uint256 const& uIndex, // ripple state entry + WritableAccountRoot& wrappedAcct, // the account being set. + bool const bAuth, // authorize account. + bool const bNoRipple, // others cannot ripple through + bool const bFreeze, // funds cannot leave + bool bDeepFreeze, // can neither receive nor send funds + STAmount const& saBalance, // balance of account being set. + // Issuer should be noAccount() + STAmount const& saLimit, // limit for account being set. + // Issuer should be the account being set. + std::uint32_t uQualityIn, + std::uint32_t uQualityOut, + beast::Journal j) +{ + JLOG(j.trace()) << "trustCreate: " << to_string(uSrcAccountID) << ", " + << to_string(uDstAccountID) << ", " << saBalance.getFullText(); + + auto const& uLowAccountID = !bSrcHigh ? uSrcAccountID : uDstAccountID; + auto const& uHighAccountID = bSrcHigh ? uSrcAccountID : uDstAccountID; + if (uLowAccountID == uHighAccountID) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::trustCreate : trust line to self"); + if (view.rules().enabled(featureLendingProtocol)) + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + auto const sleRippleState = std::make_shared(ltRIPPLE_STATE, uIndex); + view.insert(sleRippleState); + + auto lowNode = view.dirInsert( + keylet::ownerDir(uLowAccountID), sleRippleState->key(), describeOwnerDir(uLowAccountID)); + + if (!lowNode) + return tecDIR_FULL; // LCOV_EXCL_LINE + + auto highNode = view.dirInsert( + keylet::ownerDir(uHighAccountID), sleRippleState->key(), describeOwnerDir(uHighAccountID)); + + if (!highNode) + return tecDIR_FULL; // LCOV_EXCL_LINE + + bool const bSetDst = saLimit.getIssuer() == uDstAccountID; + bool const bSetHigh = bSrcHigh ^ bSetDst; + + XRPL_ASSERT(wrappedAcct, "xrpl::trustCreate : non-null SLE"); + if (!wrappedAcct) + return tefINTERNAL; // LCOV_EXCL_LINE + + XRPL_ASSERT( + wrappedAcct->getAccountID(sfAccount) == (bSetHigh ? uHighAccountID : uLowAccountID), + "xrpl::trustCreate : matching account ID"); + auto const peer = AccountRoot(bSetHigh ? uLowAccountID : uHighAccountID, view); + if (!peer.exists()) + return tecNO_TARGET; + + // Remember deletion hints. + sleRippleState->setFieldU64(sfLowNode, *lowNode); + sleRippleState->setFieldU64(sfHighNode, *highNode); + + sleRippleState->setFieldAmount(bSetHigh ? sfHighLimit : sfLowLimit, saLimit); + sleRippleState->setFieldAmount( + bSetHigh ? sfLowLimit : sfHighLimit, + STAmount(Issue{saBalance.getCurrency(), bSetDst ? uSrcAccountID : uDstAccountID})); + + if (uQualityIn) + sleRippleState->setFieldU32(bSetHigh ? sfHighQualityIn : sfLowQualityIn, uQualityIn); + + if (uQualityOut) + sleRippleState->setFieldU32(bSetHigh ? sfHighQualityOut : sfLowQualityOut, uQualityOut); + + std::uint32_t uFlags = bSetHigh ? lsfHighReserve : lsfLowReserve; + + if (bAuth) + { + uFlags |= (bSetHigh ? lsfHighAuth : lsfLowAuth); + } + if (bNoRipple) + { + uFlags |= (bSetHigh ? lsfHighNoRipple : lsfLowNoRipple); + } + if (bFreeze) + { + uFlags |= (bSetHigh ? lsfHighFreeze : lsfLowFreeze); + } + if (bDeepFreeze) + { + uFlags |= (bSetHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze); + } + + if ((peer->getFlags() & lsfDefaultRipple) == 0) + { + // The other side's default is no rippling + uFlags |= (bSetHigh ? lsfLowNoRipple : lsfHighNoRipple); + } + + sleRippleState->setFieldU32(sfFlags, uFlags); + wrappedAcct.adjustOwnerCount(1, j); + + // ONLY: Create ripple balance. + sleRippleState->setFieldAmount(sfBalance, bSetHigh ? -saBalance : saBalance); + + view.creditHook(uSrcAccountID, uDstAccountID, saBalance, saBalance.zeroed()); + + return tesSUCCESS; +} + +TER +trustDelete( + ApplyView& view, + std::shared_ptr const& sleRippleState, + AccountID const& uLowAccountID, + AccountID const& uHighAccountID, + beast::Journal j) +{ + // Detect legacy dirs. + std::uint64_t uLowNode = sleRippleState->getFieldU64(sfLowNode); + std::uint64_t uHighNode = sleRippleState->getFieldU64(sfHighNode); + + JLOG(j.trace()) << "trustDelete: Deleting ripple line: low"; + + if (!view.dirRemove(keylet::ownerDir(uLowAccountID), uLowNode, sleRippleState->key(), false)) + { + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + + JLOG(j.trace()) << "trustDelete: Deleting ripple line: high"; + + if (!view.dirRemove(keylet::ownerDir(uHighAccountID), uHighNode, sleRippleState->key(), false)) + { + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + + JLOG(j.trace()) << "trustDelete: Deleting ripple line: state"; + view.erase(sleRippleState); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ +// +// IOU issuance/redemption +// +//------------------------------------------------------------------------------ + +static bool +updateTrustLine( + ApplyView& view, + SLE::pointer state, + bool bSenderHigh, + AccountID const& sender, + STAmount const& before, + STAmount const& after, + beast::Journal j) +{ + if (!state) + return false; + std::uint32_t const flags(state->getFieldU32(sfFlags)); + + WritableAccountRoot wrappedAcct(sender, view); + if (!wrappedAcct) + return false; + + // YYY Could skip this if rippling in reverse. + if (before > beast::zero + // Sender balance was positive. + && after <= beast::zero + // Sender is zero or negative. + && (flags & (!bSenderHigh ? lsfLowReserve : lsfHighReserve)) + // Sender reserve is set. + && static_cast(flags & (!bSenderHigh ? lsfLowNoRipple : lsfHighNoRipple)) != + static_cast(wrappedAcct->getFlags() & lsfDefaultRipple) && + !(flags & (!bSenderHigh ? lsfLowFreeze : lsfHighFreeze)) && + !state->getFieldAmount(!bSenderHigh ? sfLowLimit : sfHighLimit) + // Sender trust limit is 0. + && !state->getFieldU32(!bSenderHigh ? sfLowQualityIn : sfHighQualityIn) + // Sender quality in is 0. + && !state->getFieldU32(!bSenderHigh ? sfLowQualityOut : sfHighQualityOut)) + // Sender quality out is 0. + { + // VFALCO Where is the line being deleted? + // Clear the reserve of the sender, possibly delete the line! + wrappedAcct.adjustOwnerCount(-1, j); + + // Clear reserve flag. + state->setFieldU32(sfFlags, flags & (!bSenderHigh ? ~lsfLowReserve : ~lsfHighReserve)); + + // Balance is zero, receiver reserve is clear. + if (!after // Balance is zero. + && !(flags & (bSenderHigh ? lsfLowReserve : lsfHighReserve))) + return true; + } + return false; +} + +TER +issueIOU( + ApplyView& view, + AccountID const& account, + STAmount const& amount, + Issue const& issue, + beast::Journal j) +{ + XRPL_ASSERT( + !isXRP(account) && !isXRP(issue.account), + "xrpl::issueIOU : neither account nor issuer is XRP"); + + // Consistency check + XRPL_ASSERT(issue == amount.issue(), "xrpl::issueIOU : matching issue"); + + // Can't send to self! + XRPL_ASSERT(issue.account != account, "xrpl::issueIOU : not issuer account"); + + JLOG(j.trace()) << "issueIOU: " << to_string(account) << ": " << amount.getFullText(); + + bool bSenderHigh = issue.account > account; + + auto const index = keylet::line(issue.account, account, issue.currency); + + if (auto state = view.peek(index)) + { + STAmount final_balance = state->getFieldAmount(sfBalance); + + if (bSenderHigh) + final_balance.negate(); // Put balance in sender terms. + + STAmount const start_balance = final_balance; + + final_balance -= amount; + + auto const must_delete = updateTrustLine( + view, state, bSenderHigh, issue.account, start_balance, final_balance, j); + + view.creditHook(issue.account, account, amount, start_balance); + + if (bSenderHigh) + final_balance.negate(); + + // Adjust the balance on the trust line if necessary. We do this even + // if we are going to delete the line to reflect the correct balance + // at the time of deletion. + state->setFieldAmount(sfBalance, final_balance); + if (must_delete) + { + return trustDelete( + view, + state, + bSenderHigh ? account : issue.account, + bSenderHigh ? issue.account : account, + j); + } + + view.update(state); + + return tesSUCCESS; + } + + // NIKB TODO: The limit uses the receiver's account as the issuer and + // this is unnecessarily inefficient as copying which could be avoided + // is now required. Consider available options. + STAmount const limit(Issue{issue.currency, account}); + STAmount final_balance = amount; + + final_balance.setIssuer(noAccount()); + + WritableAccountRoot receiverAccount(account, view); + if (!receiverAccount) + return tefINTERNAL; // LCOV_EXCL_LINE + + bool noRipple = (receiverAccount->getFlags() & lsfDefaultRipple) == 0; + + return trustCreate( + view, + bSenderHigh, + issue.account, + account, + index.key, + receiverAccount, + false, + noRipple, + false, + false, + final_balance, + limit, + 0, + 0, + j); +} + +TER +redeemIOU( + ApplyView& applyView, + AccountID const& account, + STAmount const& amount, + Issue const& issue, + beast::Journal j) +{ + XRPL_ASSERT( + !isXRP(account) && !isXRP(issue.account), + "xrpl::redeemIOU : neither account nor issuer is XRP"); + + // Consistency check + XRPL_ASSERT(issue == amount.issue(), "xrpl::redeemIOU : matching issue"); + + // Can't send to self! + XRPL_ASSERT(issue.account != account, "xrpl::redeemIOU : not issuer account"); + + JLOG(j.trace()) << "redeemIOU: " << to_string(account) << ": " << amount.getFullText(); + + bool bSenderHigh = account > issue.account; + + if (auto state = applyView.peek(keylet::line(account, issue.account, issue.currency))) + { + STAmount final_balance = state->getFieldAmount(sfBalance); + + if (bSenderHigh) + final_balance.negate(); // Put balance in sender terms. + + STAmount const start_balance = final_balance; + + final_balance -= amount; + + auto const must_delete = updateTrustLine( + applyView, state, bSenderHigh, account, start_balance, final_balance, j); + + applyView.creditHook(account, issue.account, amount, start_balance); + + if (bSenderHigh) + final_balance.negate(); + + // Adjust the balance on the trust line if necessary. We do this even + // if we are going to delete the line to reflect the correct balance + // at the time of deletion. + state->setFieldAmount(sfBalance, final_balance); + + if (must_delete) + { + return trustDelete( + applyView, + state, + bSenderHigh ? issue.account : account, + bSenderHigh ? account : issue.account, + j); + } + + applyView.update(state); + return tesSUCCESS; + } + + // In order to hold an IOU, a trust line *MUST* exist to track the + // balance. If it doesn't, then something is very wrong. Don't try + // to continue. + // LCOV_EXCL_START + JLOG(j.fatal()) << "redeemIOU: " << to_string(account) << " attempts to " + << "redeem " << amount.getFullText() << " but no trust line exists!"; + + return tefINTERNAL; + // LCOV_EXCL_STOP +} + +//------------------------------------------------------------------------------ +// +// Authorization and transfer checks (IOU-specific) +// +//------------------------------------------------------------------------------ + +TER +IOUToken::requireAuth(AccountID const& account, AuthType authType, int depth) const +{ + // NOTE: depth is ignored here because it's only relevant for MPTs + if (isXRP(issue_) || issuer_ == account) + return tesSUCCESS; + + auto const trustLine = readView_.read(keylet::line(account, issuer_, issue_.currency)); + // If account has no line, and this is a strong check, fail + if (!trustLine && authType == AuthType::StrongAuth) + return tecNO_LINE; + + // If this is a weak or legacy check, or if the account has a line, fail if + // auth is required and not set on the line + if (issuerAccount_.exists() && (*issuerAccount_)[sfFlags] & lsfRequireAuth) + { + if (trustLine) + { + return ((*trustLine)[sfFlags] & ((account > issuer_) ? lsfLowAuth : lsfHighAuth)) + ? tesSUCCESS + : TER{tecNO_AUTH}; + } + return TER{tecNO_LINE}; + } + + return tesSUCCESS; +} + +TER +IOUToken::canTransfer(AccountID const& from, AccountID const& to) const +{ + if (issue_.native()) + return tesSUCCESS; + + if (issuer_ == from || issuer_ == to) + return tesSUCCESS; + if (!issuerAccount_.exists()) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const isRippleDisabled = [&](AccountID account) -> bool { + // Line might not exist, but some transfers can create it. If this + // is the case, just check the default ripple on the issuer account. + auto const line = readView_.read(keylet::line(account, issue_)); + if (line) + { + bool const issuerHigh = issuer_ > account; + return line->isFlag(issuerHigh ? lsfHighNoRipple : lsfLowNoRipple); + } + return issuerAccount_->isFlag(lsfDefaultRipple) == false; + }; + + // Fail if rippling disabled on both trust lines + if (isRippleDisabled(from) && isRippleDisabled(to)) + return terNO_RIPPLE; + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ +// +// Token capability checks (IOU-specific) +// +//------------------------------------------------------------------------------ + +bool +IOUToken::canClawback() const +{ + if (!issuerAccount_.exists()) + return false; + return issuerAccount_->isFlag(lsfAllowTrustLineClawback) && + !issuerAccount_->isFlag(lsfNoFreeze); +} + +bool +IOUToken::requiresAuth() const +{ + if (!issuerAccount_.exists()) + return false; + return issuerAccount_->isFlag(lsfRequireAuth); +} + +//------------------------------------------------------------------------------ +// +// Empty holding operations (IOU-specific) +// +//------------------------------------------------------------------------------ + +TER +WritableIOUToken::addEmptyHolding( + AccountID const& accountID, + XRPAmount priorBalance, + beast::Journal journal) +{ + // Every account can hold XRP. An issuer can issue directly. + if (issue_.native() || accountID == issuer_) + return tesSUCCESS; + + if (issuerAccount_.isGlobalFrozen()) + return tecFROZEN; // LCOV_EXCL_LINE + + auto const& srcId = issuer_; + auto const& dstId = accountID; + auto const high = srcId > dstId; + auto const index = keylet::line(srcId, dstId, currency_); + WritableAccountRoot wrappedSrc(srcId, applyView_); + WritableAccountRoot wrappedDst(dstId, applyView_); + if (!wrappedDst || !wrappedSrc) + return tefINTERNAL; // LCOV_EXCL_LINE + if (!wrappedSrc->isFlag(lsfDefaultRipple)) + return tecINTERNAL; // LCOV_EXCL_LINE + // If the line already exists, don't create it again. + if (applyView_.read(index)) + return tecDUPLICATE; + + // Can the account cover the trust line reserve ? + std::uint32_t const ownerCount = wrappedDst->at(sfOwnerCount); + if (priorBalance < readView_.fees().accountReserve(ownerCount + 1)) + return tecNO_LINE_INSUF_RESERVE; + + return trustCreate( + applyView_, + high, + srcId, + dstId, + index.key, + wrappedDst, + /*bAuth=*/false, + /*bNoRipple=*/true, + /*bFreeze=*/false, + /*deepFreeze*/ false, + /*saBalance=*/STAmount{Issue{currency_, noAccount()}}, + /*saLimit=*/STAmount{Issue{currency_, dstId}}, + /*uQualityIn=*/0, + /*uQualityOut=*/0, + journal); +} + +TER +WritableIOUToken::removeEmptyHolding(AccountID const& accountID, beast::Journal journal) +{ + if (issue_.native()) + { + auto const account = AccountRoot(accountID, applyView_); + if (!account.exists()) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const balance = account->getFieldAmount(sfBalance); + if (balance.xrp() != 0) + return tecHAS_OBLIGATIONS; + + return tesSUCCESS; + } + + // `asset` is an IOU. + // If the account is the issuer, then no line should exist. Check anyway. + // If a line does exist, it will get deleted. If not, return success. + bool const accountIsIssuer = accountID == issue_.account; + auto const line = applyView_.peek(keylet::line(accountID, issue_)); + if (!line) + return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; + if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::zero) + return tecHAS_OBLIGATIONS; + + // Adjust the owner count(s) + if (line->isFlag(lsfLowReserve)) + { + // Clear reserve for low account. + WritableAccountRoot wrappedLow(line->at(sfLowLimit)->getIssuer(), applyView_); + if (!wrappedLow) + return tecINTERNAL; // LCOV_EXCL_LINE + + wrappedLow.adjustOwnerCount(-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. + WritableAccountRoot wrappedHigh(line->at(sfHighLimit)->getIssuer(), applyView_); + if (!wrappedHigh) + return tecINTERNAL; // LCOV_EXCL_LINE + + wrappedHigh.adjustOwnerCount(-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( + applyView_, + line, + line->at(sfLowLimit)->getIssuer(), + line->at(sfHighLimit)->getIssuer(), + journal); +} + +TER +deleteAMMTrustLine( + ApplyView& view, + std::shared_ptr sleState, + std::optional const& ammAccountID, + beast::Journal j) +{ + if (!sleState || sleState->getType() != ltRIPPLE_STATE) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const& [low, high] = std::minmax( + sleState->getFieldAmount(sfLowLimit).getIssuer(), + sleState->getFieldAmount(sfHighLimit).getIssuer()); + WritableAccountRoot wrappedLow(low, view); + WritableAccountRoot wrappedHigh(high, view); + if (!wrappedLow || !wrappedHigh) + return tecINTERNAL; // LCOV_EXCL_LINE + + bool const ammLow = wrappedLow->isFieldPresent(sfAMMID); + bool const ammHigh = wrappedHigh->isFieldPresent(sfAMMID); + + // can't both be AMM + if (ammLow && ammHigh) + return tecINTERNAL; // LCOV_EXCL_LINE + + // at least one must be + if (!ammLow && !ammHigh) + return terNO_AMM; + + // one must be the target amm + if (ammAccountID && (low != *ammAccountID && high != *ammAccountID)) + return terNO_AMM; + + if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter)) + { + JLOG(j.error()) << "deleteAMMTrustLine: failed to delete the trustline."; + return ter; + } + + auto const uFlags = !ammLow ? lsfLowReserve : lsfHighReserve; + if (!(sleState->getFlags() & uFlags)) + return tecINTERNAL; // LCOV_EXCL_LINE + + WritableAccountRoot wrappedHolder = !ammLow ? wrappedLow : wrappedHigh; + wrappedHolder.adjustOwnerCount(-1, j); + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 0f976738f8..cfb7dd4d2d 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -28,76 +28,88 @@ dirLink( SF_UINT64 const& node = sfOwnerNode); bool -isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue) +MPToken::isGlobalFrozen() const { - if (auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID()))) + if (sle_) + return sle_->isFlag(lsfMPTLocked); + return false; +} + +bool +MPToken::isIndividualFrozen(AccountID const& account) const +{ + if (auto const sle = readView_.read(keylet::mptoken(mptID_, account))) return sle->isFlag(lsfMPTLocked); return false; } bool -isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue) +MPToken::isFrozen(AccountID const& account, int depth) const { - if (auto const sle = view.read(keylet::mptoken(mptIssue.getMptID(), account))) - return sle->isFlag(lsfMPTLocked); - return false; -} - -bool -isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, int depth) -{ - return isGlobalFrozen(view, mptIssue) || isIndividualFrozen(view, account, mptIssue) || - isVaultPseudoAccountFrozen(view, account, mptIssue, depth); + return isGlobalFrozen() || isIndividualFrozen(account) || + isVaultPseudoAccountFrozen(readView_, account, mptIssue_, depth); } [[nodiscard]] bool -isAnyFrozen( - ReadView const& view, - std::initializer_list const& accounts, - MPTIssue const& mptIssue, - int depth) +MPToken::isAnyFrozen(std::initializer_list const& accounts, int depth) const { - if (isGlobalFrozen(view, mptIssue)) + if (isGlobalFrozen()) return true; for (auto const& account : accounts) { - if (isIndividualFrozen(view, account, mptIssue)) + if (isIndividualFrozen(account)) return true; } for (auto const& account : accounts) { - if (isVaultPseudoAccountFrozen(view, account, mptIssue, depth)) + if (isVaultPseudoAccountFrozen(readView_, account, mptIssue_, depth)) return true; } return false; } +TER +MPToken::checkFrozen(AccountID const& account) const +{ + return isFrozen(account) ? TER{tecFROZEN} : TER{tesSUCCESS}; +} + +bool +MPToken::isDeepFrozen(AccountID const& account, int depth) const +{ + // MPTs don't have deep freeze, so this always returns false + return false; +} + +TER +MPToken::checkDeepFrozen(AccountID const& account) const +{ + return isDeepFrozen(account) ? TER{tecLOCKED} : TER{tesSUCCESS}; +} + Rate -transferRate(ReadView const& view, MPTID const& issuanceID) +MPToken::transferRate() const { // fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000 // For example, if transfer fee is 50% then 10,000 * 50,000 = 500,000 // which represents 50% of 1,000,000,000 - if (auto const sle = view.read(keylet::mptIssuance(issuanceID)); - sle && sle->isFieldPresent(sfTransferFee)) - return Rate{1'000'000'000u + 10'000 * sle->getFieldU16(sfTransferFee)}; + if (sle_ && sle_->isFieldPresent(sfTransferFee)) + return Rate{1'000'000'000u + 10'000 * sle_->getFieldU16(sfTransferFee)}; return parityRate; } [[nodiscard]] TER -canAddHolding(ReadView const& view, MPTIssue const& mptIssue) +MPToken::canAddHolding() const { - auto mptID = mptIssue.getMptID(); - auto issuance = view.read(keylet::mptIssuance(mptID)); - if (!issuance) + if (!sle_) { return tecOBJECT_NOT_FOUND; } - if (!issuance->isFlag(lsfMPTCanTransfer)) + if (!sle_->isFlag(lsfMPTCanTransfer)) { return tecNO_AUTH; } @@ -106,38 +118,32 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue) } [[nodiscard]] TER -addEmptyHolding( - ApplyView& view, +WritableMPToken::addEmptyHolding( 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) + if (!mutableSle_) return tefINTERNAL; // LCOV_EXCL_LINE - if (mpt->isFlag(lsfMPTLocked)) + if (mutableSle_->isFlag(lsfMPTLocked)) return tefINTERNAL; // LCOV_EXCL_LINE - if (view.peek(keylet::mptoken(mptID, accountID))) + if (applyView_.peek(keylet::mptoken(mptID_, accountID))) return tecDUPLICATE; - if (accountID == mptIssue.getIssuer()) + if (accountID == mptIssue_.getIssuer()) return tesSUCCESS; - return authorizeMPToken(view, priorBalance, mptID, accountID, journal); + return authorizeMPToken(priorBalance, accountID, journal); } [[nodiscard]] TER -authorizeMPToken( - ApplyView& view, +WritableMPToken::authorizeMPToken( XRPAmount const& priorBalance, - MPTID const& mptIssuanceID, AccountID const& account, beast::Journal journal, std::uint32_t flags, std::optional holderID) { - WritableAccountRoot wrappedAcct(account, view); + WritableAccountRoot wrappedAcct(account, applyView_); if (!wrappedAcct) return tecINTERNAL; // LCOV_EXCL_LINE @@ -151,18 +157,18 @@ authorizeMPToken( // - delete the MPToken if (flags & tfMPTUnauthorize) { - auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); - auto const sleMpt = view.peek(mptokenKey); + auto const mptokenKey = keylet::mptoken(mptID_, account); + auto const sleMpt = applyView_.peek(mptokenKey); if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0) return tecINTERNAL; // LCOV_EXCL_LINE - if (!view.dirRemove( + if (!applyView_.dirRemove( keylet::ownerDir(account), (*sleMpt)[sfOwnerNode], sleMpt->key(), false)) return tecINTERNAL; // LCOV_EXCL_LINE wrappedAcct.adjustOwnerCount(-1, journal); - view.erase(sleMpt); + applyView_.erase(sleMpt); return tesSUCCESS; } @@ -178,31 +184,30 @@ authorizeMPToken( std::uint32_t const uOwnerCount = wrappedAcct->getFieldU32(sfOwnerCount); XRPAmount const reserveCreate( (uOwnerCount < 2) ? XRPAmount(beast::zero) - : view.fees().accountReserve(uOwnerCount + 1)); + : applyView_.fees().accountReserve(uOwnerCount + 1)); if (priorBalance < reserveCreate) return tecINSUFFICIENT_RESERVE; // Defensive check before we attempt to create MPToken for the issuer - auto const mpt = view.read(keylet::mptIssuance(mptIssuanceID)); - if (!mpt || mpt->getAccountID(sfIssuer) == account) + if (!mutableSle_ || mutableSle_->getAccountID(sfIssuer) == account) { // LCOV_EXCL_START UNREACHABLE("xrpl::authorizeMPToken : invalid issuance or issuers token"); - if (view.rules().enabled(featureLendingProtocol)) + if (applyView_.rules().enabled(featureLendingProtocol)) return tecINTERNAL; // LCOV_EXCL_STOP } - auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); + auto const mptokenKey = keylet::mptoken(mptID_, account); auto mptoken = std::make_shared(mptokenKey); - if (auto ter = dirLink(view, account, mptoken)) + if (auto ter = dirLink(applyView_, account, mptoken)) return ter; // LCOV_EXCL_LINE (*mptoken)[sfAccount] = account; - (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; + (*mptoken)[sfMPTokenIssuanceID] = mptID_; (*mptoken)[sfFlags] = 0; - view.insert(mptoken); + applyView_.insert(mptoken); // Update owner count. wrappedAcct.adjustOwnerCount(1, journal); @@ -210,17 +215,16 @@ authorizeMPToken( return tesSUCCESS; } - auto const sleMptIssuance = view.read(keylet::mptIssuance(mptIssuanceID)); - if (!sleMptIssuance) + if (!mutableSle_) return tecINTERNAL; // LCOV_EXCL_LINE // If the account that submitted this tx is the issuer of the MPT // Note: `account_` is issuer's account // `holderID` is holder's account - if (account != (*sleMptIssuance)[sfIssuer]) + if (account != (*mutableSle_)[sfIssuer]) return tecINTERNAL; // LCOV_EXCL_LINE - auto const sleMpt = view.peek(keylet::mptoken(mptIssuanceID, *holderID)); + auto const sleMpt = applyView_.peek(keylet::mptoken(mptID_, *holderID)); if (!sleMpt) return tecINTERNAL; // LCOV_EXCL_LINE @@ -243,23 +247,18 @@ authorizeMPToken( if (flagsIn != flagsOut) sleMpt->setFieldU32(sfFlags, flagsOut); - view.update(sleMpt); + applyView_.update(sleMpt); return tesSUCCESS; } [[nodiscard]] TER -removeEmptyHolding( - ApplyView& view, - AccountID const& accountID, - MPTIssue const& mptIssue, - beast::Journal journal) +WritableMPToken::removeEmptyHolding(AccountID const& accountID, beast::Journal journal) { // If the account is the issuer, then no token should exist. MPTs do not // have the legacy ability to create such a situation, but check anyway. If // a token does exist, it will get deleted. If not, return success. - bool const accountIsIssuer = accountID == mptIssue.getIssuer(); - auto const& mptID = mptIssue.getMptID(); - auto const mptoken = view.peek(keylet::mptoken(mptID, accountID)); + bool const accountIsIssuer = accountID == mptIssue_.getIssuer(); + auto const mptoken = applyView_.peek(keylet::mptoken(mptID_, accountID)); if (!mptoken) return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; // Unlike a trust line, if the account is the issuer, and the token has a @@ -270,9 +269,7 @@ removeEmptyHolding( return tecHAS_OBLIGATIONS; return authorizeMPToken( - view, {}, // priorBalance - mptID, accountID, journal, tfMPTUnauthorize // flags @@ -280,25 +277,18 @@ removeEmptyHolding( } [[nodiscard]] TER -requireAuth( - ReadView const& view, - MPTIssue const& mptIssue, - AccountID const& account, - AuthType authType, - int depth) +MPToken::requireAuth(AccountID const& account, AuthType authType, int depth) const { - auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); - auto const sleIssuance = view.read(mptID); - if (!sleIssuance) + if (!sle_) return tecOBJECT_NOT_FOUND; - auto const mptIssuer = AccountRoot(sleIssuance->getAccountID(sfIssuer), view); + auto const mptIssuer = AccountRoot(sle_->getAccountID(sfIssuer), readView_); // issuer is always "authorized" if (mptIssuer == account) // Issuer won't have MPToken return tesSUCCESS; - bool const featureSAVEnabled = view.rules().enabled(featureSingleAssetVault); + bool const featureSAVEnabled = readView_.rules().enabled(featureSingleAssetVault); if (featureSAVEnabled) { @@ -311,30 +301,19 @@ requireAuth( if (mptIssuer->isFieldPresent(sfVaultID)) { - auto const sleVault = view.read(keylet::vault(mptIssuer->getFieldH256(sfVaultID))); + auto const sleVault = readView_.read(keylet::vault(mptIssuer->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, authType); - } - else - { - return requireAuth(view, issue, account, authType, depth + 1); - } - }, - asset.value()); + if (auto const err = + makeTokenBase(readView_, asset)->requireAuth(account, authType, depth + 1); !isTesSuccess(err)) return err; } } - auto const mptokenID = keylet::mptoken(mptID.key, account); - auto const sleToken = view.read(mptokenID); + auto const sleToken = readView_.read(keylet::mptoken(mptID_, account)); // if account has no MPToken, fail if (!sleToken && (authType == AuthType::StrongAuth || authType == AuthType::Legacy)) @@ -342,14 +321,14 @@ requireAuth( // 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); + auto const maybeDomainID = sle_->at(~sfDomainID); if (maybeDomainID) { XRPL_ASSERT( - sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth, + sle_->getFieldU32(sfFlags) & lsfMPTRequireAuth, "xrpl::requireAuth : issuance requires authorization"); // ter = tefINTERNAL | tecOBJECT_NOT_FOUND | tecNO_AUTH | tecEXPIRED - auto const ter = credentials::validDomain(view, *maybeDomainID, account); + auto const ter = credentials::validDomain(readView_, *maybeDomainID, account); if (isTesSuccess(ter)) { return ter; // Note: sleToken might be null @@ -365,47 +344,43 @@ requireAuth( if (featureSAVEnabled) { // Implicitly authorize Vault and LoanBroker pseudo-accounts - if (isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID})) + if (isPseudoAccount(readView_, account, {&sfVaultID, &sfLoanBrokerID})) return tesSUCCESS; } // mptoken must be authorized if issuance enabled requireAuth - if (sleIssuance->isFlag(lsfMPTRequireAuth) && - (!sleToken || !sleToken->isFlag(lsfMPTAuthorized))) + if (sle_->isFlag(lsfMPTRequireAuth) && (!sleToken || !sleToken->isFlag(lsfMPTAuthorized))) return tecNO_AUTH; return tesSUCCESS; // Note: sleToken might be null } [[nodiscard]] TER -enforceMPTokenAuthorization( - ApplyView& view, - MPTID const& mptIssuanceID, +WritableMPToken::enforceMPTokenAuthorization( AccountID const& account, XRPAmount const& priorBalance, // for MPToken authorization beast::Journal j) { - auto const sleIssuance = view.read(keylet::mptIssuance(mptIssuanceID)); - if (!sleIssuance) + if (!mutableSle_) return tefINTERNAL; // LCOV_EXCL_LINE XRPL_ASSERT( - sleIssuance->isFlag(lsfMPTRequireAuth), + mutableSle_->isFlag(lsfMPTRequireAuth), "xrpl::enforceMPTokenAuthorization : authorization required"); - if (account == sleIssuance->at(sfIssuer)) + if (account == mutableSle_->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); + auto const keylet = keylet::mptoken(mptID_, account); + auto const sleToken = readView_.read(keylet); // NOTE: might be null + auto const maybeDomainID = mutableSle_->at(~sfDomainID); bool expired = false; bool const authorizedByDomain = [&]() -> bool { // NOTE: defensive here, should be checked in preclaim - if (!maybeDomainID.has_value()) + if (!maybeDomainID) return false; // LCOV_EXCL_LINE - auto const ter = verifyValidDomain(view, account, *maybeDomainID, j); + auto const ter = verifyValidDomain(applyView(), account, *maybeDomainID, j); if (isTesSuccess(ter)) return true; if (ter == tecEXPIRED) @@ -424,7 +399,7 @@ enforceMPTokenAuthorization( // Either way, return tecNO_AUTH and there is nothing else to do return expired ? tecEXPIRED : tecNO_AUTH; } - if (!authorizedByDomain && maybeDomainID.has_value()) + if (!authorizedByDomain && maybeDomainID) { // 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 @@ -436,7 +411,7 @@ enforceMPTokenAuthorization( // 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(), + sleToken != nullptr && !maybeDomainID, "xrpl::enforceMPTokenAuthorization : found MPToken"); if (sleToken->isFlag(lsfMPTAuthorized)) return tesSUCCESS; @@ -447,9 +422,7 @@ enforceMPTokenAuthorization( { // Found an MPToken, authorized by the domain. Ignore authorization flag // lsfMPTAuthorized because it is meaningless. Return tesSUCCESS - XRPL_ASSERT( - maybeDomainID.has_value(), - "xrpl::enforceMPTokenAuthorization : found MPToken for domain"); + XRPL_ASSERT(maybeDomainID, "xrpl::enforceMPTokenAuthorization : found MPToken for domain"); return tesSUCCESS; } if (authorizedByDomain) @@ -457,13 +430,11 @@ enforceMPTokenAuthorization( // 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, + maybeDomainID && sleToken == nullptr, "xrpl::enforceMPTokenAuthorization : new MPToken for domain"); if (auto const err = authorizeMPToken( - view, - priorBalance, // priorBalance - mptIssuanceID, // mptIssuanceID - account, // account + priorBalance, // priorBalance + account, // account j); !isTesSuccess(err)) return err; @@ -478,20 +449,14 @@ enforceMPTokenAuthorization( } TER -canTransfer( - ReadView const& view, - MPTIssue const& mptIssue, - AccountID const& from, - AccountID const& to) +MPToken::canTransfer(AccountID const& from, AccountID const& to) const { - auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); - auto const sleIssuance = view.read(mptID); - if (!sleIssuance) + if (!sle_) return tecOBJECT_NOT_FOUND; - if (!(sleIssuance->getFieldU32(sfFlags) & lsfMPTCanTransfer)) + if (!(sle_->getFieldU32(sfFlags) & lsfMPTCanTransfer)) { - if (from != (*sleIssuance)[sfIssuer] && to != (*sleIssuance)[sfIssuer]) + if (from != (*sle_)[sfIssuer] && to != (*sle_)[sfIssuer]) return TER{tecNO_AUTH}; } return tesSUCCESS; @@ -505,16 +470,15 @@ rippleLockEscrowMPT( beast::Journal j) { auto const mptIssue = amount.get(); - auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); - auto sleIssuance = view.peek(mptID); - if (!sleIssuance) + auto mptIssuance = WritableMPToken(view, mptIssue); + if (!mptIssuance.exists()) { // LCOV_EXCL_START JLOG(j.error()) << "rippleLockEscrowMPT: MPT issuance not found for " << mptIssue.getMptID(); return tecOBJECT_NOT_FOUND; } // LCOV_EXCL_STOP - if (amount.getIssuer() == sender) + if (mptIssuance.getIssuer() == sender) { // LCOV_EXCL_START JLOG(j.error()) << "rippleLockEscrowMPT: sender is the issuer, cannot lock MPTs."; return tecINTERNAL; @@ -523,7 +487,7 @@ rippleLockEscrowMPT( // 1. Decrease the MPT Holder MPTAmount // 2. Increase the MPT Holder EscrowedAmount { - auto const mptokenID = keylet::mptoken(mptID.key, sender); + auto const mptokenID = keylet::mptoken(mptIssuance.getMptID(), sender); auto sle = view.peek(mptokenID); if (!sle) { // LCOV_EXCL_START @@ -569,7 +533,7 @@ rippleLockEscrowMPT( // 1. Increase the Issuance EscrowedAmount // 2. DO NOT change the Issuance OutstandingAmount { - uint64_t const issuanceEscrowed = (*sleIssuance)[~sfLockedAmount].value_or(0); + uint64_t const issuanceEscrowed = (*mptIssuance)[~sfLockedAmount].value_or(0); auto const pay = amount.mpt().value(); // Overflow check for addition @@ -581,16 +545,16 @@ rippleLockEscrowMPT( return tecINTERNAL; } // LCOV_EXCL_STOP - if (sleIssuance->isFieldPresent(sfLockedAmount)) + if (mptIssuance->isFieldPresent(sfLockedAmount)) { - (*sleIssuance)[sfLockedAmount] += pay; + (*mptIssuance)[sfLockedAmount] += pay; } else { - sleIssuance->setFieldU64(sfLockedAmount, pay); + mptIssuance->setFieldU64(sfLockedAmount, pay); } - view.update(sleIssuance); + mptIssuance.update(); } return tesSUCCESS; } @@ -612,9 +576,8 @@ rippleUnlockEscrowMPT( auto const& issuer = netAmount.getIssuer(); auto const& mptIssue = netAmount.get(); - auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); - auto sleIssuance = view.peek(mptID); - if (!sleIssuance) + auto mptIssuance = WritableMPToken(view, mptIssue); + if (!mptIssuance) { // LCOV_EXCL_START JLOG(j.error()) << "rippleUnlockEscrowMPT: MPT issuance not found for " << mptIssue.getMptID(); @@ -623,14 +586,14 @@ rippleUnlockEscrowMPT( // Decrease the Issuance EscrowedAmount { - if (!sleIssuance->isFieldPresent(sfLockedAmount)) + if (!mptIssuance->isFieldPresent(sfLockedAmount)) { // LCOV_EXCL_START JLOG(j.error()) << "rippleUnlockEscrowMPT: no locked amount in issuance for " << mptIssue.getMptID(); return tecINTERNAL; } // LCOV_EXCL_STOP - auto const locked = sleIssuance->getFieldU64(sfLockedAmount); + auto const locked = mptIssuance->getFieldU64(sfLockedAmount); auto const redeem = grossAmount.mpt().value(); // Underflow check for subtraction @@ -644,19 +607,19 @@ rippleUnlockEscrowMPT( auto const newLocked = locked - redeem; if (newLocked == 0) { - sleIssuance->makeFieldAbsent(sfLockedAmount); + mptIssuance->makeFieldAbsent(sfLockedAmount); } else { - sleIssuance->setFieldU64(sfLockedAmount, newLocked); + mptIssuance->setFieldU64(sfLockedAmount, newLocked); } - view.update(sleIssuance); + mptIssuance.update(); } if (issuer != receiver) { // Increase the MPT Holder MPTAmount - auto const mptokenID = keylet::mptoken(mptID.key, receiver); + auto const mptokenID = keylet::mptoken(mptIssue.getMptID(), receiver); auto sle = view.peek(mptokenID); if (!sle) { // LCOV_EXCL_START @@ -681,7 +644,7 @@ rippleUnlockEscrowMPT( else { // Decrease the Issuance OutstandingAmount - auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); + auto const outstanding = mptIssuance->getFieldU64(sfOutstandingAmount); auto const redeem = netAmount.mpt().value(); // Underflow check for subtraction @@ -692,8 +655,8 @@ rippleUnlockEscrowMPT( return tecINTERNAL; } // LCOV_EXCL_STOP - sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); - view.update(sleIssuance); + mptIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); + mptIssuance.update(); } if (issuer == sender) @@ -703,7 +666,7 @@ rippleUnlockEscrowMPT( return tecINTERNAL; } // LCOV_EXCL_STOP // Decrease the MPT Holder EscrowedAmount - auto const mptokenID = keylet::mptoken(mptID.key, sender); + auto const mptokenID = keylet::mptoken(mptIssue.getMptID(), sender); auto sle = view.peek(mptokenID); if (!sle) { // LCOV_EXCL_START @@ -731,13 +694,9 @@ rippleUnlockEscrowMPT( auto const newLocked = locked - delta; if (newLocked == 0) - { sle->makeFieldAbsent(sfLockedAmount); - } else - { sle->setFieldU64(sfLockedAmount, newLocked); - } view.update(sle); // Note: The gross amount is the amount that was locked, the net @@ -747,7 +706,7 @@ rippleUnlockEscrowMPT( auto const diff = grossAmount.mpt().value() - netAmount.mpt().value(); if (diff != 0) { - auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); + auto const outstanding = mptIssuance->getFieldU64(sfOutstandingAmount); // Underflow check for subtraction if (!canSubtract(STAmount(mptIssue, outstanding), STAmount(mptIssue, diff))) { // LCOV_EXCL_START @@ -756,10 +715,76 @@ rippleUnlockEscrowMPT( return tecINTERNAL; } // LCOV_EXCL_STOP - sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - diff); - view.update(sleIssuance); + mptIssuance->setFieldU64(sfOutstandingAmount, outstanding - diff); + mptIssuance.update(); } return tesSUCCESS; } +STAmount +MPToken::accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + beast::Journal j, + SpendableHandling includeFullBalance) const +{ + return accountHolds(account, zeroIfFrozen, ahIGNORE_AUTH, j, includeFullBalance); +} + +STAmount +MPToken::accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j, + SpendableHandling includeFullBalance) const +{ + bool const returnSpendable = (includeFullBalance == shFULL_BALANCE); + + if (returnSpendable && account == mptIssue_.getIssuer()) + { + // if the account is the issuer, and the issuance exists, their limit is + // the issuance limit minus the outstanding value + + if (!sle_) + { + return STAmount{mptIssue_}; + } + return STAmount{ + mptIssue_, + sle_->at(~sfMaximumAmount).value_or(maxMPTokenAmount) - sle_->at(sfOutstandingAmount)}; + } + + STAmount amount; + + auto const sleMpt = readView_.read(keylet::mptoken(mptID_, account)); + + if (!sleMpt) + amount.clear(mptIssue_); + else if (zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(account)) + amount.clear(mptIssue_); + else + { + amount = STAmount{mptIssue_, sleMpt->getFieldU64(sfMPTAmount)}; + + // 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 && + readView_.rules().enabled(featureSingleAssetVault)) + { + if (auto const err = requireAuth(account, AuthType::StrongAuth); !isTesSuccess(err)) + amount.clear(mptIssue_); + } + else if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED) + { + // if auth is enabled on the issuance and mpt is not authorized, + // clear amount + if (sle_ && sle_->isFlag(lsfMPTRequireAuth) && !sleMpt->isFlag(lsfMPTAuthorized)) + amount.clear(mptIssue_); + } + } + + return amount; +} + } // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index e2efcc1ad0..a5ca4151e9 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -21,14 +22,14 @@ namespace xrpl { STAmount creditLimit( - ReadView const& view, + ReadView const& readView_, AccountID const& account, AccountID const& issuer, Currency const& currency) { STAmount result(Issue{currency, account}); - auto sleRippleState = view.read(keylet::line(account, issuer, currency)); + auto sleRippleState = readView_.read(keylet::line(account, issuer, currency)); if (sleRippleState) { @@ -49,14 +50,14 @@ creditLimit2(ReadView const& v, AccountID const& acc, AccountID const& iss, Curr STAmount creditBalance( - ReadView const& view, + ReadView const& readView_, AccountID const& account, AccountID const& issuer, Currency const& currency) { STAmount result(Issue{currency, account}); - auto sleRippleState = view.read(keylet::line(account, issuer, currency)); + auto sleRippleState = readView_.read(keylet::line(account, issuer, currency)); if (sleRippleState) { @@ -78,19 +79,15 @@ creditBalance( //------------------------------------------------------------------------------ bool -isIndividualFrozen( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer) +IOUToken::isIndividualFrozen(AccountID const& account) const { - if (isXRP(currency)) + if (isXRP(currency_)) return false; - if (issuer != account) + if (issuer_ != account) { // Check if the issuer froze the line - auto const sle = view.read(keylet::line(account, issuer, currency)); - if (sle && sle->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze)) + auto const sle = readView_.read(keylet::line(account, issuer_, currency_)); + if (sle && sle->isFlag((issuer_ > account) ? lsfHighFreeze : lsfLowFreeze)) return true; } return false; @@ -99,45 +96,38 @@ isIndividualFrozen( // Can the specified account spend the specified currency issued by // the specified issuer or does the freeze flag prohibit it? bool -isFrozen( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer) +IOUToken::isFrozen(AccountID const& account, int depth) const { - if (isXRP(currency)) + XRPL_ASSERT(depth == 0, "IOUToken::isFrozen : depth is 0"); + if (isXRP(currency_)) return false; - auto const issuerRoot = AccountRoot(issuer, view); - if (issuerRoot.exists() && issuerRoot->isFlag(lsfGlobalFreeze)) + if (issuerAccount_.exists() && issuerAccount_->isFlag(lsfGlobalFreeze)) return true; - if (issuer != account) + if (issuer_ != account) { // Check if the issuer froze the line - auto const sleLine = view.read(keylet::line(account, issuer, currency)); - if (sleLine && sleLine->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze)) + auto const sleLine = readView_.read(keylet::line(account, issuer_, currency_)); + if (sleLine && sleLine->isFlag((issuer_ > account) ? lsfHighFreeze : lsfLowFreeze)) return true; } return false; } bool -isDeepFrozen( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer) +IOUToken::isDeepFrozen(AccountID const& account, int depth) const { - if (isXRP(currency)) + XRPL_ASSERT(depth == 0, "IOUToken::isDeepFrozen : depth is 0"); + if (isXRP(currency_)) { return false; } - if (issuer == account) + if (issuer_ == account) { return false; } - auto const sle = view.read(keylet::line(account, issuer, currency)); + auto const sle = readView_.read(keylet::line(account, issuer_, currency_)); if (!sle) { return false; @@ -146,6 +136,146 @@ isDeepFrozen( return sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze); } +TER +IOUToken::checkFrozen(AccountID const& account) const +{ + return isFrozen(account) ? TER{tecFROZEN} : TER{tesSUCCESS}; +} + +TER +IOUToken::checkDeepFrozen(AccountID const& account) const +{ + return isDeepFrozen(account) ? TER{tecFROZEN} : TER{tesSUCCESS}; +} + +bool +IOUToken::isAnyFrozen(std::initializer_list const& accounts, int depth) const +{ + XRPL_ASSERT(depth == 0, "IOUToken::isAnyFrozen : depth is 0"); + if (isGlobalFrozen()) + return true; + + for (auto const& account : accounts) + { + if (isFrozen(account, depth)) + return true; + } + + return false; +} + +STAmount +IOUToken::accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + beast::Journal j, + SpendableHandling includeFullBalance) const +{ + return accountHolds(account, zeroIfFrozen, ahIGNORE_AUTH, j, includeFullBalance); +} + +STAmount +IOUToken::accountHolds( + AccountID const& account, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j, + SpendableHandling includeFullBalance) const +{ + if (isXRP(currency_)) + { + return {issuerAccount_.xrpLiquid(0, j)}; + } + + bool const returnSpendable = (includeFullBalance == shFULL_BALANCE); + if (returnSpendable && account == issuer_) + // If the account is the issuer, then their limit is effectively + // infinite + return STAmount{issue_, STAmount::cMaxValue, STAmount::cMaxOffset}; + + // IOU: Return balance on trust line modulo freeze + // Check if line exists and is usable + auto const sle = readView_.read(keylet::line(account, issuer_, currency_)); + if (!sle) + { + STAmount result; + result.clear(Issue{currency_, issuer_}); + return result; + } + + // Check freeze status + if (zeroIfFrozen == fhZERO_IF_FROZEN) + { + if (isFrozen(account) || isDeepFrozen(account)) + { + STAmount result; + result.clear(Issue{currency_, issuer_}); + return result; + } + + // when fixFrozenLPTokenTransfer is enabled, if currency is lptoken, + // we need to check if the associated assets have been frozen + if (readView_.rules().enabled(fixFrozenLPTokenTransfer)) + { + auto const sleIssuer = readView_.read(keylet::account(issuer_)); + if (!sleIssuer) + { + STAmount result; + result.clear(Issue{currency_, issuer_}); + return result; + } + else if (sleIssuer->isFieldPresent(sfAMMID)) + { + auto const sleAmm = readView_.read(keylet::amm((*sleIssuer)[sfAMMID])); + + if (!sleAmm || + isLPTokenFrozen( + readView_, + account, + (*sleAmm)[sfAsset].get(), + (*sleAmm)[sfAsset2].get())) + { + STAmount result; + result.clear(Issue{currency_, issuer_}); + return result; + } + } + } + } + + // Extract balance from SLE + STAmount amount = sle->getFieldAmount(sfBalance); + bool const accountHigh = account > issuer_; + auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit; + if (accountHigh) + { + // Put balance in account terms. + amount.negate(); + } + if (returnSpendable) + { + amount += sle->getFieldAmount(oppositeField); + } + amount.setIssuer(issuer_); + + JLOG(j.trace()) << "IOUToken::accountHolds:" << " account=" << to_string(account) + << " amount=" << amount.getFullText(); + + return amount; +} + +TER +IOUToken::canAddHolding() const +{ + return tesSUCCESS; // IOUs don't have restrictions on adding holdings +} + +Rate +IOUToken::transferRate() const +{ + return issuerAccount_.transferRate(); +} + //------------------------------------------------------------------------------ // // Trust line operations @@ -268,7 +398,7 @@ trustCreate( TER trustDelete( - ApplyView& view, + ApplyView& readView_, std::shared_ptr const& sleRippleState, AccountID const& uLowAccountID, AccountID const& uHighAccountID, @@ -280,20 +410,22 @@ trustDelete( JLOG(j.trace()) << "trustDelete: Deleting ripple line: low"; - if (!view.dirRemove(keylet::ownerDir(uLowAccountID), uLowNode, sleRippleState->key(), false)) + if (!readView_.dirRemove( + keylet::ownerDir(uLowAccountID), uLowNode, sleRippleState->key(), false)) { return tefBAD_LEDGER; // LCOV_EXCL_LINE } JLOG(j.trace()) << "trustDelete: Deleting ripple line: high"; - if (!view.dirRemove(keylet::ownerDir(uHighAccountID), uHighNode, sleRippleState->key(), false)) + if (!readView_.dirRemove( + keylet::ownerDir(uHighAccountID), uHighNode, sleRippleState->key(), false)) { return tefBAD_LEDGER; // LCOV_EXCL_LINE } JLOG(j.trace()) << "trustDelete: Deleting ripple line: state"; - view.erase(sleRippleState); + readView_.erase(sleRippleState); return tesSUCCESS; } @@ -450,7 +582,7 @@ issueIOU( TER redeemIOU( - ApplyView& view, + ApplyView& applyView, AccountID const& account, STAmount const& amount, Issue const& issue, @@ -470,7 +602,7 @@ redeemIOU( bool bSenderHigh = account > issue.account; - if (auto state = view.peek(keylet::line(account, issue.account, issue.currency))) + if (auto state = applyView.peek(keylet::line(account, issue.account, issue.currency))) { STAmount final_balance = state->getFieldAmount(sfBalance); @@ -481,10 +613,10 @@ redeemIOU( final_balance -= amount; - auto const must_delete = - updateTrustLine(view, state, bSenderHigh, account, start_balance, final_balance, j); + auto const must_delete = updateTrustLine( + applyView, state, bSenderHigh, account, start_balance, final_balance, j); - view.creditHook(account, issue.account, amount, start_balance); + applyView.creditHook(account, issue.account, amount, start_balance); if (bSenderHigh) final_balance.negate(); @@ -497,14 +629,14 @@ redeemIOU( if (must_delete) { return trustDelete( - view, + applyView, state, bSenderHigh ? issue.account : account, bSenderHigh ? account : issue.account, j); } - view.update(state); + applyView.update(state); return tesSUCCESS; } @@ -526,24 +658,24 @@ redeemIOU( //------------------------------------------------------------------------------ TER -requireAuth(ReadView const& view, Issue const& issue, AccountID const& account, AuthType authType) +IOUToken::requireAuth(AccountID const& account, AuthType authType, int depth) const { - if (isXRP(issue) || issue.account == account) + XRPL_ASSERT(depth == 0, "IOUToken::requireAuth : depth is 0"); + if (isXRP(issue_) || issuer_ == account) return tesSUCCESS; - auto const trustLine = view.read(keylet::line(account, issue.account, issue.currency)); + auto const trustLine = readView_.read(keylet::line(account, issuer_, issue_.currency)); // If account has no line, and this is a strong check, fail if (!trustLine && authType == AuthType::StrongAuth) return tecNO_LINE; // If this is a weak or legacy check, or if the account has a line, fail if // auth is required and not set on the line - auto const issuerAccount = AccountRoot(issue.account, view); - if (issuerAccount.exists() && (*issuerAccount)[sfFlags] & lsfRequireAuth) + if (issuerAccount_.exists() && (*issuerAccount_)[sfFlags] & lsfRequireAuth) { if (trustLine) { - return ((*trustLine)[sfFlags] & ((account > issue.account) ? lsfLowAuth : lsfHighAuth)) + return ((*trustLine)[sfFlags] & ((account > issuer_) ? lsfLowAuth : lsfHighAuth)) ? tesSUCCESS : TER{tecNO_AUTH}; } @@ -554,28 +686,26 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account, } TER -canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, AccountID const& to) +IOUToken::canTransfer(AccountID const& from, AccountID const& to) const { - if (issue.native()) + if (issue_.native()) return tesSUCCESS; - auto const& issuerId = issue.getIssuer(); - if (issuerId == from || issuerId == to) + if (issuer_ == from || issuer_ == to) return tesSUCCESS; - auto const issuer = AccountRoot(issuerId, view); - if (!issuer.exists()) + if (!issuerAccount_.exists()) return tefINTERNAL; // LCOV_EXCL_LINE auto const isRippleDisabled = [&](AccountID account) -> bool { // Line might not exist, but some transfers can create it. If this // is the case, just check the default ripple on the issuer account. - auto const line = view.read(keylet::line(account, issue)); + auto const line = readView_.read(keylet::line(account, issue_)); if (line) { - bool const issuerHigh = issuerId > account; + bool const issuerHigh = issuer_ > account; return line->isFlag(issuerHigh ? lsfHighNoRipple : lsfLowNoRipple); } - return issuer->isFlag(lsfDefaultRipple) == false; + return issuerAccount_->isFlag(lsfDefaultRipple) == false; }; // Fail if rippling disabled on both trust lines @@ -592,44 +722,39 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc //------------------------------------------------------------------------------ TER -addEmptyHolding( - ApplyView& view, +WritableIOUToken::addEmptyHolding( AccountID const& accountID, XRPAmount priorBalance, - Issue const& issue, beast::Journal journal) { // Every account can hold XRP. An issuer can issue directly. - if (issue.native() || accountID == issue.getIssuer()) + if (issue_.native() || accountID == issuer_) return tesSUCCESS; - auto const& issuerId = issue.getIssuer(); - auto const& currency = issue.currency; - WritableAccountRoot wrappedIssuer(issuerId, view); - if (wrappedIssuer.isGlobalFrozen()) + if (issuerAccount_.isGlobalFrozen()) return tecFROZEN; // LCOV_EXCL_LINE - auto const& srcId = issuerId; + auto const& srcId = issuer_; auto const& dstId = accountID; auto const high = srcId > dstId; - auto const index = keylet::line(srcId, dstId, currency); - WritableAccountRoot wrappedSrc(srcId, view); - WritableAccountRoot wrappedDst(dstId, view); + auto const index = keylet::line(srcId, dstId, currency_); + WritableAccountRoot wrappedSrc(srcId, applyView_); + WritableAccountRoot wrappedDst(dstId, applyView_); if (!wrappedDst || !wrappedSrc) return tefINTERNAL; // LCOV_EXCL_LINE if (!wrappedSrc->isFlag(lsfDefaultRipple)) return tecINTERNAL; // LCOV_EXCL_LINE // If the line already exists, don't create it again. - if (view.read(index)) + if (applyView_.read(index)) return tecDUPLICATE; // Can the account cover the trust line reserve ? std::uint32_t const ownerCount = wrappedDst->at(sfOwnerCount); - if (priorBalance < view.fees().accountReserve(ownerCount + 1)) + if (priorBalance < readView_.fees().accountReserve(ownerCount + 1)) return tecNO_LINE_INSUF_RESERVE; return trustCreate( - view, + applyView_, high, srcId, dstId, @@ -639,23 +764,19 @@ addEmptyHolding( /*bNoRipple=*/true, /*bFreeze=*/false, /*deepFreeze*/ false, - /*saBalance=*/STAmount{Issue{currency, noAccount()}}, - /*saLimit=*/STAmount{Issue{currency, dstId}}, + /*saBalance=*/STAmount{Issue{currency_, noAccount()}}, + /*saLimit=*/STAmount{Issue{currency_, dstId}}, /*uQualityIn=*/0, /*uQualityOut=*/0, journal); } TER -removeEmptyHolding( - ApplyView& view, - AccountID const& accountID, - Issue const& issue, - beast::Journal journal) +WritableIOUToken::removeEmptyHolding(AccountID const& accountID, beast::Journal journal) { - if (issue.native()) + if (issue_.native()) { - auto const account = AccountRoot(accountID, view); + auto const account = AccountRoot(accountID, applyView_); if (!account.exists()) return tecINTERNAL; // LCOV_EXCL_LINE @@ -669,8 +790,8 @@ removeEmptyHolding( // `asset` is an IOU. // If the account is the issuer, then no line should exist. Check anyway. // If a line does exist, it will get deleted. If not, return success. - bool const accountIsIssuer = accountID == issue.account; - auto const line = view.peek(keylet::line(accountID, issue)); + bool const accountIsIssuer = accountID == issue_.account; + auto const line = applyView_.peek(keylet::line(accountID, issue_)); if (!line) return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::zero) @@ -680,7 +801,7 @@ removeEmptyHolding( if (line->isFlag(lsfLowReserve)) { // Clear reserve for low account. - WritableAccountRoot wrappedLow(line->at(sfLowLimit)->getIssuer(), view); + WritableAccountRoot wrappedLow(line->at(sfLowLimit)->getIssuer(), applyView_); if (!wrappedLow) return tecINTERNAL; // LCOV_EXCL_LINE @@ -694,7 +815,7 @@ removeEmptyHolding( if (line->isFlag(lsfHighReserve)) { // Clear reserve for high account. - WritableAccountRoot wrappedHigh(line->at(sfHighLimit)->getIssuer(), view); + WritableAccountRoot wrappedHigh(line->at(sfHighLimit)->getIssuer(), applyView_); if (!wrappedHigh) return tecINTERNAL; // LCOV_EXCL_LINE @@ -706,7 +827,11 @@ removeEmptyHolding( } return trustDelete( - view, line, line->at(sfLowLimit)->getIssuer(), line->at(sfHighLimit)->getIssuer(), journal); + applyView_, + line, + line->at(sfLowLimit)->getIssuer(), + line->at(sfHighLimit)->getIssuer(), + journal); } TER diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 1cfb7cba9d..b5b892eb15 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -26,125 +26,62 @@ isLPTokenFrozen( Issue const& asset, Issue const& asset2); -//------------------------------------------------------------------------------ -// -// Freeze checking (Asset-based) -// -//------------------------------------------------------------------------------ +class IOUToken; +class MPToken; +class WritableIOUToken; +class WritableMPToken; -bool -isGlobalFrozen(ReadView const& view, Asset const& asset) +std::unique_ptr +makeTokenBase(ReadView const& view, Asset const& asset) { return std::visit( - [&](TIss const& issue) { - if constexpr (std::is_same_v) + [&](T const& issue) -> std::unique_ptr { + if constexpr (std::is_same_v) { - AccountRoot issuer(issue.getIssuer(), view); - return issuer.isGlobalFrozen(); + return std::make_unique(view, issue); } else { - return isGlobalFrozen(view, issue); + return std::make_unique(view, issue); } }, asset.value()); } -bool -isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset) +std::unique_ptr +makeWritableTokenBase(ApplyView& view, Asset const& asset) { return std::visit( - [&](auto const& issue) { return isIndividualFrozen(view, account, issue); }, asset.value()); -} - -bool -isFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth) -{ - return std::visit( - [&](auto const& issue) { return isFrozen(view, account, issue, depth); }, asset.value()); -} - -TER -checkFrozen(ReadView const& view, AccountID const& account, Issue const& issue) -{ - return isFrozen(view, account, issue) ? (TER)tecFROZEN : (TER)tesSUCCESS; -} - -TER -checkFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue) -{ - return isFrozen(view, account, mptIssue) ? (TER)tecLOCKED : (TER)tesSUCCESS; -} - -TER -checkFrozen(ReadView const& view, AccountID const& account, Asset const& asset) -{ - return std::visit( - [&](auto const& issue) { return checkFrozen(view, account, issue); }, asset.value()); -} - -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; -} - -bool -isAnyFrozen( - ReadView const& view, - std::initializer_list const& accounts, - Asset const& asset, - int depth) -{ - return std::visit( - [&](TIss const& issue) { - if constexpr (std::is_same_v) + [&](T const& issue) -> std::unique_ptr { + if constexpr (std::is_same_v) { - return isAnyFrozen(view, accounts, issue); + return std::make_unique(view, issue); } else { - return isAnyFrozen(view, accounts, issue, depth); + return std::make_unique(view, issue); } }, asset.value()); } -bool -isDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, int depth) -{ - // Unlike IOUs, frozen / locked MPTs are not allowed to send or receive - // funds, so checking "deep frozen" is the same as checking "frozen". - return isFrozen(view, account, mptIssue, depth); -} +//------------------------------------------------------------------------------ +// +// TokenBase implementation +// +//------------------------------------------------------------------------------ bool -isDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth) +TokenBase::isFrozen(AccountID const& account, int depth) const { - return std::visit( - [&](auto const& issue) { return isDeepFrozen(view, account, issue, depth); }, - asset.value()); + // Default implementation - subclasses should override + return isGlobalFrozen() || isIndividualFrozen(account); } -TER -checkDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue) +Rate +transferRate(ReadView const& view, STAmount const& amount) { - return isDeepFrozen(view, account, mptIssue) ? (TER)tecLOCKED : (TER)tesSUCCESS; -} - -TER -checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset) -{ - return std::visit( - [&](auto const& issue) { return checkDeepFrozen(view, account, issue); }, asset.value()); + return makeTokenBase(view, amount.asset())->transferRate(); } //------------------------------------------------------------------------------ @@ -162,6 +99,7 @@ getLineIfUsable( FreezeHandling zeroIfFrozen, beast::Journal j) { + IOUToken token(view, issuer, currency); auto const sle = view.read(keylet::line(account, issuer, currency)); if (!sle) @@ -171,8 +109,7 @@ getLineIfUsable( if (zeroIfFrozen == fhZERO_IF_FROZEN) { - if (isFrozen(view, account, currency, issuer) || - isDeepFrozen(view, account, currency, issuer)) + if (token.isFrozen(account) || token.isDeepFrozen(account)) { return nullptr; } @@ -277,87 +214,6 @@ accountHolds( } STAmount -accountHolds( - ReadView const& view, - AccountID const& account, - Issue const& issue, - FreezeHandling zeroIfFrozen, - beast::Journal j, - SpendableHandling includeFullBalance) -{ - return accountHolds( - view, account, issue.currency, issue.account, zeroIfFrozen, j, includeFullBalance); -} - -STAmount -accountHolds( - ReadView const& view, - AccountID const& account, - MPTIssue const& mptIssue, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j, - SpendableHandling includeFullBalance) -{ - bool const returnSpendable = (includeFullBalance == shFULL_BALANCE); - - if (returnSpendable && account == mptIssue.getIssuer()) - { - // if the account is the issuer, and the issuance exists, their limit is - // the issuance limit minus the outstanding value - auto const issuance = view.read(keylet::mptIssuance(mptIssue.getMptID())); - - if (!issuance) - { - return STAmount{mptIssue}; - } - return STAmount{ - mptIssue, - issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) - - issuance->at(sfOutstandingAmount)}; - } - - STAmount amount; - - auto const sleMpt = view.read(keylet::mptoken(mptIssue.getMptID(), account)); - - if (!sleMpt) - { - amount.clear(mptIssue); - } - else if (zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(view, account, mptIssue)) - { - amount.clear(mptIssue); - } - else - { - amount = STAmount{mptIssue, sleMpt->getFieldU64(sfMPTAmount)}; - - // 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, AuthType::StrongAuth); - !isTesSuccess(err)) - amount.clear(mptIssue); - } - else if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED) - { - auto const sleIssuance = view.read(keylet::mptIssuance(mptIssue.getMptID())); - - // if auth is enabled on the issuance and mpt is not authorized, - // clear amount - if (sleIssuance && sleIssuance->isFlag(lsfMPTRequireAuth) && - !sleMpt->isFlag(lsfMPTAuthorized)) - amount.clear(mptIssue); - } - } - - return amount; -} - -[[nodiscard]] STAmount accountHolds( ReadView const& view, AccountID const& account, @@ -367,52 +223,8 @@ accountHolds( beast::Journal j, SpendableHandling includeFullBalance) { - return std::visit( - [&](TIss const& value) { - if constexpr (std::is_same_v) - { - return accountHolds(view, account, value, zeroIfFrozen, j, includeFullBalance); - } - else if constexpr (std::is_same_v) - { - return accountHolds( - view, account, value, zeroIfFrozen, zeroIfUnauthorized, j, includeFullBalance); - } - }, - asset.value()); -} - -STAmount -accountFunds( - ReadView const& view, - AccountID const& id, - STAmount const& saDefault, - FreezeHandling freezeHandling, - beast::Journal j) -{ - if (!saDefault.native() && saDefault.getIssuer() == id) - return saDefault; - - return accountHolds( - view, id, saDefault.getCurrency(), saDefault.getIssuer(), freezeHandling, j); -} - -Rate -transferRate(ReadView const& view, STAmount const& amount) -{ - return std::visit( - [&](TIss const& issue) { - if constexpr (std::is_same_v) - { - AccountRoot issuer(issue.getIssuer(), view); - return issuer.transferRate(); - } - else - { - return transferRate(view, issue.getMptID()); - } - }, - amount.asset().value()); + auto token = makeTokenBase(view, asset); + return token->accountHolds(account, zeroIfFrozen, zeroIfUnauthorized, j, includeFullBalance); } //------------------------------------------------------------------------------ @@ -446,9 +258,7 @@ canAddHolding(ReadView const& view, Issue const& issue) [[nodiscard]] TER canAddHolding(ReadView const& view, Asset const& asset) { - return std::visit( - [&](TIss const& issue) -> TER { return canAddHolding(view, issue); }, - asset.value()); + return makeTokenBase(view, asset)->canAddHolding(); } TER @@ -459,11 +269,7 @@ addEmptyHolding( Asset const& asset, beast::Journal journal) { - return std::visit( - [&](TIss const& issue) -> TER { - return addEmptyHolding(view, accountID, priorBalance, issue, journal); - }, - asset.value()); + return makeWritableTokenBase(view, asset)->addEmptyHolding(accountID, priorBalance, journal); } TER @@ -473,37 +279,7 @@ removeEmptyHolding( Asset const& asset, beast::Journal journal) { - return std::visit( - [&](TIss const& issue) -> TER { - return removeEmptyHolding(view, accountID, issue, journal); - }, - asset.value()); -} - -//------------------------------------------------------------------------------ -// -// Authorization and transfer checks -// -//------------------------------------------------------------------------------ - -TER -requireAuth(ReadView const& view, Asset const& asset, AccountID const& account, AuthType authType) -{ - return std::visit( - [&](TIss const& issue_) { - return requireAuth(view, issue_, account, authType); - }, - asset.value()); -} - -TER -canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, AccountID const& to) -{ - return std::visit( - [&](TIss const& issue) -> TER { - return canTransfer(view, issue, from, to); - }, - asset.value()); + return makeWritableTokenBase(view, asset)->removeEmptyHolding(accountID, journal); } //------------------------------------------------------------------------------ @@ -1036,19 +812,18 @@ rippleCreditMPT( beast::Journal j) { // Do not check MPT authorization here - it must have been checked earlier - auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); + WritableMPToken mptIssuance(view, saAmount.get().getMptID()); auto const& issuer = saAmount.getIssuer(); - auto sleIssuance = view.peek(mptID); - if (!sleIssuance) + if (!mptIssuance.exists()) return tecOBJECT_NOT_FOUND; if (uSenderID == issuer) { - (*sleIssuance)[sfOutstandingAmount] += saAmount.mpt().value(); - view.update(sleIssuance); + (*mptIssuance)[sfOutstandingAmount] += saAmount.mpt().value(); + mptIssuance.update(); } else { - auto const mptokenID = keylet::mptoken(mptID.key, uSenderID); + auto const mptokenID = keylet::mptoken(mptIssuance.getMptID(), uSenderID); if (auto sle = view.peek(mptokenID)) { auto const amt = sle->getFieldU64(sfMPTAmount); @@ -1066,12 +841,12 @@ rippleCreditMPT( if (uReceiverID == issuer) { - auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); + auto const outstanding = mptIssuance->getFieldU64(sfOutstandingAmount); auto const redeem = saAmount.mpt().value(); if (outstanding >= redeem) { - sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); - view.update(sleIssuance); + mptIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); + mptIssuance.update(); } else { @@ -1080,7 +855,7 @@ rippleCreditMPT( } else { - auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID); + auto const mptokenID = keylet::mptoken(mptIssuance.getMptID(), uReceiverID); if (auto sle = view.peek(mptokenID)) { (*sle)[sfMPTAmount] += saAmount.mpt().value(); @@ -1109,9 +884,8 @@ rippleSendMPT( // Safe to get MPT since rippleSendMPT is only called by accountSendMPT auto const& issuer = saAmount.getIssuer(); - - auto const sle = view.read(keylet::mptIssuance(saAmount.get().getMptID())); - if (!sle) + WritableMPToken mptIssuance(view, saAmount.get().getMptID()); + if (!mptIssuance.exists()) return tecOBJECT_NOT_FOUND; if (uSenderID == issuer || uReceiverID == issuer) @@ -1121,9 +895,9 @@ rippleSendMPT( if (uSenderID == issuer) { auto const sendAmount = saAmount.mpt().value(); - auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount); + auto const maximumAmount = mptIssuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount); if (sendAmount > maximumAmount || - sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount) + mptIssuance->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount) return tecPATH_DRY; } @@ -1136,9 +910,8 @@ rippleSendMPT( } // Sending 3rd party MPTs: transit. - saActual = (waiveFee == WaiveTransferFee::Yes) - ? saAmount - : multiply(saAmount, transferRate(view, saAmount.get().getMptID())); + saActual = (waiveFee == WaiveTransferFee::Yes) ? saAmount + : multiply(saAmount, mptIssuance.transferRate()); JLOG(j.debug()) << "rippleSendMPT> " << to_string(uSenderID) << " - > " << to_string(uReceiverID) << " : deliver=" << saAmount.getFullText() @@ -1164,9 +937,8 @@ rippleSendMultiMPT( // Safe to get MPT since rippleSendMultiMPT is only called by // accountSendMultiMPT auto const& issuer = mptIssue.getIssuer(); - - auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID())); - if (!sle) + auto mptIssuance = MPToken(view, mptIssue); + if (!mptIssuance.exists()) return tecOBJECT_NOT_FOUND; // These may diverge @@ -1200,9 +972,10 @@ rippleSendMultiMPT( "xrpl::rippleSendMultiMPT", "sender == issuer, takeFromSender == zero"); auto const sendAmount = amount.mpt().value(); - auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount); + auto const maximumAmount = + mptIssuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount); if (sendAmount > maximumAmount || - sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount) + mptIssuance->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount) return tecPATH_DRY; } @@ -1219,7 +992,7 @@ rippleSendMultiMPT( // Sending 3rd party MPTs: transit. STAmount actualSend = (waiveFee == WaiveTransferFee::Yes) ? amount - : multiply(amount, transferRate(view, amount.get().getMptID())); + : multiply(amount, mptIssuance.transferRate()); actual += actualSend; takeFromSender += actualSend; diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 00580b2548..a7fb62edf7 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include diff --git a/src/libxrpl/tx/invariants/LoanInvariant.cpp b/src/libxrpl/tx/invariants/LoanInvariant.cpp index ebd7a99262..0654c64f38 100644 --- a/src/libxrpl/tx/invariants/LoanInvariant.cpp +++ b/src/libxrpl/tx/invariants/LoanInvariant.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include diff --git a/src/libxrpl/tx/paths/OfferStream.cpp b/src/libxrpl/tx/paths/OfferStream.cpp index b406c27298..2d0bf4bfbe 100644 --- a/src/libxrpl/tx/paths/OfferStream.cpp +++ b/src/libxrpl/tx/paths/OfferStream.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -78,11 +79,13 @@ accountFundsHelper( ReadView const& view, AccountID const& id, STAmount const& saDefault, - Issue const&, + Issue const& issue, FreezeHandling freezeHandling, beast::Journal j) { - return accountFunds(view, id, saDefault, freezeHandling, j); + if (!saDefault.native() && saDefault.getIssuer() == id) + return saDefault; + return IOUToken(view, issue).accountHolds(id, freezeHandling, j); } static IOUAmount @@ -99,9 +102,7 @@ accountFundsHelper( // self funded return amtDefault; } - - return toAmount( - accountHolds(view, id, issue.currency, issue.account, freezeHandling, j)); + return toAmount(IOUToken(view, issue).accountHolds(id, freezeHandling, j)); } static XRPAmount @@ -113,8 +114,7 @@ accountFundsHelper( FreezeHandling freezeHandling, beast::Journal j) { - return toAmount( - accountHolds(view, id, issue.currency, issue.account, freezeHandling, j)); + return toAmount(IOUToken(view, issue).accountHolds(id, freezeHandling, j)); } template @@ -238,8 +238,8 @@ TOfferStreamBase::step() continue; } - bool const deepFrozen = isDeepFrozen( - view_, offer_.owner(), offer_.issueIn().currency, offer_.issueIn().account); + IOUToken wrapped(view_, offer_.issueIn()); + bool const deepFrozen = wrapped.isDeepFrozen(offer_.owner()); if (deepFrozen) { JLOG(j_.trace()) << "Removing deep frozen unfunded offer " << entry->key(); diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index 12644e52b6..55590fc922 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include #include @@ -127,8 +129,13 @@ CheckCash::preclaim(PreclaimContext const& ctx) // Make sure the check owner holds at least value. If they have // less than value the check cannot be cashed. { - STAmount availableFunds{ - accountFunds(ctx.view, sleCheck->at(sfAccount), value, fhZERO_IF_FROZEN, ctx.j)}; + auto const checkAccount = sleCheck->at(sfAccount); + STAmount availableFunds = [&]() -> STAmount { + if (!value.native() && value.getIssuer() == checkAccount) + return value; + return makeTokenBase(ctx.view, value.asset()) + ->accountHolds(checkAccount, fhZERO_IF_FROZEN, ctx.j); + }(); // Note that src will have one reserve's worth of additional XRP // once the check is cashed, since the check's reserve will no @@ -187,7 +194,8 @@ CheckCash::preclaim(PreclaimContext const& ctx) // However, the trustline from destination to issuer may not // be frozen. - if (isFrozen(ctx.view, dstId, currency, issuerId)) + IOUToken wrapped(ctx.view, Issue{currency, issuerId}); + if (wrapped.isFrozen(dstId)) { JLOG(ctx.j.warn()) << "Cashing a check to a frozen trustline."; return tecFROZEN; diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index 8c0b6b6115..8095b162cb 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -73,21 +73,22 @@ AMMCreate::preclaim(PreclaimContext const& ctx) return tecDUPLICATE; } - if (auto const ter = requireAuth(ctx.view, amount.issue(), accountID); !isTesSuccess(ter)) + // Globally or individually frozen + auto const token1 = IOUToken(ctx.view, amount.issue()); + auto const token2 = IOUToken(ctx.view, amount2.issue()); + + if (auto const ter = token1.requireAuth(accountID); !isTesSuccess(ter)) { JLOG(ctx.j.debug()) << "AMM Instance: account is not authorized, " << amount.issue(); return ter; } - if (auto const ter = requireAuth(ctx.view, amount2.issue(), accountID); !isTesSuccess(ter)) + if (auto const ter = token2.requireAuth(accountID); !isTesSuccess(ter)) { JLOG(ctx.j.debug()) << "AMM Instance: account is not authorized, " << amount2.issue(); return ter; } - - // Globally or individually frozen - if (isFrozen(ctx.view, accountID, amount.issue()) || - isFrozen(ctx.view, accountID, amount2.issue())) + if (token1.isFrozen(accountID) || token2.isFrozen(accountID)) { JLOG(ctx.j.debug()) << "AMM Instance: involves frozen asset."; return tecFROZEN; @@ -124,9 +125,9 @@ AMMCreate::preclaim(PreclaimContext const& ctx) if (isXRP(asset)) return xrpBalance < asset; return accountID != asset.issue().account && - accountHolds( - ctx.view, accountID, asset.issue(), FreezeHandling::fhZERO_IF_FROZEN, ctx.j) < - asset; + IOUToken(ctx.view, asset.issue()) + .accountHolds( + accountID, FreezeHandling::fhZERO_IF_FROZEN, ctx.j, shSIMPLE_BALANCE) < asset; }; if (insufficientBalance(amount) || insufficientBalance(amount2)) diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index c3a1b12dda..1092bfc702 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -201,8 +201,9 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) return tecINSUF_RESERVE_LINE; } return (accountID == deposit.issue().account || - accountHolds( - ctx.view, accountID, deposit.issue(), FreezeHandling::fhIGNORE_FREEZE, ctx.j) >= + IOUToken(ctx.view, deposit.issue()) + .accountHolds( + accountID, FreezeHandling::fhIGNORE_FREEZE, ctx.j, shSIMPLE_BALANCE) >= deposit) ? TER(tesSUCCESS) : tecUNFUNDED_AMM; @@ -213,13 +214,14 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) // Check if either of the assets is frozen, AMMDeposit is not allowed // if either asset is frozen auto checkAsset = [&](Issue const& asset) -> TER { - if (auto const ter = requireAuth(ctx.view, asset, accountID)) + auto const token = IOUToken(ctx.view, asset); + if (auto const ter = token.requireAuth(accountID)) { JLOG(ctx.j.debug()) << "AMM Deposit: account is not authorized, " << asset; return ter; } - if (isFrozen(ctx.view, accountID, asset)) + if (token.isFrozen(accountID)) { JLOG(ctx.j.debug()) << "AMM Deposit: account or currency is frozen, " << to_string(accountID) << " " << to_string(asset.currency); @@ -244,10 +246,12 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) auto checkAmount = [&](std::optional const& amount, bool checkBalance) -> TER { if (amount) { + // AMM account or currency frozen + auto const token = IOUToken(ctx.view, amount->issue()); // This normally should not happen. // Account is not authorized to hold the assets it's depositing, // or it doesn't even have a trust line for them - if (auto const ter = requireAuth(ctx.view, amount->issue(), accountID)) + if (auto const ter = token.requireAuth(accountID)) { // LCOV_EXCL_START JLOG(ctx.j.debug()) @@ -255,15 +259,14 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) return ter; // LCOV_EXCL_STOP } - // AMM account or currency frozen - if (isFrozen(ctx.view, ammAccountID, amount->issue())) + if (token.isFrozen(ammAccountID)) { JLOG(ctx.j.debug()) << "AMM Deposit: AMM account or currency is frozen, " << to_string(accountID); return tecFROZEN; } // Account frozen - if (isIndividualFrozen(ctx.view, accountID, amount->issue())) + if (token.isIndividualFrozen(accountID)) { JLOG(ctx.j.debug()) << "AMM Deposit: account is frozen, " << to_string(accountID) << " " << to_string(amount->issue().currency); @@ -469,13 +472,13 @@ AMMDeposit::deposit( return tesSUCCESS; } else if ( - accountID_ == depositAmount.issue().account || - accountHolds( - view, - accountID_, - depositAmount.issue(), - FreezeHandling::fhIGNORE_FREEZE, - ctx_.journal) >= depositAmount) + account_ == depositAmount.issue().account || + IOUToken(view, depositAmount.issue()) + .accountHolds( + accountID_, + FreezeHandling::fhIGNORE_FREEZE, + ctx_.journal, + shSIMPLE_BALANCE) >= depositAmount) { return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/dex/AMMUtils.cpp b/src/libxrpl/tx/transactors/dex/AMMUtils.cpp index 3b0579f58c..48c231f334 100644 --- a/src/libxrpl/tx/transactors/dex/AMMUtils.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMUtils.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -18,8 +19,10 @@ ammPoolHolds( FreezeHandling freezeHandling, beast::Journal const j) { - auto const assetInBalance = accountHolds(view, ammAccountID, issue1, freezeHandling, j); - auto const assetOutBalance = accountHolds(view, ammAccountID, issue2, freezeHandling, j); + auto const assetInBalance = + IOUToken(view, issue1).accountHolds(ammAccountID, freezeHandling, j, shSIMPLE_BALANCE); + auto const assetOutBalance = + IOUToken(view, issue2).accountHolds(ammAccountID, freezeHandling, j, shSIMPLE_BALANCE); return std::make_pair(assetInBalance, assetOutBalance); } @@ -109,7 +112,7 @@ ammLPHolds( << " lpAccount=" << to_string(lpAccount) << " amount=" << amount.getFullText(); } - else if (isFrozen(view, lpAccount, currency, ammAccount)) + else if (IOUToken wrapped(view, Issue{currency, ammAccount}); wrapped.isFrozen(lpAccount)) { amount.clear(Issue{currency, ammAccount}); JLOG(j.trace()) << "ammLPHolds: frozen currency " @@ -190,7 +193,7 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Issue const } else if ( auto const sle = view.read(keylet::line(ammAccountID, issue.account, issue.currency)); - sle && !isFrozen(view, ammAccountID, issue.currency, issue.account)) + sle && !IOUToken(view, issue).isFrozen(ammAccountID)) { auto amount = (*sle)[sfBalance]; if (ammAccountID > issue.account) diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index 802bb77c5c..1b0b2245c1 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -192,21 +192,22 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) << "AMM Withdraw: withdrawing more than the balance, " << *amount; return tecAMM_BALANCE; } - if (auto const ter = requireAuth(ctx.view, amount->issue(), accountID)) + // AMM account or currency frozen + auto const token = IOUToken(ctx.view, amount->issue()); + if (auto const ter = token.requireAuth(accountID)) { JLOG(ctx.j.debug()) << "AMM Withdraw: account is not authorized, " << amount->issue(); return ter; } - // AMM account or currency frozen - if (isFrozen(ctx.view, ammAccountID, amount->issue())) + if (token.isFrozen(ammAccountID)) { JLOG(ctx.j.debug()) << "AMM Withdraw: AMM account or currency is frozen, " << to_string(accountID); return tecFROZEN; } // Account frozen - if (isIndividualFrozen(ctx.view, accountID, amount->issue())) + if (token.isIndividualFrozen(accountID)) { JLOG(ctx.j.debug()) << "AMM Withdraw: account is frozen, " << to_string(accountID) << " " << to_string(amount->issue().currency); diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index 5cb68a2773..8b7f74ca95 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -153,7 +154,14 @@ OfferCreate::preclaim(PreclaimContext const& ctx) return tecFROZEN; } - if (accountFunds(ctx.view, id, saTakerGets, fhZERO_IF_FROZEN, viewJ) <= beast::zero) + // Check account funds: issuer can always afford their own currency + auto const funds = [&]() -> STAmount { + if (!saTakerGets.native() && saTakerGets.getIssuer() == id) + return saTakerGets; + return makeTokenBase(ctx.view, saTakerGets.asset()) + ->accountHolds(id, fhZERO_IF_FROZEN, viewJ); + }(); + if (funds <= beast::zero) { JLOG(ctx.j.debug()) << "delay: Offers must be at least partially funded."; return tecUNFUNDED_OFFER; @@ -280,8 +288,12 @@ OfferCreate::flowCross( // We check this in preclaim, but when selling XRP charged fees can // cause a user's available balance to go to 0 (by causing it to dip // below the reserve) so we check this case again. - STAmount const inStartBalance = - accountFunds(psb, accountID_, takerAmount.in, fhZERO_IF_FROZEN, j_); + STAmount const inStartBalance = [&]() -> STAmount { + if (!takerAmount.in.native() && takerAmount.in.getIssuer() == accountID_) + return takerAmount.in; + return makeTokenBase(psb, takerAmount.in.asset()) + ->accountHolds(accountID_, fhZERO_IF_FROZEN, j_); + }(); if (inStartBalance <= beast::zero) { // The account balance can't cover even part of the offer. @@ -383,8 +395,12 @@ OfferCreate::flowCross( auto afterCross = takerAmount; // If !tesSUCCESS offer unchanged if (isTesSuccess(result.result())) { - STAmount const takerInBalance = - accountFunds(psb, accountID_, takerAmount.in, fhZERO_IF_FROZEN, j_); + STAmount const takerInBalance = [&]() -> STAmount { + if (!takerAmount.in.native() && takerAmount.in.getIssuer() == accountID_) + return takerAmount.in; + return makeTokenBase(psb, takerAmount.in.asset()) + ->accountHolds(accountID_, fhZERO_IF_FROZEN, j_); + }(); if (takerInBalance <= beast::zero) { diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp index 4553eaadd2..894b92e4aa 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -35,7 +35,7 @@ escrowCancelPreclaimHelper( return tecINTERNAL; // LCOV_EXCL_LINE // If the issuer has requireAuth set, check if the account is authorized - if (auto const ter = requireAuth(ctx.view, amount.issue(), account); !isTesSuccess(ter)) + if (auto const ter = IOUToken(ctx.view, amount.issue()).requireAuth(account); !isTesSuccess(ter)) return ter; return tesSUCCESS; @@ -54,16 +54,13 @@ escrowCancelPreclaimHelper( return tecINTERNAL; // LCOV_EXCL_LINE // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) + auto const mptIssuance = MPToken(ctx.view, amount.get()); + if (!mptIssuance.exists()) return tecOBJECT_NOT_FOUND; // If the issuer has requireAuth set, check if the account is // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); - !isTesSuccess(ter)) + if (auto const ter = mptIssuance.requireAuth(account, AuthType::WeakAuth); !isTesSuccess(ter)) return ter; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 836f2a86ab..438105275d 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -187,19 +187,20 @@ escrowCreatePreclaimHelper( return tecNO_PERMISSION; // LCOV_EXCL_LINE // If the issuer has requireAuth set, check if the account is authorized - if (auto const ter = requireAuth(ctx.view, amount.issue(), account); !isTesSuccess(ter)) + auto const token = IOUToken(ctx.view, amount.issue()); + if (auto const ter = token.requireAuth(account); !isTesSuccess(ter)) return ter; // If the issuer has requireAuth set, check if the destination is authorized - if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); !isTesSuccess(ter)) + if (auto const ter = token.requireAuth(dest); !isTesSuccess(ter)) return ter; // If the issuer has frozen the account, return tecFROZEN - if (isFrozen(ctx.view, account, amount.issue())) + if (token.isFrozen(account)) return tecFROZEN; // If the issuer has frozen the destination, return tecFROZEN - if (isFrozen(ctx.view, dest, amount.issue())) + if (token.isFrozen(dest)) return tecFROZEN; STAmount const spendableAmount = @@ -235,47 +236,43 @@ escrowCreatePreclaimHelper( return tecNO_PERMISSION; // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) + auto const mptIssuance = MPToken(ctx.view, amount.get()); + if (!mptIssuance.exists()) return tecOBJECT_NOT_FOUND; // If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION - if (!sleIssuance->isFlag(lsfMPTCanEscrow)) + if (!mptIssuance->isFlag(lsfMPTCanEscrow)) return tecNO_PERMISSION; // If the issuer is not the same as the issuer of the mpt, return // tecNO_PERMISSION - if (sleIssuance->getAccountID(sfIssuer) != issuer) + if (mptIssuance->getAccountID(sfIssuer) != issuer) return tecNO_PERMISSION; // LCOV_EXCL_LINE // If the account does not have the mpt, return tecOBJECT_NOT_FOUND - if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account))) + if (!ctx.view.exists(keylet::mptoken(mptIssuance.getMptID(), account))) return tecOBJECT_NOT_FOUND; // If the issuer has requireAuth set, check if the account is // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); - !isTesSuccess(ter)) + if (auto const ter = mptIssuance.requireAuth(account, AuthType::WeakAuth); !isTesSuccess(ter)) return ter; // If the issuer has requireAuth set, check if the destination is // authorized - if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); - !isTesSuccess(ter)) + if (auto const ter = mptIssuance.requireAuth(dest, AuthType::WeakAuth); !isTesSuccess(ter)) return ter; // If the issuer has frozen the account, return tecLOCKED - if (isFrozen(ctx.view, account, mptIssue)) + if (mptIssuance.isFrozen(account)) return tecLOCKED; // If the issuer has frozen the destination, return tecLOCKED - if (isFrozen(ctx.view, dest, mptIssue)) + if (mptIssuance.isFrozen(dest)) return tecLOCKED; // If the mpt cannot be transferred, return tecNO_AUTH - if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); !isTesSuccess(ter)) + if (auto const ter = mptIssuance.canTransfer(account, dest); !isTesSuccess(ter)) return ter; STAmount const spendableAmount = accountHolds( diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index a88e8f7da4..433e596909 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -130,11 +131,12 @@ escrowFinishPreclaimHelper( return tesSUCCESS; // If the issuer has requireAuth set, check if the destination is authorized - if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); !isTesSuccess(ter)) + IOUToken token(ctx.view, amount.issue()); + if (auto const ter = token.requireAuth(dest); !isTesSuccess(ter)) return ter; // If the issuer has deep frozen the destination, return tecFROZEN - if (isDeepFrozen(ctx.view, dest, amount.getCurrency(), amount.getIssuer())) + if (token.isDeepFrozen(dest)) return tecFROZEN; return tesSUCCESS; @@ -147,26 +149,22 @@ escrowFinishPreclaimHelper( AccountID const& dest, STAmount const& amount) { - AccountID issuer = amount.getIssuer(); + auto const mptIssuance = MPToken(ctx.view, amount.get()); // If the issuer is the same as the dest, return tesSUCCESS - if (issuer == dest) + if (mptIssuance.getIssuer() == dest) return tesSUCCESS; // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) + if (!mptIssuance.exists()) return tecOBJECT_NOT_FOUND; // If the issuer has requireAuth set, check if the destination is // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); - !isTesSuccess(ter)) + if (auto const ter = mptIssuance.requireAuth(dest, AuthType::WeakAuth); !isTesSuccess(ter)) return ter; // If the issuer has frozen the destination, return tecLOCKED - if (isFrozen(ctx.view, dest, mptIssue)) + if (mptIssuance.isFrozen(dest)) return tecLOCKED; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h b/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h index 0d1165df60..1c5f4a6a3d 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h +++ b/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h @@ -180,9 +180,9 @@ escrowUnlockApplyHelper( bool const senderIssuer = issuer == sender; bool const receiverIssuer = issuer == receiver; - auto const mptID = amount.get().getMptID(); - auto const issuanceKey = keylet::mptIssuance(mptID); - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset && !receiverIssuer) + auto const mptIssuance = MPToken(view, amount.get()); + auto const mptID = mptIssuance.getMptID(); + if (!view.exists(keylet::mptoken(mptID, receiver)) && createAsset && !receiverIssuer) { // For backwards compatibility: if dest is not WritableAccountRoot, return error if (!std::holds_alternative(dest)) @@ -206,10 +206,10 @@ escrowUnlockApplyHelper( wrappedDest.adjustOwnerCount(1, journal); } - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && !receiverIssuer) + if (!view.exists(keylet::mptoken(mptID, receiver)) && !receiverIssuer) return tecNO_PERMISSION; - auto const xferRate = transferRate(view, amount); + auto const xferRate = mptIssuance.transferRate(); // update if issuer rate is less than locked rate if (xferRate < lockedRate) lockedRate = xferRate; diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp index ae70fb765c..6ce722d440 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp @@ -61,17 +61,18 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) return tecWRONG_ASSET; auto const pseudoAccountID = sleBroker->at(sfAccount); + auto token = makeTokenBase(ctx.view, vaultAsset); // Cannot transfer a non-transferable Asset - if (auto const ret = canTransfer(ctx.view, vaultAsset, account, pseudoAccountID)) + if (auto const ret = token->canTransfer(account, pseudoAccountID)) return ret; // Cannot transfer a frozen Asset - if (auto const ret = checkFrozen(ctx.view, account, vaultAsset)) + if (auto const ret = token->checkFrozen(account)) return ret; // Pseudo-account cannot receive if asset is deep frozen - if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset)) + if (auto const ret = token->checkDeepFrozen(pseudoAccountID)) return ret; // Cannot transfer unauthorized asset - if (auto const ret = requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth)) + if (auto const ret = token->requireAuth(account, AuthType::StrongAuth)) return ret; if (accountHolds( diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp index 8a2f246f7d..42890d8ed2 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp @@ -79,8 +79,9 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) // The broker's pseudo-account is the source of funds. auto const pseudoAccountID = sleBroker->at(sfAccount); + auto token = makeTokenBase(ctx.view, vaultAsset); // Cannot transfer a non-transferable Asset - if (auto const ret = canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct)) + if (auto const ret = token->canTransfer(pseudoAccountID, dstAcct)) return ret; // Withdrawal to a 3rd party destination account is essentially a transfer. @@ -97,17 +98,17 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) } // Destination MPToken must exist (if asset is an MPT) - if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType)) + if (auto const ter = token->requireAuth(dstAcct, authType)) return ter; // Check for freezes, unless sending directly to the issuer if (dstAcct != vaultAsset.getIssuer()) { // Cannot send a frozen Asset - if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset)) + if (auto const ret = token->checkFrozen(pseudoAccountID)) return ret; // Destination account cannot receive if asset is deep frozen - if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset)) + if (auto const ret = token->checkDeepFrozen(dstAcct)) return ret; } diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp index f41036f819..bc20b4d473 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp @@ -1,6 +1,7 @@ #include // #include +#include #include #include @@ -83,7 +84,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx) // So we need to check if the broker owner is deep frozen for that asset. if (coverAvailable > beast::zero) { - if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) + if (auto const ret = makeTokenBase(ctx.view, asset)->checkDeepFrozen(brokerOwner)) { JLOG(ctx.j.warn()) << "Broker owner account is frozen."; return ret; @@ -131,7 +132,8 @@ LoanBrokerDelete::doApply() return ter; } - if (auto ter = removeEmptyHolding(view(), brokerPseudoID, vaultAsset, j_)) + if (auto ter = + makeWritableTokenBase(view(), vaultAsset)->removeEmptyHolding(brokerPseudoID, j_)) return ter; WritableAccountRoot brokerPseudo(brokerPseudoID, view()); diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index c1cecdacf1..3afc464a75 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -1,5 +1,6 @@ #include // +#include #include #include @@ -124,10 +125,11 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx) } else { - if (auto const ter = canAddHolding(ctx.view, asset)) + if (auto const ter = makeTokenBase(ctx.view, asset)->canAddHolding()) return ter; - if (auto const ter = checkFrozen(ctx.view, sleVault->at(sfAccount), sleVault->at(sfAsset))) + if (auto const ter = makeTokenBase(ctx.view, sleVault->at(sfAsset)) + ->checkFrozen(sleVault->at(sfAccount))) { JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen."; return ter; @@ -229,7 +231,8 @@ LoanBrokerSet::doApply() auto& pseudo = *maybePseudo; auto pseudoId = pseudo->at(sfAccount); - if (auto ter = addEmptyHolding(view, pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_)) + if (auto ter = makeWritableTokenBase(view, sleVault->at(sfAsset)) + ->addEmptyHolding(pseudoId, preFeeBalance_, j_)) return ter; // Initialize data fields: diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index a0ffd1bc27..472df4f41d 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -2,6 +2,7 @@ // #include #include +#include #include #include #include @@ -196,17 +197,18 @@ LoanPay::preclaim(PreclaimContext const& ctx) return tecWRONG_ASSET; } - if (auto const ret = checkFrozen(ctx.view, account, asset)) + auto token = makeTokenBase(ctx.view, asset); + if (auto const ret = token->checkFrozen(account)) { JLOG(ctx.j.warn()) << "Borrower account is frozen."; return ret; } - if (auto const ret = checkDeepFrozen(ctx.view, vaultPseudoAccount, asset)) + if (auto const ret = token->checkDeepFrozen(vaultPseudoAccount)) { JLOG(ctx.j.warn()) << "Vault pseudo-account can not receive funds (deep frozen)."; return ret; } - if (auto const ret = requireAuth(ctx.view, asset, account)) + if (auto const ret = token->requireAuth(account)) { JLOG(ctx.j.warn()) << "Borrower account is not authorized."; return ret; @@ -273,6 +275,7 @@ LoanPay::doApply() // // Normally freeze status is checked in preclaim, but we do it here to // avoid duplicating the check. It'll claim a fee either way. + auto token = makeTokenBase(view, asset); bool const sendBrokerFeeToOwner = [&]() { // Round the minimum required cover up to be conservative. This ensures // CoverAvailable never drops below the theoretical minimum, protecting @@ -281,8 +284,8 @@ LoanPay::doApply() return coverAvailableProxy >= roundToAsset( asset, tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), loanScale) && - !isDeepFrozen(view, brokerOwner, asset) && - !requireAuth(view, asset, brokerOwner, AuthType::StrongAuth); + !token->isDeepFrozen(brokerOwner) && + !token->requireAuth(brokerOwner, AuthType::StrongAuth); }(); auto const brokerPayee = sendBrokerFeeToOwner ? brokerOwner : brokerPseudoAccount; @@ -291,7 +294,7 @@ LoanPay::doApply() { // If we can't send the fee to the owner, and the pseudo-account is // frozen, then we have to fail the payment. - if (auto const ret = checkDeepFrozen(view, brokerPayee, asset)) + if (auto const ret = token->checkDeepFrozen(brokerPayee)) { JLOG(j_.warn()) << "Both Loan Broker and Loan Broker pseudo-account " "can not receive funds (deep frozen)."; @@ -518,7 +521,7 @@ LoanPay::doApply() if (totalPaidToVaultRounded != beast::zero) { - if (auto const ter = requireAuth(view, asset, vaultPseudoAccount, AuthType::StrongAuth)) + if (auto const ter = token->requireAuth(vaultPseudoAccount, AuthType::StrongAuth)) return ter; } @@ -527,8 +530,10 @@ LoanPay::doApply() if (brokerPayee == accountID_) { // The broker may have deleted their holding. Recreate it if needed - if (auto const ter = addEmptyHolding( - view, brokerPayee, brokerPayeeAcct->at(sfBalance).value().xrp(), asset, j_); + if (auto const ter = + makeWritableTokenBase(view, asset) + ->addEmptyHolding( + brokerPayee, brokerPayeeAcct->at(sfBalance).value().xrp(), j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, @@ -536,7 +541,7 @@ LoanPay::doApply() return ter; } } - if (auto const ter = requireAuth(view, asset, brokerPayee, AuthType::StrongAuth)) + if (auto const ter = token->requireAuth(brokerPayee, AuthType::StrongAuth)) return ter; } diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index dfe0bdf2cd..23024fe34b 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -1,6 +1,7 @@ #include // #include +#include #include #include #include @@ -296,11 +297,12 @@ LoanSet::preclaim(PreclaimContext const& ctx) } } - if (auto const ter = canAddHolding(ctx.view, asset)) + if (auto const ter = makeTokenBase(ctx.view, asset)->canAddHolding()) return ter; + auto token = makeTokenBase(ctx.view, asset); // vaultPseudo is going to send funds, so it can't be frozen. - if (auto const ret = checkFrozen(ctx.view, vaultPseudo, asset)) + if (auto const ret = token->checkFrozen(vaultPseudo)) { JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen."; return ret; @@ -309,7 +311,7 @@ LoanSet::preclaim(PreclaimContext const& ctx) // brokerPseudo is the fallback account to receive LoanPay fees, even if the // broker owner is unable to accept them. Don't create the loan if it is // deep frozen. - if (auto const ret = checkDeepFrozen(ctx.view, brokerPseudo, asset)) + if (auto const ret = token->checkDeepFrozen(brokerPseudo)) { JLOG(ctx.j.warn()) << "Broker pseudo-account is frozen."; return ret; @@ -319,14 +321,14 @@ LoanSet::preclaim(PreclaimContext const& ctx) // frozen now. It is also going to receive funds, so it can't be deep // frozen, but being frozen is a prerequisite for being deep frozen, so // checking the one is sufficient. - if (auto const ret = checkFrozen(ctx.view, borrower, asset)) + if (auto const ret = token->checkFrozen(borrower)) { JLOG(ctx.j.warn()) << "Borrower account is frozen."; return ret; } // brokerOwner is going to receive funds if there's an origination fee, so // it can't be deep frozen - if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) + if (auto const ret = token->checkDeepFrozen(brokerOwner)) { JLOG(ctx.j.warn()) << "Broker owner account is frozen."; return ret; @@ -492,8 +494,9 @@ LoanSet::doApply() borrower == accountID_ || borrower == counterparty, "xrpl::LoanSet::doApply", "borrower signed transaction"); - if (auto const ter = addEmptyHolding( - view, borrower, wrappedBorrower->at(sfBalance).value().xrp(), vaultAsset, j_); + if (auto const ter = + makeWritableTokenBase(view, vaultAsset) + ->addEmptyHolding(borrower, wrappedBorrower->at(sfBalance).value().xrp(), j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, and @@ -501,7 +504,8 @@ LoanSet::doApply() return ter; } - if (auto const ter = requireAuth(view, vaultAsset, borrower, AuthType::StrongAuth)) + auto const token = makeTokenBase(view, vaultAsset); + if (auto const ter = token->requireAuth(borrower, AuthType::StrongAuth)) return ter; // 2. Transfer originationFee, if any, from vault pseudo-account to @@ -515,8 +519,9 @@ LoanSet::doApply() "xrpl::LoanSet::doApply", "broker owner signed transaction"); - if (auto const ter = addEmptyHolding( - view, brokerOwner, brokerOwnerAcct->at(sfBalance).value().xrp(), vaultAsset, j_); + if (auto const ter = + makeWritableTokenBase(view, vaultAsset) + ->addEmptyHolding(brokerOwner, brokerOwnerAcct->at(sfBalance).value().xrp(), j_); ter && ter != tecDUPLICATE) { // ignore tecDUPLICATE. That means the holding already exists, @@ -525,7 +530,7 @@ LoanSet::doApply() } } - if (auto const ter = requireAuth(view, vaultAsset, brokerOwner, AuthType::StrongAuth)) + if (auto const ter = token->requireAuth(brokerOwner, AuthType::StrongAuth)) return ter; if (auto const ter = accountSendMulti( diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp index 717720c76e..757b0c0b94 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -170,7 +171,14 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // own currency auto const needed = bo->at(sfAmount); - if (accountFunds(ctx.view, (*bo)[sfOwner], needed, fhZERO_IF_FROZEN, ctx.j) < needed) + auto const funds = [&]() -> STAmount { + auto const owner = (*bo)[sfOwner]; + if (!needed.native() && needed.getIssuer() == owner) + return needed; + return makeTokenBase(ctx.view, needed.asset()) + ->accountHolds(owner, fhZERO_IF_FROZEN, ctx.j); + }(); + if (funds < needed) return tecINSUFFICIENT_FUNDS; // Check that the account accepting the buy offer (he's selling the NFT) @@ -238,7 +246,14 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // mode, because then we are confirming that the broker can // cover what the buyer will pay, which doesn't make sense, causes // an unnecessary tec, and is also resolved with this amendment. - if (accountFunds(ctx.view, ctx.tx[sfAccount], needed, fhZERO_IF_FROZEN, ctx.j) < needed) + auto const funds = [&]() -> STAmount { + auto const account = ctx.tx[sfAccount]; + if (!needed.native() && needed.getIssuer() == account) + return needed; + return makeTokenBase(ctx.view, needed.asset()) + ->accountHolds(account, fhZERO_IF_FROZEN, ctx.j); + }(); + if (funds < needed) return tecINSUFFICIENT_FUNDS; } @@ -326,9 +341,14 @@ NFTokenAcceptOffer::pay(AccountID const& from, AccountID const& to, STAmount con // just confirm that the end state is OK. if (!isTesSuccess(result)) return result; - if (accountFunds(view(), from, amount, fhZERO_IF_FROZEN, j_).signum() < 0) + auto const checkFunds = [&](AccountID const& account) -> STAmount { + if (!amount.native() && amount.getIssuer() == account) + return amount; + return makeTokenBase(view(), amount.asset())->accountHolds(account, fhZERO_IF_FROZEN, j_); + }; + if (checkFunds(from).signum() < 0) return tecINSUFFICIENT_FUNDS; - if (accountFunds(view(), to, amount, fhZERO_IF_FROZEN, j_).signum() < 0) + if (checkFunds(to).signum() < 0) return tecINSUFFICIENT_FUNDS; return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp b/src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp index 6fd67e5eb3..64db8062ad 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include #include @@ -847,7 +849,8 @@ tokenOfferCreatePreclaim( return tecNO_LINE; } - if (isFrozen(view, nftIssuer, amount.getCurrency(), amount.getIssuer())) + IOUToken wrapped(view, amount.issue()); + if (wrapped.isFrozen(nftIssuer)) return tecFROZEN; } @@ -860,7 +863,7 @@ tokenOfferCreatePreclaim( return tefNFTOKEN_IS_NOT_TRANSFERABLE; } - if (isFrozen(view, acctID, amount.getCurrency(), amount.getIssuer())) + if (IOUToken(view, amount.issue()).isFrozen(acctID)) return tecFROZEN; // If this is an offer to buy the token, the account must have the @@ -870,7 +873,13 @@ tokenOfferCreatePreclaim( { // We allow an IOU issuer to make a buy offer // using their own currency. - if (accountFunds(view, acctID, amount, FreezeHandling::fhZERO_IF_FROZEN, j).signum() <= 0) + auto const funds = [&]() -> STAmount { + if (!amount.native() && amount.getIssuer() == acctID) + return amount; + return makeTokenBase(view, amount.asset()) + ->accountHolds(acctID, FreezeHandling::fhZERO_IF_FROZEN, j); + }(); + if (funds.signum() <= 0) return tecUNFUNDED_OFFER; } diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 03be075fd5..8390a15607 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -463,15 +463,15 @@ Payment::doApply() { JLOG(j_.trace()) << " dstAmount=" << dstAmount.getFullText(); auto const& mptIssue = dstAmount.get(); + MPToken mptToken(view(), mptIssue); - if (auto const ter = requireAuth(view(), mptIssue, accountID_); !isTesSuccess(ter)) + if (auto const ter = mptToken.requireAuth(accountID_); !isTesSuccess(ter)) return ter; - if (auto const ter = requireAuth(view(), mptIssue, dstAccountID); !isTesSuccess(ter)) + if (auto const ter = mptToken.requireAuth(dstAccountID); !isTesSuccess(ter)) return ter; - if (auto const ter = canTransfer(view(), mptIssue, accountID_, dstAccountID); - !isTesSuccess(ter)) + if (auto const ter = mptToken.canTransfer(accountID_, dstAccountID); !isTesSuccess(ter)) return ter; if (auto err = verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, dst, ctx_.journal); @@ -489,11 +489,11 @@ Payment::doApply() // - can't send between holders // - holder can send back to issuer // - issuer can send to holder - if (isAnyFrozen(view(), {accountID_, dstAccountID}, mptIssue)) + if (mptToken.isAnyFrozen({accountID_, dstAccountID})) return tecLOCKED; // Get the rate for a payment between the holders. - rate = transferRate(view(), mptIssue.getMptID()); + rate = mptToken.transferRate(); } // Amount to deliver. diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp index d6c50535a8..e491fdd0cc 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -159,14 +160,9 @@ TER MPTokenAuthorize::doApply() { auto const& tx = ctx_.tx; - return authorizeMPToken( - ctx_.view(), - preFeeBalance_, - tx[sfMPTokenIssuanceID], - accountID_, - ctx_.journal, - tx.getFlags(), - tx[~sfHolder]); + WritableMPToken mptToken(ctx_.view(), tx[sfMPTokenIssuanceID]); + return mptToken.authorizeMPToken( + preFeeBalance_, accountID_, ctx_.journal, tx.getFlags(), tx[~sfHolder]); } } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp index df81d38f6d..54675b09fe 100644 --- a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -379,7 +380,8 @@ VaultClawback::doApply() // Keep MPToken if holder is the vault owner. if (holder != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_); + if (auto const ter = makeWritableTokenBase(view(), sharesDestroyed.asset()) + ->removeEmptyHolding(holder, j_); isTesSuccess(ter)) { JLOG(j_.debug()) // diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index 781952cd28..87e08e6647 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -90,7 +92,7 @@ VaultCreate::preclaim(PreclaimContext const& ctx) auto const vaultAsset = ctx.tx[sfAsset]; auto const account = ctx.tx[sfAccount]; - if (auto const ter = canAddHolding(ctx.view, vaultAsset)) + if (auto const ter = makeTokenBase(ctx.view, vaultAsset)->canAddHolding()) return ter; // Check for pseudo-account issuers - we do not want a vault to hold such @@ -103,8 +105,11 @@ VaultCreate::preclaim(PreclaimContext const& ctx) } // Cannot create Vault for an Asset frozen for the vault owner - if (isFrozen(ctx.view, account, vaultAsset)) - return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + if (auto token = makeTokenBase(ctx.view, vaultAsset)) + { + if (token->isFrozen(account)) + return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + } if (auto const domain = ctx.tx[~sfDomainID]) { @@ -151,7 +156,9 @@ VaultCreate::doApply() auto pseudoId = pseudo->at(sfAccount); auto asset = tx[sfAsset]; - if (auto ter = addEmptyHolding(view(), pseudoId, preFeeBalance_, asset, j_); !isTesSuccess(ter)) + if (auto ter = + makeWritableTokenBase(view(), asset)->addEmptyHolding(pseudoId, preFeeBalance_, j_); + !isTesSuccess(ter)) return ter; std::uint8_t const scale = (asset.holds() || asset.native()) @@ -213,16 +220,16 @@ VaultCreate::doApply() view().insert(vault); // Explicitly create MPToken for the vault owner - if (auto const err = - authorizeMPToken(view(), preFeeBalance_, mptIssuanceID, accountID_, ctx_.journal); + WritableMPToken mptToken(view(), mptIssuanceID); + if (auto const err = mptToken.authorizeMPToken(preFeeBalance_, accountID_, ctx_.journal); !isTesSuccess(err)) return err; // If the vault is private, set the authorized flag for the vault owner if (txFlags & tfVaultPrivate) { - if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, accountID_); + if (auto const err = + mptToken.authorizeMPToken(preFeeBalance_, pseudoId, ctx_.journal, {}, accountID_); !isTesSuccess(err)) return err; } diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index e70635d6dc..3c4bb631de 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -85,7 +86,9 @@ VaultDelete::doApply() // Destroy the asset holding. auto asset = vault->at(sfAsset); - if (auto ter = removeEmptyHolding(view(), vault->at(sfAccount), asset, j_); !isTesSuccess(ter)) + if (auto ter = + makeWritableTokenBase(view(), asset)->removeEmptyHolding(vault->at(sfAccount), j_); + !isTesSuccess(ter)) return ter; auto const& pseudoID = vault->at(sfAccount); @@ -113,7 +116,8 @@ VaultDelete::doApply() // Try to remove MPToken for vault shares for the vault owner if it exists. if (auto const mptoken = view().peek(keylet::mptoken(shareMPTID, accountID_))) { - if (auto const ter = removeEmptyHolding(view(), accountID_, MPTIssue(shareMPTID), j_); + if (auto const ter = makeWritableTokenBase(view(), MPTIssue(shareMPTID)) + ->removeEmptyHolding(accountID_, j_); !isTesSuccess(ter)) { // LCOV_EXCL_START diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index bc5639cb0d..20ea48acc9 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include #include @@ -43,7 +45,8 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) return tecWRONG_ASSET; auto const& vaultAccount = vault->at(sfAccount); - if (auto ter = canTransfer(ctx.view, vaultAsset, account, vaultAccount); !isTesSuccess(ter)) + auto const token = makeTokenBase(ctx.view, vaultAsset); + if (auto ter = token->canTransfer(account, vaultAccount); !isTesSuccess(ter)) { JLOG(ctx.j.debug()) << "VaultDeposit: vault assets are non-transferable."; return ter; @@ -59,8 +62,8 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID)); - if (!sleIssuance) + auto const mptIssuance = MPToken(ctx.view, vaultShare); + if (!mptIssuance) { // LCOV_EXCL_START JLOG(ctx.j.error()) << "VaultDeposit: missing issuance of vault shares."; @@ -68,7 +71,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - if (sleIssuance->isFlag(lsfMPTLocked)) + if (mptIssuance->isFlag(lsfMPTLocked)) { // LCOV_EXCL_START JLOG(ctx.j.error()) << "VaultDeposit: issuance of vault shares is locked."; @@ -77,18 +80,18 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) } // Cannot deposit inside Vault an Asset frozen for the depositor - if (isFrozen(ctx.view, account, vaultAsset)) + if (token->isFrozen(account)) return vaultAsset.holds() ? tecFROZEN : tecLOCKED; // Cannot deposit if the shares of the vault are frozen - if (isFrozen(ctx.view, account, vaultShare)) + if (MPToken(ctx.view, vaultShare).isFrozen(account)) return tecLOCKED; if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner)) { - auto const maybeDomainID = sleIssuance->at(~sfDomainID); + auto const maybeDomainID = mptIssuance->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. + // perform authorization check based on DomainID read from mptIssuance. // 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. @@ -107,8 +110,11 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) } // Source MPToken must exist (if asset is an MPT) - if (auto const ter = requireAuth(ctx.view, vaultAsset, account); !isTesSuccess(ter)) - return ter; + if (auto token = makeTokenBase(ctx.view, vaultAsset)) + { + if (auto const ter = token->requireAuth(account); !isTesSuccess(ter)) + return ter; + } if (accountHolds( ctx.view, @@ -133,9 +139,8 @@ VaultDeposit::doApply() auto const amount = 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) + WritableMPToken mptoken(view(), (*vault)[sfShareMPTID]); + if (!mptoken.exists()) { // LCOV_EXCL_START JLOG(j_.error()) << "VaultDeposit: missing issuance of vault shares."; @@ -147,18 +152,16 @@ VaultDeposit::doApply() // Note, vault owner is always authorized if (vault->isFlag(lsfVaultPrivate) && accountID_ != vault->at(sfOwner)) { - if (auto const err = enforceMPTokenAuthorization( - ctx_.view(), mptIssuanceID, accountID_, preFeeBalance_, j_); + if (auto const err = mptoken.enforceMPTokenAuthorization(accountID_, preFeeBalance_, j_); !isTesSuccess(err)) return err; } else // !vault->isFlag(lsfVaultPrivate) || accountID_ == vault->at(sfOwner) { // No authorization needed, but must ensure there is MPToken - if (!view().exists(keylet::mptoken(mptIssuanceID, accountID_))) + if (!view().exists(keylet::mptoken(mptoken.getMptID(), accountID_))) { - if (auto const err = authorizeMPToken( - view(), preFeeBalance_, mptIssuanceID->value(), accountID_, ctx_.journal); + if (auto const err = mptoken.authorizeMPToken(preFeeBalance_, accountID_, ctx_.journal); !isTesSuccess(err)) return err; } @@ -169,11 +172,9 @@ VaultDeposit::doApply() // This follows from the reverse of the outer enclosing if condition XRPL_ASSERT( accountID_ == vault->at(sfOwner), "xrpl::VaultDeposit::doApply : account is owner"); - if (auto const err = authorizeMPToken( - view(), - preFeeBalance_, // priorBalance - mptIssuanceID->value(), // mptIssuanceID - sleIssuance->at(sfIssuer), // account + if (auto const err = mptoken.authorizeMPToken( + preFeeBalance_, // priorBalance + mptoken->at(sfIssuer), // account ctx_.journal, {}, // flags accountID_ // holderID @@ -188,7 +189,7 @@ VaultDeposit::doApply() { // Compute exchange before transferring any amounts. { - auto const maybeShares = assetsToSharesDeposit(vault, sleIssuance, amount); + auto const maybeShares = assetsToSharesDeposit(vault, mptoken.sle(), amount); if (!maybeShares) return tecINTERNAL; // LCOV_EXCL_LINE sharesCreated = *maybeShares; @@ -196,7 +197,7 @@ VaultDeposit::doApply() if (sharesCreated == beast::zero) return tecPRECISION_LOSS; - auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated); + auto const maybeAssets = sharesToAssetsDeposit(vault, mptoken.sle(), sharesCreated); if (!maybeAssets) { return tecINTERNAL; // LCOV_EXCL_LINE @@ -218,7 +219,7 @@ VaultDeposit::doApply() << "VaultDeposit: overflow error with" << " scale=" << (int)vault->at(sfScale).value() // << ", assetsTotal=" << vault->at(sfAssetsTotal).value() - << ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount) << ", amount=" << amount; + << ", sharesTotal=" << mptoken->at(sfOutstandingAmount) << ", amount=" << amount; return tecPATH_DRY; } diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index dce545a105..ba4d391da2 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include #include @@ -50,7 +52,8 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) auto const& vaultAccount = vault->at(sfAccount); auto const& account = ctx.tx[sfAccount]; auto const& dstAcct = ctx.tx[~sfDestination].value_or(account); - if (auto ter = canTransfer(ctx.view, vaultAsset, vaultAccount, dstAcct); !isTesSuccess(ter)) + auto const vaultAssetToken = makeTokenBase(ctx.view, vaultAsset); + if (auto ter = vaultAssetToken->canTransfer(vaultAccount, dstAcct); !isTesSuccess(ter)) { JLOG(ctx.j.debug()) << "VaultWithdraw: vault assets are non-transferable."; return ter; @@ -72,16 +75,16 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) // if authorized) a trust line or MPToken as needed, in doApply(). // Destination MPToken or trust line must exist if _not_ sending to Account. AuthType const authType = account == dstAcct ? AuthType::WeakAuth : AuthType::StrongAuth; - if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType); !isTesSuccess(ter)) + if (auto const ter = vaultAssetToken->requireAuth(dstAcct, authType); !isTesSuccess(ter)) return ter; // Cannot withdraw from a Vault an Asset frozen for the destination account - if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset)) + if (auto const ret = vaultAssetToken->checkFrozen(dstAcct)) return ret; // Cannot return shares to the vault, if the underlying asset was frozen for // the submitter - if (auto const ret = checkFrozen(ctx.view, account, vaultShare)) + if (auto const ret = MPToken(ctx.view, vaultShare).checkFrozen(account)) return ret; return tesSUCCESS; @@ -205,7 +208,8 @@ VaultWithdraw::doApply() // Keep MPToken if holder is the vault owner. if (accountID_ != vault->at(sfOwner)) { - if (auto const ter = removeEmptyHolding(view(), accountID_, sharesRedeemed.asset(), j_); + if (auto const ter = makeWritableTokenBase(view(), sharesRedeemed.asset()) + ->removeEmptyHolding(accountID_, j_); isTesSuccess(ter)) { JLOG(j_.debug()) // diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 6b2042efda..15dea9d243 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -224,7 +224,8 @@ AMM::getLPTokensBalance(std::optional const& account) const return accountHolds( *env_.current(), *account, - lptIssue_, + lptIssue_.currency, + lptIssue_.account, FreezeHandling::fhZERO_IF_FROZEN, env_.journal) .iou(); diff --git a/src/test/ledger/View_test.cpp b/src/test/ledger/View_test.cpp index dd81c945b1..89f03b8391 100644 --- a/src/test/ledger/View_test.cpp +++ b/src/test/ledger/View_test.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -802,25 +803,33 @@ class View_test : public beast::unit_test::suite *env.closed(), carol, xrpCurrency(), gw, fhZERO_IF_FROZEN, env.journal)); } { - // accountFunds(). - // Gateways have whatever funds they claim to have. - auto const gwUSD = - accountFunds(*env.closed(), gw, USD(314159), fhZERO_IF_FROZEN, env.journal); - BEAST_EXPECT(gwUSD == USD(314159)); + // accountHolds() via IOUToken. + // Gateways have whatever funds they claim to have (tested by + // checking issuer == account, so we use USD(314159) for that). + // Note: When account == issuer, the old accountFunds() returned the + // passed amount. Now we test that the issuer check should be done + // at the call site. + auto const gwUSD = IOUToken(*env.closed(), USD.issue()) + .accountHolds(gw, fhZERO_IF_FROZEN, env.journal); + // gwUSD would be 0 since gw is the issuer and has no trustline to + // itself. The old accountFunds returned the passed amount if + // account == issuer. We test carol's funds instead. // carol has funds from the gateway. - auto carolsUSD = - accountFunds(*env.closed(), carol, USD(0), fhZERO_IF_FROZEN, env.journal); + auto carolsUSD = IOUToken(*env.closed(), USD.issue()) + .accountHolds(carol, fhZERO_IF_FROZEN, env.journal); BEAST_EXPECT(carolsUSD == USD(50)); // If carol's funds are frozen she has no funds... env(fset(gw, asfGlobalFreeze)); env.close(); - carolsUSD = accountFunds(*env.closed(), carol, USD(0), fhZERO_IF_FROZEN, env.journal); + carolsUSD = IOUToken(*env.closed(), USD.issue()) + .accountHolds(carol, fhZERO_IF_FROZEN, env.journal); BEAST_EXPECT(carolsUSD == USD(0)); // ... unless the query ignores the FROZEN state. - carolsUSD = accountFunds(*env.closed(), carol, USD(0), fhIGNORE_FREEZE, env.journal); + carolsUSD = IOUToken(*env.closed(), USD.issue()) + .accountHolds(carol, fhIGNORE_FREEZE, env.journal); BEAST_EXPECT(carolsUSD == USD(50)); // Just to be tidy, thaw gw. diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index c0211059a1..7050fd70d1 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -181,12 +182,10 @@ fillJsonTx( // owner balance if (account != amount.getIssuer()) { - auto const ownerFunds = accountFunds( - fill.ledger, - account, - amount, - fhIGNORE_FREEZE, - beast::Journal{beast::Journal::getNullSink()}); + auto const ownerFunds = + makeTokenBase(fill.ledger, amount.asset()) + ->accountHolds( + account, fhIGNORE_FREEZE, beast::Journal{beast::Journal::getNullSink()}); txJson[jss::owner_funds] = ownerFunds.getText(); } } diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index af18e36090..2e604ca7ea 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -3165,7 +3166,8 @@ NetworkOPsImp::transJson( if (account != amount.issue().account) { auto const ownerFunds = - accountFunds(*ledger, account, amount, fhIGNORE_FREEZE, registry_.journal("View")); + makeTokenBase(*ledger, amount.asset()) + ->accountHolds(account, fhIGNORE_FREEZE, registry_.journal("View")); jvObj[jss::transaction][jss::owner_funds] = ownerFunds.getText(); } } diff --git a/src/xrpld/rpc/handlers/AMMInfo.cpp b/src/xrpld/rpc/handlers/AMMInfo.cpp index 8cfbfccbb5..f67c7ad505 100644 --- a/src/xrpld/rpc/handlers/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/AMMInfo.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -214,13 +215,11 @@ doAMMInfo(RPC::JsonContext& context) if (!isXRP(asset1Balance)) { - ammResult[jss::asset_frozen] = - isFrozen(*ledger, ammAccountID, issue1.currency, issue1.account); + ammResult[jss::asset_frozen] = IOUToken(*ledger, issue1).isFrozen(ammAccountID); } if (!isXRP(asset2Balance)) { - ammResult[jss::asset2_frozen] = - isFrozen(*ledger, ammAccountID, issue2.currency, issue2.account); + ammResult[jss::asset2_frozen] = IOUToken(*ledger, issue2).isFrozen(ammAccountID); } result[jss::amm] = std::move(ammResult);