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

@@ -18,10 +18,16 @@
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/credentials.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/trust.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/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
@@ -61,6 +67,48 @@ class MPToken_test : public beast::unit_test::suite
.metadata = "test",
.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
mptAlice.create(
{.maxAmt = 100,
@@ -140,6 +188,48 @@ class MPToken_test : public beast::unit_test::suite
BEAST_EXPECT(
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
@@ -499,6 +589,59 @@ class MPToken_test : public beast::unit_test::suite
.flags = 0x00000008,
.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
mptAlice.set(
{.account = alice,
@@ -582,6 +725,53 @@ class MPToken_test : public beast::unit_test::suite
mptAlice.set(
{.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
@@ -590,71 +780,136 @@ class MPToken_test : public beast::unit_test::suite
testcase("Enabled set transaction");
using namespace test::jtx;
// Test locking and unlocking
Env env{*this, features};
Account const alice("alice"); // issuer
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
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
// Test locking and unlocking
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(
{.account = alice,
.holder = bob,
.flags = tfMPTUnlock,
.err = tecOBJECT_NOT_FOUND});
{.account = alice, .holder = bob, .flags = tfMPTUnlock});
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
mptAlice.authorize(
{.account = bob,
.flags = tfMPTUnauthorize,
.err = tecNO_PERMISSION});
if (features[featureSingleAssetVault])
{
// Add permissioned domain
std::string const credType = "credential";
// alice unlocks mptissuance
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
// Test setting and resetting domain ID
Env env{*this, features};
// alice unlocks bob's mptoken
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
auto const domainId1 = [&]() {
Account const credIssuer1{"credIssuer1"};
env.fund(XRP(1000), credIssuer1);
// 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});
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());
}();
}();
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
@@ -889,6 +1144,200 @@ class MPToken_test : public beast::unit_test::suite
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
{
Env env(*this, features);
@@ -1340,10 +1789,8 @@ class MPToken_test : public beast::unit_test::suite
}
void
testDepositPreauth()
testDepositPreauth(FeatureBitset features)
{
testcase("DepositPreauth");
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder
@@ -1352,8 +1799,11 @@ class MPToken_test : public beast::unit_test::suite
char const credType[] = "abcde";
if (features[featureCredentials])
{
Env env(*this);
testcase("DepositPreauth");
Env env(*this, features);
env.fund(XRP(50000), diana, dpIssuer);
env.close();
@@ -2297,6 +2747,8 @@ public:
// MPTokenIssuanceCreate
testCreateValidation(all - featureSingleAssetVault);
testCreateValidation(
(all | featureSingleAssetVault) - featurePermissionedDomains);
testCreateValidation(all | featureSingleAssetVault);
testCreateEnabled(all - featureSingleAssetVault);
testCreateEnabled(all | featureSingleAssetVault);
@@ -2314,7 +2766,11 @@ public:
testAuthorizeEnabled(all | featureSingleAssetVault);
// MPTokenIssuanceSet
testSetValidation(all);
testSetValidation(all - featureSingleAssetVault);
testSetValidation(
(all | featureSingleAssetVault) - featurePermissionedDomains);
testSetValidation(all | featureSingleAssetVault);
testSetEnabled(all - featureSingleAssetVault);
testSetEnabled(all | featureSingleAssetVault);
@@ -2323,8 +2779,9 @@ public:
testClawback(all);
// Test Direct Payment
testPayment(all);
testDepositPreauth();
testPayment(all | featureSingleAssetVault);
testDepositPreauth(all);
testDepositPreauth(all - featureCredentials);
// Test MPT Amount is invalid in Tx, which don't support MPT
testMPTInvalidInTx(all);