Add support for DomainID in MPTokenIssuance transactions (#5509)

This change adds support for `DomainID` to existing transactions `MPTokenIssuanceCreate` and `MPTokenIssuanceSet`.

In #5224 `DomainID` was added as an access control mechanism for `SingleAssetVault`. The actual implementation of this feature lies in `MPToken` and `MPTokenIssuance`, hence it makes sense to enable the use of `DomainID` also in `MPTokenIssuanceCreate` and `MPTokenIssuanceSet`, following same rules as in Vault:

* `MPTokenIssuanceCreate` and `MPTokenIssuanceSet` can only set `DomainID` if flag `MPTRequireAuth` is set.
* `MPTokenIssuanceCreate` requires that `DomainID` be a non-zero, uint256 number.
* `MPTokenIssuanceSet` allows `DomainID` to be zero (or empty) in which case it will remove `DomainID` from the `MPTokenIssuance` object.

The change is amendment-gated by `SingleAssetVault`. This is a non-breaking change because `SingleAssetVault` amendment is `Supported::no`, i.e. at this moment considered a work in progress, which cannot be enabled on the network.
This commit is contained in:
Bronek Kozicki
2025-07-23 18:21:30 +01:00
committed by GitHub
parent 433eeabfa5
commit 80d82c5b2b
7 changed files with 615 additions and 67 deletions

View File

@@ -482,8 +482,7 @@ LEDGER_ENTRY(ltDELEGATE, 0x0083, Delegate, delegate, ({
})) }))
/** A ledger object representing a single asset vault. /** A ledger object representing a single asset vault.
\sa keylet::vault
\sa keylet::mptoken
*/ */
LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
{sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED},

View File

@@ -409,6 +409,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::de
{sfTransferFee, soeOPTIONAL}, {sfTransferFee, soeOPTIONAL},
{sfMaximumAmount, soeOPTIONAL}, {sfMaximumAmount, soeOPTIONAL},
{sfMPTokenMetadata, soeOPTIONAL}, {sfMPTokenMetadata, soeOPTIONAL},
{sfDomainID, soeOPTIONAL},
})) }))
/** This transaction type destroys a MPTokensIssuance instance */ /** This transaction type destroys a MPTokensIssuance instance */
@@ -420,6 +421,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation::
TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, ({ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, ({
{sfMPTokenIssuanceID, soeREQUIRED}, {sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeOPTIONAL}, {sfHolder, soeOPTIONAL},
{sfDomainID, soeOPTIONAL},
})) }))
/** This transaction type authorizes a MPToken instance */ /** This transaction type authorizes a MPToken instance */
@@ -478,7 +480,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({
{sfAsset, soeREQUIRED, soeMPTSupported}, {sfAsset, soeREQUIRED, soeMPTSupported},
{sfAssetsMaximum, soeOPTIONAL}, {sfAssetsMaximum, soeOPTIONAL},
{sfMPTokenMetadata, soeOPTIONAL}, {sfMPTokenMetadata, soeOPTIONAL},
{sfDomainID, soeOPTIONAL}, // PermissionedDomainID {sfDomainID, soeOPTIONAL},
{sfWithdrawalPolicy, soeOPTIONAL}, {sfWithdrawalPolicy, soeOPTIONAL},
{sfData, soeOPTIONAL}, {sfData, soeOPTIONAL},
})) }))
@@ -487,7 +489,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({
TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, ({ TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, ({
{sfVaultID, soeREQUIRED}, {sfVaultID, soeREQUIRED},
{sfAssetsMaximum, soeOPTIONAL}, {sfAssetsMaximum, soeOPTIONAL},
{sfDomainID, soeOPTIONAL}, // PermissionedDomainID {sfDomainID, soeOPTIONAL},
{sfData, soeOPTIONAL}, {sfData, soeOPTIONAL},
})) }))

View File

