Support DynamicMPT XLS-94d (#5705)

* extends the functionality of the MPTokenIssuanceSet transaction, allowing the issuer to update fields or flags that were explicitly marked as mutable during creation.
This commit is contained in:
yinyiqian1
2025-09-15 15:42:36 -04:00
committed by GitHub
parent 3e4e9a2ddc
commit ccb9f1e42d
12 changed files with 1215 additions and 26 deletions

View File

@@ -188,6 +188,15 @@ enum LedgerSpecificFlags {
lsfMPTCanTransfer = 0x00000020,
lsfMPTCanClawback = 0x00000040,
lsfMPTCanMutateCanLock = 0x00000002,
lsfMPTCanMutateRequireAuth = 0x00000004,
lsfMPTCanMutateCanEscrow = 0x00000008,
lsfMPTCanMutateCanTrade = 0x00000010,
lsfMPTCanMutateCanTransfer = 0x00000020,
lsfMPTCanMutateCanClawback = 0x00000040,
lsfMPTCanMutateMetadata = 0x00010000,
lsfMPTCanMutateTransferFee = 0x00020000,
// ltMPTOKEN
lsfMPTAuthorized = 0x00000002,

View File

@@ -151,6 +151,20 @@ constexpr std::uint32_t const tfMPTCanClawback = lsfMPTCanClawback;
constexpr std::uint32_t const tfMPTokenIssuanceCreateMask =
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback);
// MPTokenIssuanceCreate MutableFlags:
// Indicating specific fields or flags may be changed after issuance.
constexpr std::uint32_t const tfMPTCanMutateCanLock = lsfMPTCanMutateCanLock;
constexpr std::uint32_t const tfMPTCanMutateRequireAuth = lsfMPTCanMutateRequireAuth;
constexpr std::uint32_t const tfMPTCanMutateCanEscrow = lsfMPTCanMutateCanEscrow;
constexpr std::uint32_t const tfMPTCanMutateCanTrade = lsfMPTCanMutateCanTrade;
constexpr std::uint32_t const tfMPTCanMutateCanTransfer = lsfMPTCanMutateCanTransfer;
constexpr std::uint32_t const tfMPTCanMutateCanClawback = lsfMPTCanMutateCanClawback;
constexpr std::uint32_t const tfMPTCanMutateMetadata = lsfMPTCanMutateMetadata;
constexpr std::uint32_t const tfMPTCanMutateTransferFee = lsfMPTCanMutateTransferFee;
constexpr std::uint32_t const tfMPTokenIssuanceCreateMutableMask =
~(tfMPTCanMutateCanLock | tfMPTCanMutateRequireAuth | tfMPTCanMutateCanEscrow | tfMPTCanMutateCanTrade
| tfMPTCanMutateCanTransfer | tfMPTCanMutateCanClawback | tfMPTCanMutateMetadata | tfMPTCanMutateTransferFee);
// MPTokenAuthorize flags:
constexpr std::uint32_t const tfMPTUnauthorize = 0x00000001;
constexpr std::uint32_t const tfMPTokenAuthorizeMask = ~(tfUniversal | tfMPTUnauthorize);
@@ -161,6 +175,25 @@ constexpr std::uint32_t const tfMPTUnlock = 0x00000002;
constexpr std::uint32_t const tfMPTokenIssuanceSetMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock);
constexpr std::uint32_t const tfMPTokenIssuanceSetPermissionMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock);
// MPTokenIssuanceSet MutableFlags:
// Set or Clear flags.
constexpr std::uint32_t const tfMPTSetCanLock = 0x00000001;
constexpr std::uint32_t const tfMPTClearCanLock = 0x00000002;
constexpr std::uint32_t const tfMPTSetRequireAuth = 0x00000004;
constexpr std::uint32_t const tfMPTClearRequireAuth = 0x00000008;
constexpr std::uint32_t const tfMPTSetCanEscrow = 0x00000010;
constexpr std::uint32_t const tfMPTClearCanEscrow = 0x00000020;
constexpr std::uint32_t const tfMPTSetCanTrade = 0x00000040;
constexpr std::uint32_t const tfMPTClearCanTrade = 0x00000080;
constexpr std::uint32_t const tfMPTSetCanTransfer = 0x00000100;
constexpr std::uint32_t const tfMPTClearCanTransfer = 0x00000200;
constexpr std::uint32_t const tfMPTSetCanClawback = 0x00000400;
constexpr std::uint32_t const tfMPTClearCanClawback = 0x00000800;
constexpr std::uint32_t const tfMPTokenIssuanceSetMutableMask = ~(tfMPTSetCanLock | tfMPTClearCanLock |
tfMPTSetRequireAuth | tfMPTClearRequireAuth | tfMPTSetCanEscrow | tfMPTClearCanEscrow |
tfMPTSetCanTrade | tfMPTClearCanTrade | tfMPTSetCanTransfer | tfMPTClearCanTransfer |
tfMPTSetCanClawback | tfMPTClearCanClawback);
// MPTokenIssuanceDestroy flags:
constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal;

View File

@@ -32,6 +32,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(DynamicMPT, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (TokenEscrowV1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (DelegateV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo)

View File

@@ -412,6 +412,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfDomainID, soeOPTIONAL},
{sfMutableFlags, soeDEFAULT},
}))
/** A ledger object which tracks MPToken

View File

@@ -114,6 +114,7 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48)
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
TYPED_SFIELD(sfMutableFlags, UINT32, 53)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)

View File

@@ -548,6 +548,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate,
{sfMaximumAmount, soeOPTIONAL},
{sfMPTokenMetadata, soeOPTIONAL},
{sfDomainID, soeOPTIONAL},
{sfMutableFlags, soeOPTIONAL},
}))
/** This transaction type destroys a MPTokensIssuance instance */
@@ -566,6 +567,9 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
{sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeOPTIONAL},
{sfDomainID, soeOPTIONAL},
{sfMPTokenMetadata, soeOPTIONAL},
{sfTransferFee, soeOPTIONAL},
{sfMutableFlags, soeOPTIONAL},
}))
/** This transaction type authorizes a MPToken instance */

