diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index e3efe8fec2..81cb7bd0be 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -187,6 +187,7 @@ enum LedgerSpecificFlags { lsfMPTCanTrade = 0x00000010, lsfMPTCanTransfer = 0x00000020, lsfMPTCanClawback = 0x00000040, + lsfMPTMutableMeta = 0x00000080, // ltMPTOKEN lsfMPTAuthorized = 0x00000002, diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 2831933afb..03282a6918 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -148,8 +148,9 @@ constexpr std::uint32_t const tfMPTCanEscrow = lsfMPTCanEscrow; constexpr std::uint32_t const tfMPTCanTrade = lsfMPTCanTrade; constexpr std::uint32_t const tfMPTCanTransfer = lsfMPTCanTransfer; constexpr std::uint32_t const tfMPTCanClawback = lsfMPTCanClawback; +constexpr std::uint32_t const tfMPTMutableMeta = lsfMPTMutableMeta; constexpr std::uint32_t const tfMPTokenIssuanceCreateMask = - ~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback); + ~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback | tfMPTMutableMeta); // MPTokenAuthorize flags: constexpr std::uint32_t const tfMPTUnauthorize = 0x00000001; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 63c1b2258b..e7ee5762a1 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -35,6 +35,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(MPTMutableMeta, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMClawbackRounding, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 1d59e71850..0c5a106f20 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -420,6 +420,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation:: TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, + {sfMPTokenMetadata, soeOPTIONAL}, })) /** This transaction type authorizes a MPToken instance */ diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 46b64e40f2..c47d52cea0 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -22,6 +22,7 @@ #include #include +#include #include namespace ripple { @@ -86,6 +87,14 @@ class MPToken_test : public beast::unit_test::suite .metadata = "", .err = temMALFORMED}); + // oversized metadata returns error + mptAlice.create( + {.maxAmt = 100, + .assetScale = 0, + .transferFee = 0, + .metadata = std::string(2050, 'B'), + .err = temMALFORMED}); + // MaximumAmout of 0 returns error mptAlice.create( {.maxAmt = 0, @@ -108,6 +117,29 @@ class MPToken_test : public beast::unit_test::suite .metadata = "test", .err = temMALFORMED}); } + + // test preflight tfMPTMutableMeta when feature is disabled + { + Env env{*this, features - featureMPTMutableMeta}; + MPTTester mptAlice(env, alice); + + mptAlice.create( + {.maxAmt = 100, + .assetScale = 0, + .transferFee = 0, + .flags = tfMPTMutableMeta, + .err = temDISABLED} + ); + + mptAlice.create( + {.maxAmt = 100, + .assetScale = 0, + .transferFee = 0, + .metadata = "abc", + .flags = tfMPTMutableMeta, + .err = temDISABLED} + ); + } } void @@ -512,6 +544,14 @@ class MPToken_test : public beast::unit_test::suite .holder = alice, .flags = tfMPTLock, .err = temMALFORMED}); + + // Oversized metadata + mptAlice.set( + {.account = alice, + .holder = alice, + .metadata = std::string(2050, 'B'), + .flags = tfMPTLock, + .err = temMALFORMED}); } // Validate fields in MPTokenIssuanceSet (preclaim) diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index d33432d316..f23701272d 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -19,6 +19,7 @@ #include +#include #include namespace ripple { @@ -231,6 +232,8 @@ MPTTester::set(MPTSet const& arg) Throw("MPT has not been created"); jv[sfMPTokenIssuanceID] = to_string(*id_); } + if (arg.metadata) + jv[sfMPTokenMetadata] = strHex(*arg.metadata); if (arg.holder) jv[sfHolder] = arg.holder->human(); if (arg.delegate) diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 64eaa452f5..425f0882b1 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -135,6 +135,7 @@ struct MPTSet std::optional account = std::nullopt; std::optional holder = std::nullopt; std::optional id = std::nullopt; + std::optional metadata = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index 1b96b27f24..37875727fa 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -30,6 +30,8 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureMPTokensV1)) return temDISABLED; + if (ctx.tx.isFlag(tfMPTMutableMeta) && !ctx.rules.enabled(featureMPTMutableMeta)) + return temDISABLED; if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 06ea089526..88d2d97454 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -21,6 +21,9 @@ #include #include +#include +#include +#include #include namespace ripple { @@ -30,6 +33,8 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureMPTokensV1)) return temDISABLED; + if (ctx.tx.isFieldPresent(sfMPTokenMetadata) && !ctx.rules.enabled(featureMPTMutableMeta)) + return temDISABLED; if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -43,6 +48,13 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) else if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock)) return temINVALID_FLAG; + if (auto const metadata = ctx.tx[~sfMPTokenMetadata]) + { + // Note: metadata->length() == 0 is valid here, will erase metatdata + if (metadata->length() > maxMPTokenMetadataLength) + return temMALFORMED; + } + auto const accountID = ctx.tx[sfAccount]; auto const holderID = ctx.tx[~sfHolder]; if (holderID && accountID == holderID) @@ -97,6 +109,11 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) if (!sleMptIssuance) return tecOBJECT_NOT_FOUND; + if (ctx.tx.isFieldPresent(sfMPTokenMetadata) && !sleMptIssuance->isFlag(lsfMPTMutableMeta)) + return tecNO_PERMISSION; + + // TODO check below is invalid if we are trying to update metadata + // if the mpt has disabled locking if (!((*sleMptIssuance)[sfFlags] & lsfMPTCanLock)) return tecNO_PERMISSION;