@@ -18,10 +18,16 @@
//============================================================================== //==============================================================================
#include <test/jtx.h> #include <test/jtx.h>
#include <test/jtx/credentials.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/trust.h> #include <test/jtx/trust.h>
#include <test/jtx/xchain_bridge.h> #include <test/jtx/xchain_bridge.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/Feature.h> #include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h> #include <xrpl/protocol/jss.h>
namespace ripple { namespace ripple {
@@ -61,6 +67,48 @@ class MPToken_test : public beast::unit_test::suite
.metadata = "test", .metadata = "test",
.err = temMALFORMED}); .err = temMALFORMED});
if (!features[featureSingleAssetVault])
{
// tries to set DomainID when SAV is disabled
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.metadata = "test",
.flags = tfMPTRequireAuth,
.domainID = uint256(42),
.err = temDISABLED});
}
else if (!features[featurePermissionedDomains])
{
// tries to set DomainID when PD is disabled
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.metadata = "test",
.flags = tfMPTRequireAuth,
.domainID = uint256(42),
.err = temDISABLED});
}
else
{
// tries to set DomainID when RequireAuth is not set
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.metadata = "test",
.domainID = uint256(42),
.err = temMALFORMED});
// tries to set zero DomainID
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.metadata = "test",
.flags = tfMPTRequireAuth,
.domainID = beast::zero,
.err = temMALFORMED});
}
// tries to set a txfee greater than max // tries to set a txfee greater than max
mptAlice.create( mptAlice.create(
{.maxAmt = 100, {.maxAmt = 100,
@@ -140,6 +188,48 @@ class MPToken_test : public beast::unit_test::suite
BEAST_EXPECT( BEAST_EXPECT(
result[sfMaximumAmount.getJsonName()] == "9223372036854775807"); result[sfMaximumAmount.getJsonName()] == "9223372036854775807");
} }
if (features[featureSingleAssetVault])
{
// Add permissioned domain
Account const credIssuer1{"credIssuer1"};
std::string const credType = "credential";
pdomain::Credentials const credentials1{
{.issuer = credIssuer1, .credType = credType}};
{
Env env{*this, features};
env.fund(XRP(1000), credIssuer1);
env(pdomain::setTx(credIssuer1, credentials1));
auto const domainId1 = [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
MPTTester mptAlice(env, alice);
mptAlice.create({
.maxAmt = maxMPTokenAmount, // 9'223'372'036'854'775'807
.assetScale = 1,
.transferFee = 10,
.metadata = "123",
.ownerCount = 1,
.flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow |
tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback,
.domainID = domainId1,
});
// Get the hash for the most recent transaction.
std::string const txHash{
env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
Json::Value const result = env.rpc("tx", txHash)[jss::result];
BEAST_EXPECT(
result[sfMaximumAmount.getJsonName()] ==
"9223372036854775807");
}
}
} }
void void
@@ -499,6 +589,59 @@ class MPToken_test : public beast::unit_test::suite
.flags = 0x00000008, .flags = 0x00000008,
.err = temINVALID_FLAG}); .err = temINVALID_FLAG});
if (!features[featureSingleAssetVault])
{
// test invalid flags - nothing is being changed
mptAlice.set(
{.account = alice,
.flags = 0x00000000,
.err = tecNO_PERMISSION});
mptAlice.set(
{.account = alice,
.holder = bob,
.flags = 0x00000000,
.err = tecNO_PERMISSION});
// cannot set DomainID since SAV is not enabled
mptAlice.set(
{.account = alice,
.domainID = uint256(42),
.err = temDISABLED});
}
else
{
// test invalid flags - nothing is being changed
mptAlice.set(
{.account = alice,
.flags = 0x00000000,
.err = temMALFORMED});
mptAlice.set(
{.account = alice,
.holder = bob,
.flags = 0x00000000,
.err = temMALFORMED});
if (!features[featurePermissionedDomains])
{
// cannot set DomainID since PD is not enabled
mptAlice.set(
{.account = alice,
.domainID = uint256(42),
.err = temDISABLED});
}
else
{
// cannot set DomainID since Holder is set
mptAlice.set(
{.account = alice,
.holder = bob,
.domainID = uint256(42),
.err = temMALFORMED});
}
}
// set both lock and unlock flags at the same time will fail // set both lock and unlock flags at the same time will fail
mptAlice.set( mptAlice.set(
{.account = alice, {.account = alice,
@@ -582,6 +725,53 @@ class MPToken_test : public beast::unit_test::suite
mptAlice.set( mptAlice.set(
{.holder = cindy, .flags = tfMPTLock, .err = tecNO_DST}); {.holder = cindy, .flags = tfMPTLock, .err = tecNO_DST});
} }
if (features[featureSingleAssetVault] &&
features[featurePermissionedDomains])
{
// Add permissioned domain
Account const credIssuer1{"credIssuer1"};
std::string const credType = "credential";
pdomain::Credentials const credentials1{
{.issuer = credIssuer1, .credType = credType}};
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create({});
// Trying to set DomainID on a public MPTokenIssuance
mptAlice.set(
{.domainID = uint256(42), .err = tecNO_PERMISSION});
mptAlice.set(
{.domainID = beast::zero, .err = tecNO_PERMISSION});
}
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create({.flags = tfMPTRequireAuth});
// Trying to set non-existing DomainID
mptAlice.set(
{.domainID = uint256(42), .err = tecOBJECT_NOT_FOUND});
// Trying to lock but locking is disabled
mptAlice.set(
{.flags = tfMPTUnlock,
.domainID = uint256(42),
.err = tecNO_PERMISSION});
mptAlice.set(
{.flags = tfMPTUnlock,
.domainID = beast::zero,
.err = tecNO_PERMISSION});
}
}
} }
void void
@@ -590,71 +780,136 @@ class MPToken_test : public beast::unit_test::suite
testcase("Enabled set transaction"); testcase("Enabled set transaction");
using namespace test::jtx; using namespace test::jtx;
// Test locking and unlocking
Env env{*this, features};
Account const alice("alice"); // issuer Account const alice("alice"); // issuer
Account const bob("bob"); // holder Account const bob("bob"); // holder
MPTTester mptAlice(env, alice, {.holders = {bob}});
// create a mptokenissuance with locking
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock});
mptAlice.authorize({.account = bob, .holderCount = 1});
// locks bob's mptoken
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// trying to lock bob's mptoken again will still succeed
// but no changes to the objects
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// alice locks the mptissuance
mptAlice.set({.account = alice, .flags = tfMPTLock});
// alice tries to lock up both mptissuance and mptoken again
// it will not change the flags and both will remain locked.
mptAlice.set({.account = alice, .flags = tfMPTLock});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// alice unlocks bob's mptoken
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
// locks up bob's mptoken again
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
if (!features[featureSingleAssetVault])
{ {
// Delete bobs' mptoken even though it is locked // Test locking and unlocking
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize}); Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
// create a mptokenissuance with locking
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock});
mptAlice.authorize({.account = bob, .holderCount = 1});
// locks bob's mptoken
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// trying to lock bob's mptoken again will still succeed
// but no changes to the objects
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// alice locks the mptissuance
mptAlice.set({.account = alice, .flags = tfMPTLock});
// alice tries to lock up both mptissuance and mptoken again
// it will not change the flags and both will remain locked.
mptAlice.set({.account = alice, .flags = tfMPTLock});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// alice unlocks bob's mptoken
mptAlice.set( mptAlice.set(
{.account = alice, {.account = alice, .holder = bob, .flags = tfMPTUnlock});
.holder = bob,
.flags = tfMPTUnlock,
.err = tecOBJECT_NOT_FOUND});
return; // locks up bob's mptoken again
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
if (!features[featureSingleAssetVault])
{
// Delete bobs' mptoken even though it is locked
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
mptAlice.set(
{.account = alice,
.holder = bob,
.flags = tfMPTUnlock,
.err = tecOBJECT_NOT_FOUND});
return;
}
// Cannot delete locked MPToken
mptAlice.authorize(
{.account = bob,
.flags = tfMPTUnauthorize,
.err = tecNO_PERMISSION});
// alice unlocks mptissuance
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
// alice unlocks bob's mptoken
mptAlice.set(
{.account = alice, .holder = bob, .flags = tfMPTUnlock});
// alice unlocks mptissuance and bob's mptoken again despite that
// they are already unlocked. Make sure this will not change the
// flags
mptAlice.set(
{.account = alice, .holder = bob, .flags = tfMPTUnlock});
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
} }
// Cannot delete locked MPToken if (features[featureSingleAssetVault])
mptAlice.authorize( {
{.account = bob, // Add permissioned domain
.flags = tfMPTUnauthorize, std::string const credType = "credential";
.err = tecNO_PERMISSION});
// alice unlocks mptissuance // Test setting and resetting domain ID
mptAlice.set({.account = alice, .flags = tfMPTUnlock}); Env env{*this, features};
// alice unlocks bob's mptoken auto const domainId1 = [&]() {
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock}); Account const credIssuer1{"credIssuer1"};
env.fund(XRP(1000), credIssuer1);
// alice unlocks mptissuance and bob's mptoken again despite that pdomain::Credentials const credentials1{
// they are already unlocked. Make sure this will not change the {.issuer = credIssuer1, .credType = credType}};
// flags
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock}); env(pdomain::setTx(credIssuer1, credentials1));
mptAlice.set({.account = alice, .flags = tfMPTUnlock}); return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
auto const domainId2 = [&]() {
Account const credIssuer2{"credIssuer2"};
env.fund(XRP(1000), credIssuer2);
pdomain::Credentials const credentials2{
{.issuer = credIssuer2, .credType = credType}};
env(pdomain::setTx(credIssuer2, credentials2));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
MPTTester mptAlice(env, alice, {.holders = {bob}});
// create a mptokenissuance with auth.
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth});
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
// reset "domain not set" to "domain not set", i.e. no change
mptAlice.set({.domainID = beast::zero});
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
// reset "domain not set" to domain1
mptAlice.set({.domainID = domainId1});
BEAST_EXPECT(mptAlice.checkDomainID(domainId1));
// reset domain1 to domain2
mptAlice.set({.domainID = domainId2});
BEAST_EXPECT(mptAlice.checkDomainID(domainId2));
// reset domain to "domain not set"
mptAlice.set({.domainID = beast::zero});
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
}
} }
void void
@@ -889,6 +1144,200 @@ class MPToken_test : public beast::unit_test::suite
mptAlice.pay(bob, alice, 100, tecNO_AUTH); mptAlice.pay(bob, alice, 100, tecNO_AUTH);
} }
if (features[featureSingleAssetVault] &&
features[featurePermissionedDomains])
{
// If RequireAuth is enabled and domain is a match, payment succeeds
{
Env env{*this, features};
std::string const credType = "credential";
Account const credIssuer1{"credIssuer1"};
env.fund(XRP(1000), credIssuer1, bob);
auto const domainId1 = [&]() {
pdomain::Credentials const credentials1{
{.issuer = credIssuer1, .credType = credType}};
env(pdomain::setTx(credIssuer1, credentials1));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
// bob is authorized via domain
env(credentials::create(bob, credIssuer1, credType));
env(credentials::accept(bob, credIssuer1, credType));
env.close();
MPTTester mptAlice(env, alice, {});
env.close();
mptAlice.create({
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
.domainID = domainId1,
});
mptAlice.authorize({.account = bob});
env.close();
// bob is authorized via domain
mptAlice.pay(alice, bob, 100);
mptAlice.set({.domainID = beast::zero});
// bob is no longer authorized
mptAlice.pay(alice, bob, 100, tecNO_AUTH);
}
{
Env env{*this, features};
std::string const credType = "credential";
Account const credIssuer1{"credIssuer1"};
env.fund(XRP(1000), credIssuer1, bob);
auto const domainId1 = [&]() {
pdomain::Credentials const credentials1{
{.issuer = credIssuer1, .credType = credType}};
env(pdomain::setTx(credIssuer1, credentials1));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
// bob is authorized via domain
env(credentials::create(bob, credIssuer1, credType));
env(credentials::accept(bob, credIssuer1, credType));
env.close();
MPTTester mptAlice(env, alice, {});
env.close();
mptAlice.create({
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
.domainID = domainId1,
});
// bob creates an empty MPToken
mptAlice.authorize({.account = bob});
// alice authorizes bob to hold funds
mptAlice.authorize({.account = alice, .holder = bob});
// alice sends 100 MPT to bob
mptAlice.pay(alice, bob, 100);
// alice UNAUTHORIZES bob
mptAlice.authorize(
{.account = alice,
.holder = bob,
.flags = tfMPTUnauthorize});
// bob is still authorized, via domain
mptAlice.pay(bob, alice, 10);
mptAlice.set({.domainID = beast::zero});
// bob fails to send back to alice because he is no longer
// authorize to move his funds!
mptAlice.pay(bob, alice, 10, tecNO_AUTH);
}
{
Env env{*this, features};
std::string const credType = "credential";
// credIssuer1 is the owner of domainId1 and a credential issuer
Account const credIssuer1{"credIssuer1"};
// credIssuer2 is the owner of domainId2 and a credential issuer
// Note, domainId2 also lists credentials issued by credIssuer1
Account const credIssuer2{"credIssuer2"};
env.fund(XRP(1000), credIssuer1, credIssuer2, bob, carol);
auto const domainId1 = [&]() {
pdomain::Credentials const credentials{
{.issuer = credIssuer1, .credType = credType}};
env(pdomain::setTx(credIssuer1, credentials));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
auto const domainId2 = [&]() {
pdomain::Credentials const credentials{
{.issuer = credIssuer1, .credType = credType},
{.issuer = credIssuer2, .credType = credType}};
env(pdomain::setTx(credIssuer2, credentials));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
// bob is authorized via credIssuer1 which is recognized by both
// domainId1 and domainId2
env(credentials::create(bob, credIssuer1, credType));
env(credentials::accept(bob, credIssuer1, credType));
env.close();
// carol is authorized via credIssuer2, only recognized by
// domainId2
env(credentials::create(carol, credIssuer2, credType));
env(credentials::accept(carol, credIssuer2, credType));
env.close();
MPTTester mptAlice(env, alice, {});
env.close();
mptAlice.create({
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
.domainID = domainId1,
});
// bob and carol create an empty MPToken
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
env.close();
// alice sends 50 MPT to bob but cannot send to carol
mptAlice.pay(alice, bob, 50);
mptAlice.pay(alice, carol, 50, tecNO_AUTH);
env.close();
// bob cannot send to carol because they are not on the same
// domain (since credIssuer2 is not recognized by domainId1)
mptAlice.pay(bob, carol, 10, tecNO_AUTH);
env.close();
// alice updates domainID to domainId2 which recognizes both
// credIssuer1 and credIssuer2
mptAlice.set({.domainID = domainId2});
// alice can now send to carol
mptAlice.pay(alice, carol, 10);
env.close();
// bob can now send to carol because both are in the same
// domain
mptAlice.pay(bob, carol, 10);
env.close();
// bob loses his authorization and can no longer send MPT
env(credentials::deleteCred(
credIssuer1, bob, credIssuer1, credType));
env.close();
mptAlice.pay(bob, carol, 10, tecNO_AUTH);
mptAlice.pay(bob, alice, 10, tecNO_AUTH);
}
}
// Non-issuer cannot send to each other if MPTCanTransfer isn't set // Non-issuer cannot send to each other if MPTCanTransfer isn't set
{ {
Env env(*this, features); Env env(*this, features);
@@ -1340,10 +1789,8 @@ class MPToken_test : public beast::unit_test::suite
} }
void void
testDepositPreauth() testDepositPreauth(FeatureBitset features)
{ {
testcase("DepositPreauth");
using namespace test::jtx; using namespace test::jtx;
Account const alice("alice"); // issuer Account const alice("alice"); // issuer
Account const bob("bob"); // holder Account const bob("bob"); // holder
@@ -1352,8 +1799,11 @@ class MPToken_test : public beast::unit_test::suite
char const credType[] = "abcde"; char const credType[] = "abcde";
if (features[featureCredentials])
{ {
Env env(*this); testcase("DepositPreauth");
Env env(*this, features);
env.fund(XRP(50000), diana, dpIssuer); env.fund(XRP(50000), diana, dpIssuer);
env.close(); env.close();
@@ -2297,6 +2747,8 @@ public:
// MPTokenIssuanceCreate // MPTokenIssuanceCreate
testCreateValidation(all - featureSingleAssetVault); testCreateValidation(all - featureSingleAssetVault);
testCreateValidation(
(all | featureSingleAssetVault) - featurePermissionedDomains);
testCreateValidation(all | featureSingleAssetVault); testCreateValidation(all | featureSingleAssetVault);
testCreateEnabled(all - featureSingleAssetVault); testCreateEnabled(all - featureSingleAssetVault);
testCreateEnabled(all | featureSingleAssetVault); testCreateEnabled(all | featureSingleAssetVault);
@@ -2314,7 +2766,11 @@ public:
testAuthorizeEnabled(all | featureSingleAssetVault); testAuthorizeEnabled(all | featureSingleAssetVault);
// MPTokenIssuanceSet // MPTokenIssuanceSet
testSetValidation(all); testSetValidation(all - featureSingleAssetVault);
testSetValidation(
(all | featureSingleAssetVault) - featurePermissionedDomains);
testSetValidation(all | featureSingleAssetVault);
testSetEnabled(all - featureSingleAssetVault); testSetEnabled(all - featureSingleAssetVault);
testSetEnabled(all | featureSingleAssetVault); testSetEnabled(all | featureSingleAssetVault);
@@ -2323,8 +2779,9 @@ public:
testClawback(all); testClawback(all);
// Test Direct Payment // Test Direct Payment
testPayment(all); testPayment(all | featureSingleAssetVault);
testDepositPreauth(); testDepositPreauth(all);
testDepositPreauth(all - featureCredentials);
// Test MPT Amount is invalid in Tx, which don't support MPT // Test MPT Amount is invalid in Tx, which don't support MPT
testMPTInvalidInTx(all); testMPTInvalidInTx(all);

View File

@@ -19,6 +19,7 @@
#include <test/jtx.h> #include <test/jtx.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/jss.h> #include <xrpl/protocol/jss.h>
namespace ripple { namespace ripple {
@@ -99,6 +100,8 @@ MPTTester::create(MPTCreate const& arg)
jv[sfMPTokenMetadata] = strHex(*arg.metadata); jv[sfMPTokenMetadata] = strHex(*arg.metadata);
if (arg.maxAmt) if (arg.maxAmt)
jv[sfMaximumAmount] = std::to_string(*arg.maxAmt); jv[sfMaximumAmount] = std::to_string(*arg.maxAmt);
if (arg.domainID)
jv[sfDomainID] = to_string(*arg.domainID);
if (submit(arg, jv) != tesSUCCESS) if (submit(arg, jv) != tesSUCCESS)
{ {
// Verify issuance doesn't exist // Verify issuance doesn't exist
@@ -235,6 +238,8 @@ MPTTester::set(MPTSet const& arg)
jv[sfHolder] = arg.holder->human(); jv[sfHolder] = arg.holder->human();
if (arg.delegate) if (arg.delegate)
jv[sfDelegate] = arg.delegate->human(); 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 (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0))
{ {
auto require = [&](std::optional<Account> const& holder, auto require = [&](std::optional<Account> const& holder,
@@ -272,6 +277,16 @@ MPTTester::forObject(
return false; return false;
} }
[[nodiscard]] bool
MPTTester::checkDomainID(std::optional<uint256> expected) const
{
return forObject([&](SLEP const& sle) -> bool {
if (sle->isFieldPresent(sfDomainID))
return expected == sle->getFieldH256(sfDomainID);
return (!expected.has_value());
});
}
[[nodiscard]] bool [[nodiscard]] bool
MPTTester::checkMPTokenAmount( MPTTester::checkMPTokenAmount(
Account const& holder_, Account const& holder_,

View File

@@ -106,6 +106,7 @@ struct MPTCreate
std::optional<std::uint32_t> holderCount = std::nullopt; std::optional<std::uint32_t> holderCount = std::nullopt;
bool fund = true; bool fund = true;
std::optional<std::uint32_t> flags = {0}; std::optional<std::uint32_t> flags = {0};
std::optional<uint256> domainID = std::nullopt;
std::optional<TER> err = std::nullopt; std::optional<TER> err = std::nullopt;
}; };
@@ -139,6 +140,7 @@ struct MPTSet
std::optional<std::uint32_t> holderCount = std::nullopt; std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt; std::optional<std::uint32_t> flags = std::nullopt;
std::optional<Account> delegate = std::nullopt; std::optional<Account> delegate = std::nullopt;
std::optional<uint256> domainID = std::nullopt;
std::optional<TER> err = std::nullopt; std::optional<TER> err = std::nullopt;
}; };
@@ -165,6 +167,9 @@ public:
void void
set(MPTSet const& set = {}); set(MPTSet const& set = {});
[[nodiscard]] bool
checkDomainID(std::optional<uint256> expected) const;
[[nodiscard]] bool [[nodiscard]] bool
checkMPTokenAmount(Account const& holder, std::int64_t expectedAmount) checkMPTokenAmount(Account const& holder, std::int64_t expectedAmount)
const; const;

View File

@@ -31,6 +31,11 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
if (!ctx.rules.enabled(featureMPTokensV1)) if (!ctx.rules.enabled(featureMPTokensV1))
return temDISABLED; return temDISABLED;
if (ctx.tx.isFieldPresent(sfDomainID) &&
!(ctx.rules.enabled(featurePermissionedDomains) &&
ctx.rules.enabled(featureSingleAssetVault)))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret; return ret;
@@ -48,6 +53,16 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
return temMALFORMED; return temMALFORMED;
} }
if (auto const domain = ctx.tx[~sfDomainID])
{
if (*domain == beast::zero)
return temMALFORMED;
// Domain present implies that MPTokenIssuance is not public
if ((ctx.tx.getFlags() & tfMPTRequireAuth) == 0)
return temMALFORMED;
}
if (auto const metadata = ctx.tx[~sfMPTokenMetadata]) if (auto const metadata = ctx.tx[~sfMPTokenMetadata])
{ {
if (metadata->length() == 0 || if (metadata->length() == 0 ||
@@ -142,6 +157,7 @@ MPTokenIssuanceCreate::doApply()
.assetScale = tx[~sfAssetScale], .assetScale = tx[~sfAssetScale],
.transferFee = tx[~sfTransferFee], .transferFee = tx[~sfTransferFee],
.metadata = tx[~sfMPTokenMetadata], .metadata = tx[~sfMPTokenMetadata],
.domainId = tx[~sfDomainID],
}); });
return result ? tesSUCCESS : result.error(); return result ? tesSUCCESS : result.error();
} }

View File

@@ -21,6 +21,7 @@
#include <xrpld/app/tx/detail/MPTokenIssuanceSet.h> #include <xrpld/app/tx/detail/MPTokenIssuanceSet.h>
#include <xrpl/protocol/Feature.h> #include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TxFlags.h> #include <xrpl/protocol/TxFlags.h>
namespace ripple { namespace ripple {
@@ -31,6 +32,14 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (!ctx.rules.enabled(featureMPTokensV1)) if (!ctx.rules.enabled(featureMPTokensV1))
return temDISABLED; return temDISABLED;
if (ctx.tx.isFieldPresent(sfDomainID) &&
!(ctx.rules.enabled(featurePermissionedDomains) &&
ctx.rules.enabled(featureSingleAssetVault)))
return temDISABLED;
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
return temMALFORMED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret; return ret;
@@ -48,6 +57,13 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (holderID && accountID == holderID) if (holderID && accountID == holderID)
return temMALFORMED; return temMALFORMED;
if (ctx.rules.enabled(featureSingleAssetVault))
{
// Is this transaction actually changing anything ?
if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID))
return temMALFORMED;
}
return preflight2(ctx); return preflight2(ctx);
} }
@@ -97,9 +113,14 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
if (!sleMptIssuance) if (!sleMptIssuance)
return tecOBJECT_NOT_FOUND; return tecOBJECT_NOT_FOUND;
// if the mpt has disabled locking if (!sleMptIssuance->isFlag(lsfMPTCanLock))
if (!((*sleMptIssuance)[sfFlags] & lsfMPTCanLock)) {
return tecNO_PERMISSION; // For readability two separate `if` rather than `||` of two conditions
if (!ctx.view.rules().enabled(featureSingleAssetVault))
return tecNO_PERMISSION;
else if (ctx.tx.isFlag(tfMPTLock) || ctx.tx.isFlag(tfMPTUnlock))
return tecNO_PERMISSION;
}
// ensure it is issued by the tx submitter // ensure it is issued by the tx submitter
if ((*sleMptIssuance)[sfIssuer] != ctx.tx[sfAccount]) if ((*sleMptIssuance)[sfIssuer] != ctx.tx[sfAccount])
@@ -117,6 +138,20 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return tecOBJECT_NOT_FOUND; return tecOBJECT_NOT_FOUND;
} }
if (auto const domain = ctx.tx[~sfDomainID])
{
if (not sleMptIssuance->isFlag(lsfMPTRequireAuth))
return tecNO_PERMISSION;
if (*domain != beast::zero)
{
auto const sleDomain =
ctx.view.read(keylet::permissionedDomain(*domain));
if (!sleDomain)
return tecOBJECT_NOT_FOUND;
}
}
return tesSUCCESS; return tesSUCCESS;
} }
@@ -126,6 +161,7 @@ MPTokenIssuanceSet::doApply()
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto const txFlags = ctx_.tx.getFlags(); auto const txFlags = ctx_.tx.getFlags();
auto const holderID = ctx_.tx[~sfHolder]; auto const holderID = ctx_.tx[~sfHolder];
auto const domainID = ctx_.tx[~sfDomainID];
std::shared_ptr<SLE> sle; std::shared_ptr<SLE> sle;
if (holderID) if (holderID)
@@ -147,6 +183,24 @@ MPTokenIssuanceSet::doApply()
if (flagsIn != flagsOut) if (flagsIn != flagsOut)
sle->setFieldU32(sfFlags, flagsOut); sle->setFieldU32(sfFlags, flagsOut);
if (domainID)
{
// This is enforced in preflight.
XRPL_ASSERT(
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
if (*domainID != beast::zero)
{
sle->setFieldH256(sfDomainID, *domainID);
}
else
{
if (sle->isFieldPresent(sfDomainID))
sle->makeFieldAbsent(sfDomainID);
}
}
view().update(sle); view().update(sle);
return tesSUCCESS; return tesSUCCESS;