View File

@@ -589,7 +589,8 @@ class MPToken_test : public beast::unit_test::suite
.flags = 0x00000008,
.err = temINVALID_FLAG});
if (!features[featureSingleAssetVault])
if (!features[featureSingleAssetVault] &&
!features[featureDynamicMPT])
{
// test invalid flags - nothing is being changed
mptAlice.set(
@@ -623,7 +624,8 @@ class MPToken_test : public beast::unit_test::suite
.flags = 0x00000000,
.err = temMALFORMED});
if (!features[featurePermissionedDomains])
if (!features[featurePermissionedDomains] ||
!features[featureSingleAssetVault])
{
// cannot set DomainID since PD is not enabled
mptAlice.set(
@@ -631,7 +633,7 @@ class MPToken_test : public beast::unit_test::suite
.domainID = uint256(42),
.err = temDISABLED});
}
else
else if (features[featureSingleAssetVault])
{
// cannot set DomainID since Holder is set
mptAlice.set(
@@ -2738,6 +2740,882 @@ class MPToken_test : public beast::unit_test::suite
}
}
void
testInvalidCreateDynamic(FeatureBitset features)
{
testcase("invalid MPTokenIssuanceCreate for DynamicMPT");
using namespace test::jtx;
Account const alice("alice");
// Can not provide MutableFlags when DynamicMPT amendment is not enabled
{
Env env{*this, features - featureDynamicMPT};
MPTTester mptAlice(env, alice);
mptAlice.create(
{.ownerCount = 0, .mutableFlags = 2, .err = temDISABLED});
mptAlice.create(
{.ownerCount = 0, .mutableFlags = 0, .err = temDISABLED});
}
// MutableFlags contains invalid values
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
// Value 1 is reserved for MPT lock.
mptAlice.create(
{.ownerCount = 0, .mutableFlags = 1, .err = temINVALID_FLAG});
mptAlice.create(
{.ownerCount = 0, .mutableFlags = 17, .err = temINVALID_FLAG});
mptAlice.create(
{.ownerCount = 0,
.mutableFlags = 65535,
.err = temINVALID_FLAG});
// MutableFlags can not be 0
mptAlice.create(
{.ownerCount = 0, .mutableFlags = 0, .err = temINVALID_FLAG});
}
}
void
testInvalidSetDynamic(FeatureBitset features)
{
testcase("invalid MPTokenIssuanceSet for DynamicMPT");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
// Can not provide MutableFlags, MPTokenMetadata or TransferFee when
// DynamicMPT amendment is not enabled
{
Env env{*this, features - featureDynamicMPT};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
// MutableFlags is not allowed when DynamicMPT is not enabled
mptAlice.set(
{.account = alice,
.id = mptID,
.mutableFlags = 2,
.err = temDISABLED});
mptAlice.set(
{.account = alice,
.id = mptID,
.mutableFlags = 0,
.err = temDISABLED});
// MPTokenMetadata is not allowed when DynamicMPT is not enabled
mptAlice.set(
{.account = alice,
.id = mptID,
.metadata = "test",
.err = temDISABLED});
mptAlice.set(
{.account = alice,
.id = mptID,
.metadata = "",
.err = temDISABLED});
// TransferFee is not allowed when DynamicMPT is not enabled
mptAlice.set(
{.account = alice,
.id = mptID,
.transferFee = 100,
.err = temDISABLED});
mptAlice.set(
{.account = alice,
.id = mptID,
.transferFee = 0,
.err = temDISABLED});
}
// Can not provide holder when MutableFlags, MPTokenMetadata or
// TransferFee is present
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
// Holder is not allowed when MutableFlags is present
mptAlice.set(
{.account = alice,
.holder = bob,
.id = mptID,
.mutableFlags = 2,
.err = temMALFORMED});
// Holder is not allowed when MPTokenMetadata is present
mptAlice.set(
{.account = alice,
.holder = bob,
.id = mptID,
.metadata = "test",
.err = temMALFORMED});
// Holder is not allowed when TransferFee is present
mptAlice.set(
{.account = alice,
.holder = bob,
.id = mptID,
.transferFee = 100,
.err = temMALFORMED});
}
// Can not set Flags when MutableFlags, MPTokenMetadata or
// TransferFee is present
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.mutableFlags = tfMPTCanMutateMetadata |
tfMPTCanMutateCanLock | tfMPTCanMutateTransferFee});
// Setting flags is not allowed when MutableFlags is present
mptAlice.set(
{.account = alice,
.flags = tfMPTCanLock,
.mutableFlags = 2,
.err = temMALFORMED});
// Setting flags is not allowed when MPTokenMetadata is present
mptAlice.set(
{.account = alice,
.flags = tfMPTCanLock,
.metadata = "test",
.err = temMALFORMED});
// setting flags is not allowed when TransferFee is present
mptAlice.set(
{.account = alice,
.flags = tfMPTCanLock,
.transferFee = 100,
.err = temMALFORMED});
}
// Flags being 0 or tfFullyCanonicalSig is fine
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.transferFee = 10,
.ownerCount = 1,
.flags = tfMPTCanTransfer,
.mutableFlags =
tfMPTCanMutateTransferFee | tfMPTCanMutateMetadata});
mptAlice.set(
{.account = alice,
.flags = 0,
.transferFee = 100,
.metadata = "test"});
mptAlice.set(
{.account = alice,
.flags = tfFullyCanonicalSig,
.transferFee = 200,
.metadata = "test2"});
}
// Invalid MutableFlags
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
for (auto const flags : {10000, 0, 5000})
{
mptAlice.set(
{.account = alice,
.id = mptID,
.mutableFlags = flags,
.err = temINVALID_FLAG});
}
}
// Can not set and clear the same mutable flag
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
auto const flagCombinations = {
tfMPTSetCanLock | tfMPTClearCanLock,
tfMPTSetRequireAuth | tfMPTClearRequireAuth,
tfMPTSetCanEscrow | tfMPTClearCanEscrow,
tfMPTSetCanTrade | tfMPTClearCanTrade,
tfMPTSetCanTransfer | tfMPTClearCanTransfer,
tfMPTSetCanClawback | tfMPTClearCanClawback,
tfMPTSetCanLock | tfMPTClearCanLock | tfMPTClearCanTrade,
tfMPTSetCanTransfer | tfMPTClearCanTransfer |
tfMPTSetCanEscrow | tfMPTClearCanClawback};
for (auto const& mutableFlags : flagCombinations)
{
mptAlice.set(
{.account = alice,
.id = mptID,
.mutableFlags = mutableFlags,
.err = temINVALID_FLAG});
}
}
// Can not mutate flag which is not mutable
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1});
auto const mutableFlags = {
tfMPTSetCanLock,
tfMPTClearCanLock,
tfMPTSetRequireAuth,
tfMPTClearRequireAuth,
tfMPTSetCanEscrow,
tfMPTClearCanEscrow,
tfMPTSetCanTrade,
tfMPTClearCanTrade,
tfMPTSetCanTransfer,
tfMPTClearCanTransfer,
tfMPTSetCanClawback,
tfMPTClearCanClawback};
for (auto const& mutableFlag : mutableFlags)
{
mptAlice.set(
{.account = alice,
.mutableFlags = mutableFlag,
.err = tecNO_PERMISSION});
}
}
// Metadata exceeding max length
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1, .mutableFlags = tfMPTCanMutateMetadata});
std::string metadata(maxMPTokenMetadataLength + 1, 'a');
mptAlice.set(
{.account = alice, .metadata = metadata, .err = temMALFORMED});
}
// Can not mutate metadata when it is not mutable
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1});
mptAlice.set(
{.account = alice,
.metadata = "test",
.err = tecNO_PERMISSION});
}
// Transfer fee exceeding the max value
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
mptAlice.create(
{.ownerCount = 1, .mutableFlags = tfMPTCanMutateTransferFee});
mptAlice.set(
{.account = alice,
.id = mptID,
.transferFee = maxTransferFee + 1,
.err = temBAD_TRANSFER_FEE});
}
// Test setting non-zero transfer fee and clearing MPTCanTransfer at the
// same time
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.transferFee = 100,
.ownerCount = 1,
.flags = tfMPTCanTransfer,
.mutableFlags =
tfMPTCanMutateTransferFee | tfMPTCanMutateCanTransfer});
// Can not set non-zero transfer fee and clear MPTCanTransfer at the
// same time
mptAlice.set(
{.account = alice,
.mutableFlags = tfMPTClearCanTransfer,
.transferFee = 1,
.err = temMALFORMED});
// Can set transfer fee to zero and clear MPTCanTransfer at the same
// time. tfMPTCanTransfer will be cleared and TransferFee field will
// be removed.
mptAlice.set(
{.account = alice,
.mutableFlags = tfMPTClearCanTransfer,
.transferFee = 0});
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
}
// Can not set non-zero transfer fee when MPTCanTransfer is not set
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.mutableFlags =
tfMPTCanMutateTransferFee | tfMPTCanMutateCanTransfer});
mptAlice.set(
{.account = alice,
.transferFee = 100,
.err = tecNO_PERMISSION});
// Can not set transfer fee even when trying to set MPTCanTransfer
// at the same time. MPTCanTransfer must be set first, then transfer
// fee can be set in a separate transaction.
mptAlice.set(
{.account = alice,
.mutableFlags = tfMPTSetCanTransfer,
.transferFee = 100,
.err = tecNO_PERMISSION});
}
// Can not mutate transfer fee when it is not mutable
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.transferFee = 10,
.ownerCount = 1,
.flags = tfMPTCanTransfer});
mptAlice.set(
{.account = alice,
.transferFee = 100,
.err = tecNO_PERMISSION});
mptAlice.set(
{.account = alice, .transferFee = 0, .err = tecNO_PERMISSION});
}
// Set some flags mutable. Can not mutate the others
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.mutableFlags = tfMPTCanMutateCanTrade |
tfMPTCanMutateCanTransfer | tfMPTCanMutateMetadata});
// Can not mutate transfer fee
mptAlice.set(
{.account = alice,
.transferFee = 100,
.err = tecNO_PERMISSION});
auto const invalidFlags = {
tfMPTSetCanLock,
tfMPTClearCanLock,
tfMPTSetRequireAuth,
tfMPTClearRequireAuth,
tfMPTSetCanEscrow,
tfMPTClearCanEscrow,
tfMPTSetCanClawback,
tfMPTClearCanClawback};
// Can not mutate flags which are not mutable
for (auto const& mutableFlag : invalidFlags)
{
mptAlice.set(
{.account = alice,
.mutableFlags = mutableFlag,
.err = tecNO_PERMISSION});
}
// Can mutate MPTCanTrade
mptAlice.set({.account = alice, .mutableFlags = tfMPTSetCanTrade});
mptAlice.set(
{.account = alice, .mutableFlags = tfMPTClearCanTrade});
// Can mutate MPTCanTransfer
mptAlice.set(
{.account = alice, .mutableFlags = tfMPTSetCanTransfer});
mptAlice.set(
{.account = alice, .mutableFlags = tfMPTClearCanTransfer});
// Can mutate metadata
mptAlice.set({.account = alice, .metadata = "test"});
mptAlice.set({.account = alice, .metadata = ""});
}
}
void
testMutateMPT(FeatureBitset features)
{
testcase("Mutate MPT");
using namespace test::jtx;
Account const alice("alice");
// Mutate metadata
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create(
{.metadata = "test",
.ownerCount = 1,
.mutableFlags = tfMPTCanMutateMetadata});
std::vector<std::string> metadatas = {
"mutate metadata",
"mutate metadata 2",
"mutate metadata 3",
"mutate metadata 3",
"test",
"mutate metadata"};
for (auto const& metadata : metadatas)
{
mptAlice.set({.account = alice, .metadata = metadata});
BEAST_EXPECT(mptAlice.checkMetadata(metadata));
}
// Metadata being empty will remove the field
mptAlice.set({.account = alice, .metadata = ""});
BEAST_EXPECT(!mptAlice.isMetadataPresent());
}
// Mutate transfer fee
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create(
{.transferFee = 100,
.metadata = "test",
.ownerCount = 1,
.flags = tfMPTCanTransfer,
.mutableFlags = tfMPTCanMutateTransferFee});
for (std::uint16_t const fee : std::initializer_list<std::uint16_t>{
1, 10, 100, 200, 500, 1000, maxTransferFee})
{
mptAlice.set({.account = alice, .transferFee = fee});
BEAST_EXPECT(mptAlice.checkTransferFee(fee));
}
// Setting TransferFee to zero will remove the field
mptAlice.set({.account = alice, .transferFee = 0});
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
// Set transfer fee again
mptAlice.set({.account = alice, .transferFee = 10});
BEAST_EXPECT(mptAlice.checkTransferFee(10));
}
// Test flag toggling
{
auto testFlagToggle = [&](std::uint32_t createFlags,
std::uint32_t setFlags,
std::uint32_t clearFlags) {
Env env{*this, features};
MPTTester mptAlice(env, alice);
// Create the MPT object with the specified initial flags
mptAlice.create(
{.metadata = "test",
.ownerCount = 1,
.mutableFlags = createFlags});
// Set and clear the flag multiple times
mptAlice.set({.account = alice, .mutableFlags = setFlags});
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
mptAlice.set({.account = alice, .mutableFlags = setFlags});
mptAlice.set({.account = alice, .mutableFlags = setFlags});
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
mptAlice.set({.account = alice, .mutableFlags = setFlags});
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
};
testFlagToggle(
tfMPTCanMutateCanLock, tfMPTCanLock, tfMPTClearCanLock);
testFlagToggle(
tfMPTCanMutateRequireAuth,
tfMPTSetRequireAuth,
tfMPTClearRequireAuth);
testFlagToggle(
tfMPTCanMutateCanEscrow,
tfMPTSetCanEscrow,
tfMPTClearCanEscrow);
testFlagToggle(
tfMPTCanMutateCanTrade, tfMPTSetCanTrade, tfMPTClearCanTrade);
testFlagToggle(
tfMPTCanMutateCanTransfer,
tfMPTSetCanTransfer,
tfMPTClearCanTransfer);
testFlagToggle(
tfMPTCanMutateCanClawback,
tfMPTSetCanClawback,
tfMPTClearCanClawback);
}
}
void
testMutateCanLock(FeatureBitset features)
{
testcase("Mutate MPTCanLock");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
// Individual lock
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanLock | tfMPTCanTransfer,
.mutableFlags = tfMPTCanMutateCanLock |
tfMPTCanMutateCanTrade | tfMPTCanMutateTransferFee});
mptAlice.authorize({.account = bob, .holderCount = 1});
// Lock bob's mptoken
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// Can mutate the mutable flags and fields
mptAlice.set({.account = alice, .mutableFlags = tfMPTClearCanLock});
mptAlice.set({.account = alice, .mutableFlags = tfMPTSetCanLock});
mptAlice.set({.account = alice, .mutableFlags = tfMPTClearCanLock});
mptAlice.set({.account = alice, .mutableFlags = tfMPTSetCanTrade});
mptAlice.set(
{.account = alice, .mutableFlags = tfMPTClearCanTrade});
mptAlice.set({.account = alice, .transferFee = 200});
}
// Global lock
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanLock,
.mutableFlags = tfMPTCanMutateCanLock |
tfMPTCanMutateCanClawback | tfMPTCanMutateMetadata});
mptAlice.authorize({.account = bob, .holderCount = 1});
// Lock issuance
mptAlice.set({.account = alice, .flags = tfMPTLock});
// Can mutate the mutable flags and fields
mptAlice.set({.account = alice, .mutableFlags = tfMPTClearCanLock});
mptAlice.set({.account = alice, .mutableFlags = tfMPTSetCanLock});
mptAlice.set({.account = alice, .mutableFlags = tfMPTClearCanLock});
mptAlice.set(
{.account = alice, .mutableFlags = tfMPTSetCanClawback});
mptAlice.set(
{.account = alice, .mutableFlags = tfMPTClearCanClawback});
mptAlice.set({.account = alice, .metadata = "mutate"});
}
// Test lock and unlock after mutating MPTCanLock
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanLock,
.mutableFlags = tfMPTCanMutateCanLock |
tfMPTCanMutateCanClawback | tfMPTCanMutateMetadata});
mptAlice.authorize({.account = bob, .holderCount = 1});
// Can lock and unlock
mptAlice.set({.account = alice, .flags = tfMPTLock});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
mptAlice.set(
{.account = alice, .holder = bob, .flags = tfMPTUnlock});
// Clear lsfMPTCanLock
mptAlice.set({.account = alice, .mutableFlags = tfMPTClearCanLock});
// Can not lock or unlock
mptAlice.set(
{.account = alice,
.flags = tfMPTLock,
.err = tecNO_PERMISSION});
mptAlice.set(
{.account = alice,
.flags = tfMPTUnlock,
.err = tecNO_PERMISSION});
mptAlice.set(
{.account = alice,
.holder = bob,
.flags = tfMPTLock,
.err = tecNO_PERMISSION});
mptAlice.set(
{.account = alice,
.holder = bob,
.flags = tfMPTUnlock,
.err = tecNO_PERMISSION});
// Set MPTCanLock again
mptAlice.set({.account = alice, .mutableFlags = tfMPTSetCanLock});
// Can lock and unlock again
mptAlice.set({.account = alice, .flags = tfMPTLock});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
mptAlice.set(
{.account = alice, .holder = bob, .flags = tfMPTUnlock});
}
}
void
testMutateRequireAuth(FeatureBitset features)
{
testcase("Mutate MPTRequireAuth");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.flags = tfMPTRequireAuth,
.mutableFlags = tfMPTCanMutateRequireAuth});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = alice, .holder = bob});
// Pay to bob
mptAlice.pay(alice, bob, 1000);
// Unauthorize bob
mptAlice.authorize(
{.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
// Can not pay to bob
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
// Clear RequireAuth
mptAlice.set({.account = alice, .mutableFlags = tfMPTClearRequireAuth});
// Can pay to bob
mptAlice.pay(alice, bob, 1000);
// Set RequireAuth again
mptAlice.set({.account = alice, .mutableFlags = tfMPTSetRequireAuth});
// Can not pay to bob since he is not authorized
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
// Authorize bob again
mptAlice.authorize({.account = alice, .holder = bob});
// Can pay to bob again
mptAlice.pay(alice, bob, 100);
}
void
testMutateCanEscrow(FeatureBitset features)
{
testcase("Mutate MPTCanEscrow");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
MPTTester mptAlice(env, alice, {.holders = {carol, bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer,
.mutableFlags = tfMPTCanMutateCanEscrow});
mptAlice.authorize({.account = carol});
mptAlice.authorize({.account = bob});
auto const MPT = mptAlice["MPT"];
env(pay(alice, carol, MPT(10'000)));
env(pay(alice, bob, MPT(10'000)));
env.close();
// MPTCanEscrow is not enabled
env(escrow::create(carol, bob, MPT(3)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
fee(baseFee * 150),
ter(tecNO_PERMISSION));
// MPTCanEscrow is enabled now
mptAlice.set({.account = alice, .mutableFlags = tfMPTSetCanEscrow});
env(escrow::create(carol, bob, MPT(3)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
fee(baseFee * 150));
// Clear MPTCanEscrow
mptAlice.set({.account = alice, .mutableFlags = tfMPTClearCanEscrow});
env(escrow::create(carol, bob, MPT(3)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
fee(baseFee * 150),
ter(tecNO_PERMISSION));
}
void
testMutateCanTransfer(FeatureBitset features)
{
testcase("Mutate MPTCanTransfer");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.ownerCount = 1,
.mutableFlags =
tfMPTCanMutateCanTransfer | tfMPTCanMutateTransferFee});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
// Pay to bob
mptAlice.pay(alice, bob, 1000);
// Bob can not pay carol since MPTCanTransfer is not set
mptAlice.pay(bob, carol, 50, tecNO_AUTH);
// Can not set non-zero transfer fee when MPTCanTransfer is not set
mptAlice.set(
{.account = alice,
.transferFee = 100,
.err = tecNO_PERMISSION});
// Can not set non-zero transfer fee even when trying to set
// MPTCanTransfer at the same time
mptAlice.set(
{.account = alice,
.mutableFlags = tfMPTSetCanTransfer,
.transferFee = 100,
.err = tecNO_PERMISSION});
// Alice sets MPTCanTransfer
mptAlice.set(
{.account = alice, .mutableFlags = tfMPTSetCanTransfer});
// Can set transfer fee now
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
mptAlice.set({.account = alice, .transferFee = 100});
BEAST_EXPECT(mptAlice.isTransferFeePresent());
// Bob can pay carol
mptAlice.pay(bob, carol, 50);
// Alice clears MPTCanTransfer
mptAlice.set(
{.account = alice, .mutableFlags = tfMPTClearCanTransfer});
// TransferFee field is removed when MPTCanTransfer is cleared
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
// Bob can not pay
mptAlice.pay(bob, carol, 50, tecNO_AUTH);
}
// Can set transfer fee to zero when MPTCanTransfer is not set, but
// tfMPTCanMutateTransferFee is set.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.transferFee = 100,
.ownerCount = 1,
.flags = tfMPTCanTransfer,
.mutableFlags =
tfMPTCanMutateTransferFee | tfMPTCanMutateCanTransfer});
BEAST_EXPECT(mptAlice.checkTransferFee(100));
// Clear MPTCanTransfer and transfer fee is removed
mptAlice.set(
{.account = alice, .mutableFlags = tfMPTClearCanTransfer});
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
// Can still set transfer fee to zero, although it is already zero
mptAlice.set({.account = alice, .transferFee = 0});
// TransferFee field is still not present
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
}
}
void
testMutateCanClawback(FeatureBitset features)
{
testcase("Mutate MPTCanClawback");
using namespace test::jtx;
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.mutableFlags = tfMPTCanMutateCanClawback});
// Bob creates an MPToken
mptAlice.authorize({.account = bob});
// Alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
// MPTCanClawback is not enabled
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
// Enable MPTCanClawback
mptAlice.set({.account = alice, .mutableFlags = tfMPTSetCanClawback});
// Can clawback now
mptAlice.claw(alice, bob, 1);
// Clear MPTCanClawback
mptAlice.set({.account = alice, .mutableFlags = tfMPTClearCanClawback});
// Can not clawback
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
}
public:
void
run() override
@@ -2747,39 +3625,39 @@ public:
// MPTokenIssuanceCreate
testCreateValidation(all - featureSingleAssetVault);
testCreateValidation(
(all | featureSingleAssetVault) - featurePermissionedDomains);
testCreateValidation(all | featureSingleAssetVault);
testCreateValidation(all - featurePermissionedDomains);
testCreateValidation(all);
testCreateEnabled(all - featureSingleAssetVault);
testCreateEnabled(all | featureSingleAssetVault);
testCreateEnabled(all);
// MPTokenIssuanceDestroy
testDestroyValidation(all - featureSingleAssetVault);
testDestroyValidation(all | featureSingleAssetVault);
testDestroyValidation(all);
testDestroyEnabled(all - featureSingleAssetVault);
testDestroyEnabled(all | featureSingleAssetVault);
testDestroyEnabled(all);
// MPTokenAuthorize
testAuthorizeValidation(all - featureSingleAssetVault);
testAuthorizeValidation(all | featureSingleAssetVault);
testAuthorizeValidation(all);
testAuthorizeEnabled(all - featureSingleAssetVault);
testAuthorizeEnabled(all | featureSingleAssetVault);
testAuthorizeEnabled(all);
// MPTokenIssuanceSet
testSetValidation(all - featureSingleAssetVault - featureDynamicMPT);
testSetValidation(all - featureSingleAssetVault);
testSetValidation(
(all | featureSingleAssetVault) - featurePermissionedDomains);
testSetValidation(all | featureSingleAssetVault);
testSetValidation(all - featureDynamicMPT);
testSetValidation(all - featurePermissionedDomains);
testSetValidation(all);
testSetEnabled(all - featureSingleAssetVault);
testSetEnabled(all | featureSingleAssetVault);
testSetEnabled(all);
// MPT clawback
testClawbackValidation(all);
testClawback(all);
// Test Direct Payment
testPayment(all | featureSingleAssetVault);
testPayment(all);
testDepositPreauth(all);
testDepositPreauth(all - featureCredentials);
@@ -2794,6 +3672,16 @@ public:
// Test helpers
testHelperFunctions();
// Dynamic MPT
testInvalidCreateDynamic(all);
testInvalidSetDynamic(all);
testMutateMPT(all);
testMutateCanLock(all);
testMutateRequireAuth(all);
testMutateCanEscrow(all);
testMutateCanTransfer(all);
testMutateCanClawback(all);
}
};

View File

@@ -102,6 +102,8 @@ MPTTester::create(MPTCreate const& arg)
jv[sfMaximumAmount] = std::to_string(*arg.maxAmt);
if (arg.domainID)
jv[sfDomainID] = to_string(*arg.domainID);
if (arg.mutableFlags)
jv[sfMutableFlags] = *arg.mutableFlags;
if (submit(arg, jv) != tesSUCCESS)
{
// Verify issuance doesn't exist
@@ -240,19 +242,59 @@ MPTTester::set(MPTSet const& arg)
jv[sfDelegate] = arg.delegate->human();
if (arg.domainID)
jv[sfDomainID] = to_string(*arg.domainID);
if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0))
if (arg.mutableFlags)
jv[sfMutableFlags] = *arg.mutableFlags;
if (arg.transferFee)
jv[sfTransferFee] = *arg.transferFee;
if (arg.metadata)
jv[sfMPTokenMetadata] = strHex(*arg.metadata);
if (submit(arg, jv) == tesSUCCESS && (arg.flags || arg.mutableFlags))
{
auto require = [&](std::optional<Account> const& holder,
bool unchanged) {
auto flags = getFlags(holder);
if (!unchanged)
{
if (*arg.flags & tfMPTLock)
flags |= lsfMPTLocked;
else if (*arg.flags & tfMPTUnlock)
flags &= ~lsfMPTLocked;
else
Throw<std::runtime_error>("Invalid flags");
if (arg.flags)
{
if (*arg.flags & tfMPTLock)
flags |= lsfMPTLocked;
else if (*arg.flags & tfMPTUnlock)
flags &= ~lsfMPTLocked;
}
if (arg.mutableFlags)
{
if (*arg.mutableFlags & tfMPTSetCanLock)
flags |= lsfMPTCanLock;
else if (*arg.mutableFlags & tfMPTClearCanLock)
flags &= ~lsfMPTCanLock;
if (*arg.mutableFlags & tfMPTSetRequireAuth)
flags |= lsfMPTRequireAuth;
else if (*arg.mutableFlags & tfMPTClearRequireAuth)
flags &= ~lsfMPTRequireAuth;
if (*arg.mutableFlags & tfMPTSetCanEscrow)
flags |= lsfMPTCanEscrow;
else if (*arg.mutableFlags & tfMPTClearCanEscrow)
flags &= ~lsfMPTCanEscrow;
if (*arg.mutableFlags & tfMPTSetCanClawback)
flags |= lsfMPTCanClawback;
else if (*arg.mutableFlags & tfMPTClearCanClawback)
flags &= ~lsfMPTCanClawback;
if (*arg.mutableFlags & tfMPTSetCanTrade)
flags |= lsfMPTCanTrade;
else if (*arg.mutableFlags & tfMPTClearCanTrade)
flags &= ~lsfMPTCanTrade;
if (*arg.mutableFlags & tfMPTSetCanTransfer)
flags |= lsfMPTCanTransfer;
else if (*arg.mutableFlags & tfMPTClearCanTransfer)
flags &= ~lsfMPTCanTransfer;
}
}
env_.require(mptflags(*this, flags, holder));
};
@@ -313,6 +355,43 @@ MPTTester::checkFlags(
return expectedFlags == getFlags(holder);
}
[[nodiscard]] bool
MPTTester::checkMetadata(std::string const& metadata) const
{
return forObject([&](SLEP const& sle) -> bool {
if (sle->isFieldPresent(sfMPTokenMetadata))
return strHex(sle->getFieldVL(sfMPTokenMetadata)) ==
strHex(metadata);
return false;
});
}
[[nodiscard]] bool
MPTTester::isMetadataPresent() const
{
return forObject([&](SLEP const& sle) -> bool {
return sle->isFieldPresent(sfMPTokenMetadata);
});
}
[[nodiscard]] bool
MPTTester::checkTransferFee(std::uint16_t transferFee) const
{
return forObject([&](SLEP const& sle) -> bool {
if (sle->isFieldPresent(sfTransferFee))
return sle->getFieldU16(sfTransferFee) == transferFee;
return false;
});
}
[[nodiscard]] bool
MPTTester::isTransferFeePresent() const
{
return forObject([&](SLEP const& sle) -> bool {
return sle->isFieldPresent(sfTransferFee);
});
}
void
MPTTester::pay(
Account const& src,

View File

@@ -106,6 +106,7 @@ struct MPTCreate
std::optional<std::uint32_t> holderCount = std::nullopt;
bool fund = true;
std::optional<std::uint32_t> flags = {0};
std::optional<std::uint32_t> mutableFlags = std::nullopt;
std::optional<uint256> domainID = std::nullopt;
std::optional<TER> err = std::nullopt;
};
@@ -139,6 +140,9 @@ struct MPTSet
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<std::uint32_t> mutableFlags = std::nullopt;
std::optional<std::uint16_t> transferFee = std::nullopt;
std::optional<std::string> metadata = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<uint256> domainID = std::nullopt;
std::optional<TER> err = std::nullopt;
@@ -182,6 +186,18 @@ public:
uint32_t const expectedFlags,
std::optional<Account> const& holder = std::nullopt) const;
[[nodiscard]] bool
checkMetadata(std::string const& metadata) const;
[[nodiscard]] bool
isMetadataPresent() const;
[[nodiscard]] bool
checkTransferFee(std::uint16_t transferFee) const;
[[nodiscard]] bool
isTransferFeePresent() const;
Account const&
issuer() const
{

View File

@@ -36,9 +36,17 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
ctx.rules.enabled(featureSingleAssetVault)))
return temDISABLED;
if (ctx.tx.isFieldPresent(sfMutableFlags) &&
!ctx.rules.enabled(featureDynamicMPT))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (auto const mutableFlags = ctx.tx[~sfMutableFlags]; mutableFlags &&
(!*mutableFlags || *mutableFlags & tfMPTokenIssuanceCreateMutableMask))
return temINVALID_FLAG;
if (ctx.tx.getFlags() & tfMPTokenIssuanceCreateMask)
return temINVALID_FLAG;
@@ -132,6 +140,9 @@ MPTokenIssuanceCreate::create(
if (args.domainId)
(*mptIssuance)[sfDomainID] = *args.domainId;
if (args.mutableFlags)
(*mptIssuance)[sfMutableFlags] = *args.mutableFlags;
view.insert(mptIssuance);
}
@@ -158,6 +169,7 @@ MPTokenIssuanceCreate::doApply()
.transferFee = tx[~sfTransferFee],
.metadata = tx[~sfMPTokenMetadata],
.domainId = tx[~sfDomainID],
.mutableFlags = tx[~sfMutableFlags],
});
return result ? tesSUCCESS : result.error();
}

