mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-04 11:15:56 +00:00
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:
@@ -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,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user