mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 19:15:54 +00:00
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:
@@ -482,8 +482,7 @@ LEDGER_ENTRY(ltDELEGATE, 0x0083, Delegate, delegate, ({
|
||||
}))
|
||||
|
||||
/** A ledger object representing a single asset vault.
|
||||
|
||||
\sa keylet::mptoken
|
||||
\sa keylet::vault
|
||||
*/
|
||||
LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
|
||||
@@ -409,6 +409,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::de
|
||||
{sfTransferFee, soeOPTIONAL},
|
||||
{sfMaximumAmount, soeOPTIONAL},
|
||||
{sfMPTokenMetadata, soeOPTIONAL},
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** 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, ({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfHolder, soeOPTIONAL},
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type authorizes a MPToken instance */
|
||||
@@ -478,7 +480,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({
|
||||
{sfAsset, soeREQUIRED, soeMPTSupported},
|
||||
{sfAssetsMaximum, soeOPTIONAL},
|
||||
{sfMPTokenMetadata, soeOPTIONAL},
|
||||
{sfDomainID, soeOPTIONAL}, // PermissionedDomainID
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
{sfWithdrawalPolicy, soeOPTIONAL},
|
||||
{sfData, soeOPTIONAL},
|
||||
}))
|
||||
@@ -487,7 +489,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({
|
||||
TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, ({
|
||||
{sfVaultID, soeREQUIRED},
|
||||
{sfAssetsMaximum, soeOPTIONAL},
|
||||
{sfDomainID, soeOPTIONAL}, // PermissionedDomainID
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
{sfData, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <test/jtx.h>
|
||||
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -99,6 +100,8 @@ MPTTester::create(MPTCreate const& arg)
|
||||
jv[sfMPTokenMetadata] = strHex(*arg.metadata);
|
||||
if (arg.maxAmt)
|
||||
jv[sfMaximumAmount] = std::to_string(*arg.maxAmt);
|
||||
if (arg.domainID)
|
||||
jv[sfDomainID] = to_string(*arg.domainID);
|
||||
if (submit(arg, jv) != tesSUCCESS)
|
||||
{
|
||||
// Verify issuance doesn't exist
|
||||
@@ -235,6 +238,8 @@ MPTTester::set(MPTSet const& arg)
|
||||
jv[sfHolder] = arg.holder->human();
|
||||
if (arg.delegate)
|
||||
jv[sfDelegate] = arg.delegate->human();
|
||||
if (arg.domainID)
|
||||
jv[sfDomainID] = to_string(*arg.domainID);
|
||||
if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0))
|
||||
{
|
||||
auto require = [&](std::optional<Account> const& holder,
|
||||
@@ -272,6 +277,16 @@ MPTTester::forObject(
|
||||
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
|
||||
MPTTester::checkMPTokenAmount(
|
||||
Account const& holder_,
|
||||
|
||||
@@ -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<uint256> domainID = 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> flags = std::nullopt;
|
||||
std::optional<Account> delegate = std::nullopt;
|
||||
std::optional<uint256> domainID = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
@@ -165,6 +167,9 @@ public:
|
||||
void
|
||||
set(MPTSet const& set = {});
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkDomainID(std::optional<uint256> expected) const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkMPTokenAmount(Account const& holder, std::int64_t expectedAmount)
|
||||
const;
|
||||
|
||||
@@ -31,6 +31,11 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
|
||||
if (!ctx.rules.enabled(featureMPTokensV1))
|
||||
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))
|
||||
return ret;
|
||||
|
||||
@@ -48,6 +53,16 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
|
||||
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 (metadata->length() == 0 ||
|
||||
@@ -142,6 +157,7 @@ MPTokenIssuanceCreate::doApply()
|
||||
.assetScale = tx[~sfAssetScale],
|
||||
.transferFee = tx[~sfTransferFee],
|
||||
.metadata = tx[~sfMPTokenMetadata],
|
||||
.domainId = tx[~sfDomainID],
|
||||
});
|
||||
return result ? tesSUCCESS : result.error();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <xrpld/app/tx/detail/MPTokenIssuanceSet.h>
|
||||
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -31,6 +32,14 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
if (!ctx.rules.enabled(featureMPTokensV1))
|
||||
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))
|
||||
return ret;
|
||||
|
||||
@@ -48,6 +57,13 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
if (holderID && accountID == holderID)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -97,9 +113,14 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
if (!sleMptIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// if the mpt has disabled locking
|
||||
if (!((*sleMptIssuance)[sfFlags] & lsfMPTCanLock))
|
||||
return tecNO_PERMISSION;
|
||||
if (!sleMptIssuance->isFlag(lsfMPTCanLock))
|
||||
{
|
||||
// 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
|
||||
if ((*sleMptIssuance)[sfIssuer] != ctx.tx[sfAccount])
|
||||
@@ -117,6 +138,20 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -126,6 +161,7 @@ MPTokenIssuanceSet::doApply()
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto const txFlags = ctx_.tx.getFlags();
|
||||
auto const holderID = ctx_.tx[~sfHolder];
|
||||
auto const domainID = ctx_.tx[~sfDomainID];
|
||||
std::shared_ptr<SLE> sle;
|
||||
|
||||
if (holderID)
|
||||
@@ -147,6 +183,24 @@ MPTokenIssuanceSet::doApply()
|
||||
if (flagsIn != 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);
|
||||
|
||||
return tesSUCCESS;
|
||||
|
||||
Reference in New Issue
Block a user