View File

@@ -38,6 +38,7 @@ struct MPTCreateArgs
std::optional<std::uint16_t> transferFee{};
std::optional<Slice> const& metadata{};
std::optional<uint256> domainId{};
std::optional<std::uint32_t> mutableFlags{};
};
class MPTokenIssuanceCreate : public Transactor

View File

@@ -26,6 +26,24 @@
namespace ripple {
// Maps set/clear mutable flags in an MPTokenIssuanceSet transaction to the
// corresponding ledger mutable flags that control whether the change is
// allowed.
struct MPTMutabilityFlags
{
std::uint32_t setFlag;
std::uint32_t clearFlag;
std::uint32_t canMutateFlag;
};
static constexpr std::array<MPTMutabilityFlags, 6> mptMutabilityFlags = {
{{tfMPTSetCanLock, tfMPTClearCanLock, lsfMPTCanMutateCanLock},
{tfMPTSetRequireAuth, tfMPTClearRequireAuth, lsfMPTCanMutateRequireAuth},
{tfMPTSetCanEscrow, tfMPTClearCanEscrow, lsfMPTCanMutateCanEscrow},
{tfMPTSetCanTrade, tfMPTClearCanTrade, lsfMPTCanMutateCanTrade},
{tfMPTSetCanTransfer, tfMPTClearCanTransfer, lsfMPTCanMutateCanTransfer},
{tfMPTSetCanClawback, tfMPTClearCanClawback, lsfMPTCanMutateCanClawback}}};
NotTEC
MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
{
@@ -37,6 +55,14 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
ctx.rules.enabled(featureSingleAssetVault)))
return temDISABLED;
auto const mutableFlags = ctx.tx[~sfMutableFlags];
auto const metadata = ctx.tx[~sfMPTokenMetadata];
auto const transferFee = ctx.tx[~sfTransferFee];
auto const isMutate = mutableFlags || metadata || transferFee;
if (isMutate && !ctx.rules.enabled(featureDynamicMPT))
return temDISABLED;
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
return temMALFORMED;
@@ -57,13 +83,54 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (holderID && accountID == holderID)
return temMALFORMED;
if (ctx.rules.enabled(featureSingleAssetVault))
if (ctx.rules.enabled(featureSingleAssetVault) ||
ctx.rules.enabled(featureDynamicMPT))
{
// Is this transaction actually changing anything ?
if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID))
if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate)
return temMALFORMED;
}
if (ctx.rules.enabled(featureDynamicMPT))
{
// Holder field is not allowed when mutating MPTokenIssuance
if (isMutate && holderID)
return temMALFORMED;
// Can not set flags when mutating MPTokenIssuance
if (isMutate && (txFlags & tfUniversalMask))
return temMALFORMED;
if (transferFee && *transferFee > maxTransferFee)
return temBAD_TRANSFER_FEE;
if (metadata && metadata->length() > maxMPTokenMetadataLength)
return temMALFORMED;
if (mutableFlags)
{
if (!*mutableFlags ||
(*mutableFlags & tfMPTokenIssuanceSetMutableMask))
return temINVALID_FLAG;
// Can not set and clear the same flag
if (std::any_of(
mptMutabilityFlags.begin(),
mptMutabilityFlags.end(),
[mutableFlags](auto const& f) {
return (*mutableFlags & f.setFlag) &&
(*mutableFlags & f.clearFlag);
}))
return temINVALID_FLAG;
// Trying to set a non-zero TransferFee and clear MPTCanTransfer
// in the same transaction is not allowed.
if (transferFee.value_or(0) &&
(*mutableFlags & tfMPTClearCanTransfer))
return temMALFORMED;
}
}
return preflight2(ctx);
}
@@ -116,7 +183,8 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
if (!sleMptIssuance->isFlag(lsfMPTCanLock))
{
// For readability two separate `if` rather than `||` of two conditions
if (!ctx.view.rules().enabled(featureSingleAssetVault))
if (!ctx.view.rules().enabled(featureSingleAssetVault) &&
!ctx.view.rules().enabled(featureDynamicMPT))
return tecNO_PERMISSION;
else if (ctx.tx.isFlag(tfMPTLock) || ctx.tx.isFlag(tfMPTUnlock))
return tecNO_PERMISSION;
@@ -152,6 +220,44 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
}
}
// sfMutableFlags is soeDEFAULT, defaulting to 0 if not specified on
// the ledger.
auto const currentMutableFlags =
sleMptIssuance->getFieldU32(sfMutableFlags);
auto isMutableFlag = [&](std::uint32_t mutableFlag) -> bool {
return currentMutableFlags & mutableFlag;
};
if (auto const mutableFlags = ctx.tx[~sfMutableFlags])
{
if (std::any_of(
mptMutabilityFlags.begin(),
mptMutabilityFlags.end(),
[mutableFlags, &isMutableFlag](auto const& f) {
return !isMutableFlag(f.canMutateFlag) &&
((*mutableFlags & (f.setFlag | f.clearFlag)));
}))
return tecNO_PERMISSION;
}
if (!isMutableFlag(lsfMPTCanMutateMetadata) &&
ctx.tx.isFieldPresent(sfMPTokenMetadata))
return tecNO_PERMISSION;
if (auto const fee = ctx.tx[~sfTransferFee])
{
// A non-zero TransferFee is only valid if the lsfMPTCanTransfer flag
// was previously enabled (at issuance or via a prior mutation). Setting
// it by tfMPTSetCanTransfer in the current transaction does not meet
// this requirement.
if (fee > 0u && !sleMptIssuance->isFlag(lsfMPTCanTransfer))
return tecNO_PERMISSION;
if (!isMutableFlag(lsfMPTCanMutateTransferFee))
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
@@ -180,9 +286,47 @@ MPTokenIssuanceSet::doApply()
else if (txFlags & tfMPTUnlock)
flagsOut &= ~lsfMPTLocked;
if (auto const mutableFlags = ctx_.tx[~sfMutableFlags].value_or(0))
{
for (auto const& f : mptMutabilityFlags)
{
if (mutableFlags & f.setFlag)
flagsOut |= f.canMutateFlag;
else if (mutableFlags & f.clearFlag)
flagsOut &= ~f.canMutateFlag;
}
if (mutableFlags & tfMPTClearCanTransfer)
{
// If the lsfMPTCanTransfer flag is being cleared, then also clear
// the TransferFee field.
sle->makeFieldAbsent(sfTransferFee);
}
}
if (flagsIn != flagsOut)
sle->setFieldU32(sfFlags, flagsOut);
if (auto const transferFee = ctx_.tx[~sfTransferFee])
{
// TransferFee uses soeDEFAULT style:
// - If the field is absent, it is interpreted as 0.
// - If the field is present, it must be non-zero.
// Therefore, when TransferFee is 0, the field should be removed.
if (transferFee == 0)
sle->makeFieldAbsent(sfTransferFee);
else
sle->setFieldU16(sfTransferFee, *transferFee);
}
if (auto const metadata = ctx_.tx[~sfMPTokenMetadata])
{
if (metadata->empty())
sle->makeFieldAbsent(sfMPTokenMetadata);
else
sle->setFieldVL(sfMPTokenMetadata, *metadata);
}
if (domainID)
{
// This is enforced in preflight.