#include #include #include #include #include #include #include #include #include #include #include namespace ripple { namespace test { class MPToken_test : public beast::unit_test::suite { void testCreateValidation(FeatureBitset features) { testcase("Create Validate"); using namespace test::jtx; Account const alice("alice"); // test preflight of MPTokenIssuanceCreate { // If the MPT amendment is not enabled, you should not be able to // create MPTokenIssuances Env env{*this, features - featureMPTokensV1}; MPTTester mptAlice(env, alice); mptAlice.create({.ownerCount = 0, .err = temDISABLED}); } // test preflight of MPTokenIssuanceCreate { Env env{*this, features}; MPTTester mptAlice(env, alice); mptAlice.create({.flags = 0x00000001, .err = temINVALID_FLAG}); // tries to set a txfee while not enabling in the flag mptAlice.create( {.maxAmt = 100, .assetScale = 0, .transferFee = 1, .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, .assetScale = 0, .transferFee = maxTransferFee + 1, .metadata = "test", .flags = tfMPTCanTransfer, .err = temBAD_TRANSFER_FEE}); // tries to set a txfee while not enabling transfer mptAlice.create( {.maxAmt = 100, .assetScale = 0, .transferFee = maxTransferFee, .metadata = "test", .err = temMALFORMED}); // empty metadata returns error mptAlice.create( {.maxAmt = 100, .assetScale = 0, .transferFee = 0, .metadata = "", .err = temMALFORMED}); // MaximumAmout of 0 returns error mptAlice.create( {.maxAmt = 0, .assetScale = 1, .transferFee = 1, .metadata = "test", .err = temMALFORMED}); // MaximumAmount larger than 63 bit returns error mptAlice.create( {.maxAmt = 0xFFFF'FFFF'FFFF'FFF0, // 18'446'744'073'709'551'600 .assetScale = 0, .transferFee = 0, .metadata = "test", .err = temMALFORMED}); mptAlice.create( {.maxAmt = maxMPTokenAmount + 1, // 9'223'372'036'854'775'808 .assetScale = 0, .transferFee = 0, .metadata = "test", .err = temMALFORMED}); } } void testCreateEnabled(FeatureBitset features) { testcase("Create Enabled"); using namespace test::jtx; Account const alice("alice"); { // If the MPT amendment IS enabled, you should be able to create // MPTokenIssuances Env env{*this, features}; 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}); // 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"); } 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 testDestroyValidation(FeatureBitset features) { testcase("Destroy Validate"); using namespace test::jtx; Account const alice("alice"); Account const bob("bob"); // MPTokenIssuanceDestroy (preflight) { Env env{*this, features - featureMPTokensV1}; MPTTester mptAlice(env, alice); auto const id = makeMptID(env.seq(alice), alice); mptAlice.destroy({.id = id, .ownerCount = 0, .err = temDISABLED}); env.enableFeature(featureMPTokensV1); mptAlice.destroy( {.id = id, .flags = 0x00000001, .err = temINVALID_FLAG}); } // MPTokenIssuanceDestroy (preclaim) { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.destroy( {.id = makeMptID(env.seq(alice), alice), .ownerCount = 0, .err = tecOBJECT_NOT_FOUND}); mptAlice.create({.ownerCount = 1}); // a non-issuer tries to destroy a mptissuance they didn't issue mptAlice.destroy({.issuer = bob, .err = tecNO_PERMISSION}); // Make sure that issuer can't delete issuance when it still has // outstanding balance { // bob now holds a mptoken object mptAlice.authorize({.account = bob, .holderCount = 1}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); mptAlice.destroy({.err = tecHAS_OBLIGATIONS}); } } } void testDestroyEnabled(FeatureBitset features) { testcase("Destroy Enabled"); using namespace test::jtx; Account const alice("alice"); // If the MPT amendment IS enabled, you should be able to destroy // MPTokenIssuances Env env{*this, features}; MPTTester mptAlice(env, alice); mptAlice.create({.ownerCount = 1}); mptAlice.destroy({.ownerCount = 0}); } void testAuthorizeValidation(FeatureBitset features) { testcase("Validate authorize transaction"); using namespace test::jtx; Account const alice("alice"); Account const bob("bob"); Account const cindy("cindy"); // Validate amendment enable in MPTokenAuthorize (preflight) { Env env{*this, features - featureMPTokensV1}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.authorize( {.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED}); } // Validate fields in MPTokenAuthorize (preflight) { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1}); // The only valid MPTokenAuthorize flag is tfMPTUnauthorize, which // has a value of 1 mptAlice.authorize( {.account = bob, .flags = 0x00000002, .err = temINVALID_FLAG}); mptAlice.authorize( {.account = bob, .holder = bob, .err = temMALFORMED}); mptAlice.authorize({.holder = alice, .err = temMALFORMED}); } // Try authorizing when MPTokenIssuance doesn't exist in // MPTokenAuthorize (preclaim) { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); auto const id = makeMptID(env.seq(alice), alice); mptAlice.authorize( {.holder = bob, .id = id, .err = tecOBJECT_NOT_FOUND}); mptAlice.authorize( {.account = bob, .id = id, .err = tecOBJECT_NOT_FOUND}); } // Test bad scenarios without allowlisting in MPTokenAuthorize // (preclaim) { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1}); // bob submits a tx with a holder field mptAlice.authorize( {.account = bob, .holder = alice, .err = tecNO_PERMISSION}); // alice tries to hold onto her own token mptAlice.authorize({.account = alice, .err = tecNO_PERMISSION}); // the mpt does not enable allowlisting mptAlice.authorize({.holder = bob, .err = tecNO_AUTH}); // bob now holds a mptoken object mptAlice.authorize({.account = bob, .holderCount = 1}); // bob cannot create the mptoken the second time mptAlice.authorize({.account = bob, .err = tecDUPLICATE}); // Check that bob cannot delete MPToken when his balance is // non-zero { // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); // bob tries to delete his MPToken, but fails since he still // holds tokens mptAlice.authorize( {.account = bob, .flags = tfMPTUnauthorize, .err = tecHAS_OBLIGATIONS}); // bob pays back alice 100 tokens mptAlice.pay(bob, alice, 100); } // bob deletes/unauthorizes his MPToken mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize}); // bob receives error when he tries to delete his MPToken that has // already been deleted mptAlice.authorize( {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize, .err = tecOBJECT_NOT_FOUND}); } // Test bad scenarios with allow-listing in MPTokenAuthorize (preclaim) { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth}); // alice submits a tx without specifying a holder's account mptAlice.authorize({.err = tecNO_PERMISSION}); // alice submits a tx to authorize a holder that hasn't created // a mptoken yet mptAlice.authorize({.holder = bob, .err = tecOBJECT_NOT_FOUND}); // alice specifys a holder acct that doesn't exist mptAlice.authorize({.holder = cindy, .err = tecNO_DST}); // bob now holds a mptoken object mptAlice.authorize({.account = bob, .holderCount = 1}); // alice tries to unauthorize bob. // although tx is successful, // but nothing happens because bob hasn't been authorized yet mptAlice.authorize({.holder = bob, .flags = tfMPTUnauthorize}); // alice authorizes bob // make sure bob's mptoken has set lsfMPTAuthorized mptAlice.authorize({.holder = bob}); // alice tries authorizes bob again. // tx is successful, but bob is already authorized, // so no changes mptAlice.authorize({.holder = bob}); // bob deletes his mptoken mptAlice.authorize( {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize}); } // Test mptoken reserve requirement - first two mpts free (doApply) { Env env{*this, features}; auto const acctReserve = env.current()->fees().reserve; auto const incReserve = env.current()->fees().increment; // 1 drop BEAST_EXPECT(incReserve > XRPAmount(1)); MPTTester mptAlice1( env, alice, {.holders = {bob}, .xrpHolders = acctReserve + (incReserve - 1)}); mptAlice1.create(); MPTTester mptAlice2(env, alice, {.fund = false}); mptAlice2.create(); MPTTester mptAlice3(env, alice, {.fund = false}); mptAlice3.create({.ownerCount = 3}); // first mpt for free mptAlice1.authorize({.account = bob, .holderCount = 1}); // second mpt free mptAlice2.authorize({.account = bob, .holderCount = 2}); mptAlice3.authorize( {.account = bob, .err = tecINSUFFICIENT_RESERVE}); env(pay( env.master, bob, drops(incReserve + incReserve + incReserve))); env.close(); mptAlice3.authorize({.account = bob, .holderCount = 3}); } } void testAuthorizeEnabled(FeatureBitset features) { testcase("Authorize Enabled"); using namespace test::jtx; Account const alice("alice"); Account const bob("bob"); // Basic authorization without allowlisting { Env env{*this, features}; // alice create mptissuance without allowisting MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1}); // bob creates a mptoken mptAlice.authorize({.account = bob, .holderCount = 1}); mptAlice.authorize( {.account = bob, .holderCount = 1, .err = tecDUPLICATE}); // bob deletes his mptoken mptAlice.authorize( {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize}); } // With allowlisting { Env env{*this, features}; // alice creates a mptokenissuance that requires authorization MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth}); // bob creates a mptoken mptAlice.authorize({.account = bob, .holderCount = 1}); // alice authorizes bob mptAlice.authorize({.account = alice, .holder = bob}); // Unauthorize bob's mptoken mptAlice.authorize( {.account = alice, .holder = bob, .holderCount = 1, .flags = tfMPTUnauthorize}); mptAlice.authorize( {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize}); } // Holder can have dangling MPToken even if issuance has been destroyed. // Make sure they can still delete/unauthorize the MPToken { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1}); // bob creates a mptoken mptAlice.authorize({.account = bob, .holderCount = 1}); // alice deletes her issuance mptAlice.destroy({.ownerCount = 0}); // bob can delete his mptoken even though issuance is no longer // existent mptAlice.authorize( {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize}); } } void testSetValidation(FeatureBitset features) { testcase("Validate set transaction"); using namespace test::jtx; Account const alice("alice"); // issuer Account const bob("bob"); // holder Account const cindy("cindy"); // Validate fields in MPTokenIssuanceSet (preflight) { Env env{*this, features - featureMPTokensV1}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.set( {.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED}); env.enableFeature(featureMPTokensV1); mptAlice.create({.ownerCount = 1, .holderCount = 0}); mptAlice.authorize({.account = bob, .holderCount = 1}); // test invalid flag - only valid flags are tfMPTLock (1) and Unlock // (2) mptAlice.set( {.account = alice, .flags = 0x00000008, .err = temINVALID_FLAG}); if (!features[featureSingleAssetVault] && !features[featureDynamicMPT]) { // 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] || !features[featureSingleAssetVault]) { // cannot set DomainID since PD is not enabled mptAlice.set( {.account = alice, .domainID = uint256(42), .err = temDISABLED}); } else if (features[featureSingleAssetVault]) { // 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, .flags = tfMPTLock | tfMPTUnlock, .err = temINVALID_FLAG}); // if the holder is the same as the acct that submitted the tx, // tx fails mptAlice.set( {.account = alice, .holder = alice, .flags = tfMPTLock, .err = temMALFORMED}); } // Validate fields in MPTokenIssuanceSet (preclaim) // test when a mptokenissuance has disabled locking { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1}); // alice tries to lock a mptissuance that has disabled locking mptAlice.set( {.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION}); // alice tries to unlock mptissuance that has disabled locking mptAlice.set( {.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION}); // issuer tries to lock a bob's mptoken that has disabled // locking mptAlice.set( {.account = alice, .holder = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION}); // issuer tries to unlock a bob's mptoken that has disabled // locking mptAlice.set( {.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION}); } // Validate fields in MPTokenIssuanceSet (preclaim) // test when mptokenissuance has enabled locking { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); // alice trying to set when the mptissuance doesn't exist yet mptAlice.set( {.id = makeMptID(env.seq(alice), alice), .flags = tfMPTLock, .err = tecOBJECT_NOT_FOUND}); // create a mptokenissuance with locking mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock}); // a non-issuer acct tries to set the mptissuance mptAlice.set( {.account = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION}); // trying to set a holder who doesn't have a mptoken mptAlice.set( {.holder = bob, .flags = tfMPTLock, .err = tecOBJECT_NOT_FOUND}); // trying to set a holder who doesn't exist 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 testSetEnabled(FeatureBitset features) { testcase("Enabled set transaction"); using namespace test::jtx; Account const alice("alice"); // issuer Account const bob("bob"); // holder { // 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}); // 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}); } if (features[featureSingleAssetVault]) { // Add permissioned domain std::string const credType = "credential"; // Test setting and resetting domain ID Env env{*this, features}; auto const domainId1 = [&]() { Account const credIssuer1{"credIssuer1"}; env.fund(XRP(1000), credIssuer1); 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 testPayment(FeatureBitset features) { testcase("Payment"); using namespace test::jtx; Account const alice("alice"); // issuer Account const bob("bob"); // holder Account const carol("carol"); // holder // preflight validation // MPT is disabled { Env env{*this, features - featureMPTokensV1}; Account const alice("alice"); Account const bob("bob"); env.fund(XRP(1'000), alice); env.fund(XRP(1'000), bob); STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)}; env(pay(alice, bob, mpt), ter(temDISABLED)); } // MPT is disabled, unsigned request { Env env{*this, features - featureMPTokensV1}; Account const alice("alice"); // issuer Account const carol("carol"); auto const USD = alice["USD"]; env.fund(XRP(1'000), alice); env.fund(XRP(1'000), carol); STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)}; Json::Value jv; jv[jss::secret] = alice.name(); jv[jss::tx_json] = pay(alice, carol, mpt); jv[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base); auto const jrr = env.rpc("json", "submit", to_string(jv)); BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "temDISABLED"); } // Invalid flag { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); auto const MPT = mptAlice["MPT"]; mptAlice.authorize({.account = bob}); for (auto flags : {tfNoRippleDirect, tfLimitQuality}) env(pay(alice, bob, MPT(10)), txflags(flags), ter(temINVALID_FLAG)); } // Invalid combination of send, sendMax, deliverMin, paths { Env env{*this, features}; Account const alice("alice"); Account const carol("carol"); MPTTester mptAlice(env, alice, {.holders = {carol}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); mptAlice.authorize({.account = carol}); // sendMax and DeliverMin are valid XRP amount, // but is invalid combination with MPT amount auto const MPT = mptAlice["MPT"]; env(pay(alice, carol, MPT(100)), sendmax(XRP(100)), ter(temMALFORMED)); env(pay(alice, carol, MPT(100)), delivermin(XRP(100)), ter(temBAD_AMOUNT)); // sendMax MPT is invalid with IOU or XRP auto const USD = alice["USD"]; env(pay(alice, carol, USD(100)), sendmax(MPT(100)), ter(temMALFORMED)); env(pay(alice, carol, XRP(100)), sendmax(MPT(100)), ter(temMALFORMED)); env(pay(alice, carol, USD(100)), delivermin(MPT(100)), ter(temBAD_AMOUNT)); env(pay(alice, carol, XRP(100)), delivermin(MPT(100)), ter(temBAD_AMOUNT)); // sendmax and amount are different MPT issue test::jtx::MPT const MPT1( "MPT", makeMptID(env.seq(alice) + 10, alice)); env(pay(alice, carol, MPT1(100)), sendmax(MPT(100)), ter(temMALFORMED)); // paths is invalid env(pay(alice, carol, MPT(100)), path(~USD), ter(temMALFORMED)); } // build_path is invalid if MPT { Env env{*this, features}; Account const alice("alice"); Account const carol("carol"); MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); auto const MPT = mptAlice["MPT"]; mptAlice.authorize({.account = carol}); Json::Value payment; payment[jss::secret] = alice.name(); payment[jss::tx_json] = pay(alice, carol, MPT(100)); payment[jss::build_path] = true; auto jrr = env.rpc("json", "submit", to_string(payment)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); BEAST_EXPECT( jrr[jss::result][jss::error_message] == "Field 'build_path' not allowed in this context."); } // Can't pay negative amount { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); auto const MPT = mptAlice["MPT"]; mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, -1, temBAD_AMOUNT); mptAlice.pay(bob, carol, -1, temBAD_AMOUNT); mptAlice.pay(bob, alice, -1, temBAD_AMOUNT); env(pay(alice, bob, MPT(10)), sendmax(MPT(-1)), ter(temBAD_AMOUNT)); } // Pay to self { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); mptAlice.authorize({.account = bob}); mptAlice.pay(bob, bob, 10, temREDUNDANT); } // preclaim validation // Destination doesn't exist { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); mptAlice.authorize({.account = bob}); Account const bad{"bad"}; env.memoize(bad); mptAlice.pay(bob, bad, 10, tecNO_DST); } // apply validation // If RequireAuth is enabled, Payment fails if the receiver is not // authorized { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer}); mptAlice.authorize({.account = bob}); mptAlice.pay(alice, bob, 100, tecNO_AUTH); } // If RequireAuth is enabled, Payment fails if the sender is not // authorized { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer}); // 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 fails to send back to alice because he is no longer // authorize to move his funds! 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); Account const alice{"alice"}; Account const bob{"bob"}; Account const cindy{"cindy"}; MPTTester mptAlice(env, alice, {.holders = {bob, cindy}}); // alice creates issuance without MPTCanTransfer mptAlice.create({.ownerCount = 1, .holderCount = 0}); // bob creates a MPToken mptAlice.authorize({.account = bob}); // cindy creates a MPToken mptAlice.authorize({.account = cindy}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); // bob tries to send cindy 10 tokens, but fails because canTransfer // is off mptAlice.pay(bob, cindy, 10, tecNO_AUTH); // bob can send back to alice(issuer) just fine mptAlice.pay(bob, alice, 10); } // Holder is not authorized { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); // issuer to holder mptAlice.pay(alice, bob, 100, tecNO_AUTH); // holder to issuer mptAlice.pay(bob, alice, 100, tecNO_AUTH); // holder to holder mptAlice.pay(bob, carol, 50, tecNO_AUTH); } // Payer doesn't have enough funds { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, 100); // Pay to another holder mptAlice.pay(bob, carol, 101, tecPATH_PARTIAL); // Pay to the issuer mptAlice.pay(bob, alice, 101, tecPATH_PARTIAL); } // MPT is locked { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanTransfer}); mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, 100); mptAlice.pay(alice, carol, 100); // Global lock mptAlice.set({.account = alice, .flags = tfMPTLock}); // Can't send between holders mptAlice.pay(bob, carol, 1, tecLOCKED); mptAlice.pay(carol, bob, 2, tecLOCKED); // Issuer can send mptAlice.pay(alice, bob, 3); // Holder can send back to issuer mptAlice.pay(bob, alice, 4); // Global unlock mptAlice.set({.account = alice, .flags = tfMPTUnlock}); // Individual lock mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); // Can't send between holders mptAlice.pay(bob, carol, 5, tecLOCKED); mptAlice.pay(carol, bob, 6, tecLOCKED); // Issuer can send mptAlice.pay(alice, bob, 7); // Holder can send back to issuer mptAlice.pay(bob, alice, 8); } // Transfer fee { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); // Transfer fee is 10% mptAlice.create( {.transferFee = 10'000, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); // Holders create MPToken mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); // Payment between the issuer and the holder, no transfer fee. mptAlice.pay(alice, bob, 2'000); // Payment between the holder and the issuer, no transfer fee. mptAlice.pay(bob, alice, 1'000); BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 1'000)); // Payment between the holders. The sender doesn't have // enough funds to cover the transfer fee. mptAlice.pay(bob, carol, 1'000, tecPATH_PARTIAL); // Payment between the holders. The sender has enough funds // but SendMax is not included. mptAlice.pay(bob, carol, 100, tecPATH_PARTIAL); auto const MPT = mptAlice["MPT"]; // SendMax doesn't cover the fee env(pay(bob, carol, MPT(100)), sendmax(MPT(109)), ter(tecPATH_PARTIAL)); // Payment succeeds if sufficient SendMax is included. // 100 to carol, 10 to issuer env(pay(bob, carol, MPT(100)), sendmax(MPT(110))); // 100 to carol, 10 to issuer env(pay(bob, carol, MPT(100)), sendmax(MPT(115))); BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 780)); BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 200)); // Payment succeeds if partial payment even if // SendMax is less than deliver amount env(pay(bob, carol, MPT(100)), sendmax(MPT(90)), txflags(tfPartialPayment)); // 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest) = // 82) BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690)); BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 282)); } // Insufficient SendMax with no transfer fee { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); // Holders create MPToken mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, 1'000); auto const MPT = mptAlice["MPT"]; // SendMax is less than the amount env(pay(bob, carol, MPT(100)), sendmax(MPT(99)), ter(tecPATH_PARTIAL)); env(pay(bob, alice, MPT(100)), sendmax(MPT(99)), ter(tecPATH_PARTIAL)); // Payment succeeds if sufficient SendMax is included. env(pay(bob, carol, MPT(100)), sendmax(MPT(100))); BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 100)); // Payment succeeds if partial payment env(pay(bob, carol, MPT(100)), sendmax(MPT(99)), txflags(tfPartialPayment)); BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 199)); } // DeliverMin { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); // Holders create MPToken mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, 1'000); auto const MPT = mptAlice["MPT"]; // Fails even with the partial payment because // deliver amount < deliverMin env(pay(bob, alice, MPT(100)), sendmax(MPT(99)), delivermin(MPT(100)), txflags(tfPartialPayment), ter(tecPATH_PARTIAL)); // Payment succeeds if deliver amount >= deliverMin env(pay(bob, alice, MPT(100)), sendmax(MPT(99)), delivermin(MPT(99)), txflags(tfPartialPayment)); } // Issuer fails trying to send more than the maximum amount allowed { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.maxAmt = 100, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); mptAlice.authorize({.account = bob}); // issuer sends holder the max amount allowed mptAlice.pay(alice, bob, 100); // issuer tries to exceed max amount mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL); } // Issuer fails trying to send more than the default maximum // amount allowed { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); mptAlice.authorize({.account = bob}); // issuer sends holder the default max amount allowed mptAlice.pay(alice, bob, maxMPTokenAmount); // issuer tries to exceed max amount mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL); } // Pay more than max amount fails in the json parser before // transactor is called { Env env{*this, features}; env.fund(XRP(1'000), alice, bob); STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)}; Json::Value jv; jv[jss::secret] = alice.name(); jv[jss::tx_json] = pay(alice, bob, mpt); jv[jss::tx_json][jss::Amount][jss::value] = to_string(maxMPTokenAmount + 1); auto const jrr = env.rpc("json", "submit", to_string(jv)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); } // Pay maximum amount with the transfer fee, SendMax, and // partial payment { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.maxAmt = 10'000, .transferFee = 100, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); auto const MPT = mptAlice["MPT"]; mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); // issuer sends holder the max amount allowed mptAlice.pay(alice, bob, 10'000); // payment between the holders env(pay(bob, carol, MPT(10'000)), sendmax(MPT(10'000)), txflags(tfPartialPayment)); // Verify the metadata auto const meta = env.meta()->getJson( JsonOptions::none)[sfAffectedNodes.fieldName]; // Issuer got 10 in the transfer fees BEAST_EXPECT( meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName] [sfOutstandingAmount.fieldName] == "9990"); // Destination account got 9'990 BEAST_EXPECT( meta[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName] [sfMPTAmount.fieldName] == "9990"); // Source account spent 10'000 BEAST_EXPECT( meta[2u][sfModifiedNode.fieldName][sfPreviousFields.fieldName] [sfMPTAmount.fieldName] == "10000"); BEAST_EXPECT( !meta[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName] .isMember(sfMPTAmount.fieldName)); // payment between the holders fails without // partial payment env(pay(bob, carol, MPT(10'000)), sendmax(MPT(10'000)), ter(tecPATH_PARTIAL)); } // Pay maximum allowed amount { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.maxAmt = maxMPTokenAmount, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); auto const MPT = mptAlice["MPT"]; mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); // issuer sends holder the max amount allowed mptAlice.pay(alice, bob, maxMPTokenAmount); BEAST_EXPECT( mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount)); // payment between the holders mptAlice.pay(bob, carol, maxMPTokenAmount); BEAST_EXPECT( mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount)); // holder pays back to the issuer mptAlice.pay(carol, alice, maxMPTokenAmount); BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(0)); } // Issuer fails trying to send fund after issuance was destroyed { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); mptAlice.authorize({.account = bob}); // alice destroys issuance mptAlice.destroy({.ownerCount = 0}); // alice tries to send bob fund after issuance is destroyed, should // fail. mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND); } // Non-existent issuance { Env env{*this, features}; env.fund(XRP(1'000), alice, bob); STAmount const mpt{MPTID{0}, 100}; env(pay(alice, bob, mpt), ter(tecOBJECT_NOT_FOUND)); } // Issuer fails trying to send to an account, which doesn't own MPT for // an issuance that was destroyed { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create({.ownerCount = 1, .holderCount = 0}); // alice destroys issuance mptAlice.destroy({.ownerCount = 0}); // alice tries to send bob who doesn't own the MPT after issuance is // destroyed, it should fail mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND); } // Issuers issues maximum amount of MPT to a holder, the holder should // be able to transfer the max amount to someone else { Env env{*this, features}; Account const alice("alice"); Account const carol("bob"); Account const bob("carol"); MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.maxAmt = 100, .ownerCount = 1, .flags = tfMPTCanTransfer}); mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); mptAlice.pay(alice, bob, 100); // transfer max amount to another holder mptAlice.pay(bob, carol, 100); } // Simple payment { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); // issuer to holder mptAlice.pay(alice, bob, 100); // holder to issuer mptAlice.pay(bob, alice, 100); // holder to holder mptAlice.pay(alice, bob, 100); mptAlice.pay(bob, carol, 50); } } void testDepositPreauth(FeatureBitset features) { using namespace test::jtx; Account const alice("alice"); // issuer Account const bob("bob"); // holder Account const diana("diana"); Account const dpIssuer("dpIssuer"); // holder char const credType[] = "abcde"; if (features[featureCredentials]) { testcase("DepositPreauth"); Env env(*this, features); env.fund(XRP(50000), diana, dpIssuer); env.close(); MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer}); env(pay(diana, bob, XRP(500))); env.close(); // bob creates an empty MPToken mptAlice.authorize({.account = bob}); // alice authorizes bob to hold funds mptAlice.authorize({.account = alice, .holder = bob}); // Bob require preauthorization env(fset(bob, asfDepositAuth)); env.close(); // alice try to send 100 MPT to bob, not authorized mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); env.close(); // Bob authorize alice env(deposit::auth(bob, alice)); env.close(); // alice sends 100 MPT to bob mptAlice.pay(alice, bob, 100); env.close(); // Create credentials env(credentials::create(alice, dpIssuer, credType)); env.close(); env(credentials::accept(alice, dpIssuer, credType)); env.close(); auto const jv = credentials::ledgerEntry(env, alice, dpIssuer, credType); std::string const credIdx = jv[jss::result][jss::index].asString(); // alice sends 100 MPT to bob with credentials which aren't required mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}}); env.close(); // Bob revoke authorization env(deposit::unauth(bob, alice)); env.close(); // alice try to send 100 MPT to bob, not authorized mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); env.close(); // alice sends 100 MPT to bob with credentials, not authorized mptAlice.pay(alice, bob, 100, tecNO_PERMISSION, {{credIdx}}); env.close(); // Bob authorize credentials env(deposit::authCredentials(bob, {{dpIssuer, credType}})); env.close(); // alice try to send 100 MPT to bob, not authorized mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); env.close(); // alice sends 100 MPT to bob with credentials mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}}); env.close(); } testcase("DepositPreauth disabled featureCredentials"); { Env env(*this, testable_amendments() - featureCredentials); std::string const credIdx = "D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6" "E2"; env.fund(XRP(50000), diana, dpIssuer); env.close(); MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer}); env(pay(diana, bob, XRP(500))); env.close(); // bob creates an empty MPToken mptAlice.authorize({.account = bob}); // alice authorizes bob to hold funds mptAlice.authorize({.account = alice, .holder = bob}); // Bob require preauthorization env(fset(bob, asfDepositAuth)); env.close(); // alice try to send 100 MPT to bob, not authorized mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); env.close(); // alice try to send 100 MPT to bob with credentials, amendment // disabled mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}}); env.close(); // Bob authorize alice env(deposit::auth(bob, alice)); env.close(); // alice sends 100 MPT to bob mptAlice.pay(alice, bob, 100); env.close(); // alice sends 100 MPT to bob with credentials, amendment disabled mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}}); env.close(); // Bob revoke authorization env(deposit::unauth(bob, alice)); env.close(); // alice try to send 100 MPT to bob mptAlice.pay(alice, bob, 100, tecNO_PERMISSION); env.close(); // alice sends 100 MPT to bob with credentials, amendment disabled mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}}); env.close(); } } void testMPTInvalidInTx(FeatureBitset features) { testcase("MPT Issue Invalid in Transaction"); using namespace test::jtx; // Validate that every transaction with an amount/issue field, // which doesn't support MPT, fails. // keyed by transaction + amount/issue field std::set txWithAmounts; for (auto const& format : TxFormats::getInstance()) { for (auto const& e : format.getSOTemplate()) { // Transaction has amount/issue fields. // Exclude pseudo-transaction SetFee. Don't consider // the Fee field since it's included in every transaction. if (e.supportMPT() == soeMPTNotSupported && e.sField().getName() != jss::Fee && format.getName() != jss::SetFee) { txWithAmounts.insert( format.getName() + e.sField().fieldName); break; } } } Account const alice("alice"); auto const USD = alice["USD"]; Account const carol("carol"); MPTIssue issue(makeMptID(1, alice)); STAmount mpt{issue, UINT64_C(100)}; auto const jvb = bridge(alice, USD, alice, USD); for (auto const& feature : {features, features - featureMPTokensV1}) { Env env{*this, feature}; env.fund(XRP(1'000), alice); env.fund(XRP(1'000), carol); auto test = [&](Json::Value const& jv, std::string const& mptField) { txWithAmounts.erase( jv[jss::TransactionType].asString() + mptField); // tx is signed auto jtx = env.jt(jv); Serializer s; jtx.stx->add(s); auto jrr = env.rpc("submit", strHex(s.slice())); BEAST_EXPECT( jrr[jss::result][jss::error] == "invalidTransaction"); // tx is unsigned Json::Value jv1; jv1[jss::secret] = alice.name(); jv1[jss::tx_json] = jv; jrr = env.rpc("json", "submit", to_string(jv1)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); jrr = env.rpc("json", "sign", to_string(jv1)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); }; auto toSFieldRef = [](SField const& field) { return std::ref(field); }; auto setMPTFields = [&](SField const& field, Json::Value& jv, bool withAmount = true) { jv[jss::Asset] = to_json(xrpIssue()); jv[jss::Asset2] = to_json(USD.issue()); if (withAmount) jv[field.fieldName] = USD(10).value().getJson(JsonOptions::none); if (field == sfAsset) jv[jss::Asset] = to_json(mpt.get()); else if (field == sfAsset2) jv[jss::Asset2] = to_json(mpt.get()); else jv[field.fieldName] = mpt.getJson(JsonOptions::none); }; // All transactions with sfAmount, which don't support MPT. // Transactions with amount fields, which can't be MPT. // Transactions with issue fields, which can't be MPT. // AMMCreate auto ammCreate = [&](SField const& field) { Json::Value jv; jv[jss::TransactionType] = jss::AMMCreate; jv[jss::Account] = alice.human(); jv[jss::Amount] = (field.fieldName == sfAmount.fieldName) ? mpt.getJson(JsonOptions::none) : "100000000"; jv[jss::Amount2] = (field.fieldName == sfAmount2.fieldName) ? mpt.getJson(JsonOptions::none) : "100000000"; jv[jss::TradingFee] = 0; test(jv, field.fieldName); }; ammCreate(sfAmount); ammCreate(sfAmount2); // AMMDeposit auto ammDeposit = [&](SField const& field) { Json::Value jv; jv[jss::TransactionType] = jss::AMMDeposit; jv[jss::Account] = alice.human(); jv[jss::Flags] = tfSingleAsset; setMPTFields(field, jv); test(jv, field.fieldName); }; for (SField const& field : {toSFieldRef(sfAmount), toSFieldRef(sfAmount2), toSFieldRef(sfEPrice), toSFieldRef(sfLPTokenOut), toSFieldRef(sfAsset), toSFieldRef(sfAsset2)}) ammDeposit(field); // AMMWithdraw auto ammWithdraw = [&](SField const& field) { Json::Value jv; jv[jss::TransactionType] = jss::AMMWithdraw; jv[jss::Account] = alice.human(); jv[jss::Flags] = tfSingleAsset; setMPTFields(field, jv); test(jv, field.fieldName); }; ammWithdraw(sfAmount); for (SField const& field : {toSFieldRef(sfAmount2), toSFieldRef(sfEPrice), toSFieldRef(sfLPTokenIn), toSFieldRef(sfAsset), toSFieldRef(sfAsset2)}) ammWithdraw(field); // AMMBid auto ammBid = [&](SField const& field) { Json::Value jv; jv[jss::TransactionType] = jss::AMMBid; jv[jss::Account] = alice.human(); setMPTFields(field, jv); test(jv, field.fieldName); }; for (SField const& field : {toSFieldRef(sfBidMin), toSFieldRef(sfBidMax), toSFieldRef(sfAsset), toSFieldRef(sfAsset2)}) ammBid(field); // AMMClawback auto ammClawback = [&](SField const& field) { Json::Value jv; jv[jss::TransactionType] = jss::AMMClawback; jv[jss::Account] = alice.human(); jv[jss::Holder] = carol.human(); setMPTFields(field, jv); test(jv, field.fieldName); }; for (SField const& field : {toSFieldRef(sfAmount), toSFieldRef(sfAsset), toSFieldRef(sfAsset2)}) ammClawback(field); // AMMDelete auto ammDelete = [&](SField const& field) { Json::Value jv; jv[jss::TransactionType] = jss::AMMDelete; jv[jss::Account] = alice.human(); setMPTFields(field, jv, false); test(jv, field.fieldName); }; ammDelete(sfAsset); ammDelete(sfAsset2); // AMMVote auto ammVote = [&](SField const& field) { Json::Value jv; jv[jss::TransactionType] = jss::AMMVote; jv[jss::Account] = alice.human(); jv[jss::TradingFee] = 100; setMPTFields(field, jv, false); test(jv, field.fieldName); }; ammVote(sfAsset); ammVote(sfAsset2); // CheckCash auto checkCash = [&](SField const& field) { Json::Value jv; jv[jss::TransactionType] = jss::CheckCash; jv[jss::Account] = alice.human(); jv[sfCheckID.fieldName] = to_string(uint256{1}); jv[field.fieldName] = mpt.getJson(JsonOptions::none); test(jv, field.fieldName); }; checkCash(sfAmount); checkCash(sfDeliverMin); // CheckCreate { Json::Value jv; jv[jss::TransactionType] = jss::CheckCreate; jv[jss::Account] = alice.human(); jv[jss::Destination] = carol.human(); jv[jss::SendMax] = mpt.getJson(JsonOptions::none); test(jv, jss::SendMax.c_str()); } // OfferCreate { Json::Value jv = offer(alice, USD(100), mpt); test(jv, jss::TakerPays.c_str()); jv = offer(alice, mpt, USD(100)); test(jv, jss::TakerGets.c_str()); } // PaymentChannelCreate { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; jv[jss::Account] = alice.human(); jv[jss::Destination] = carol.human(); jv[jss::SettleDelay] = 1; jv[sfPublicKey.fieldName] = strHex(alice.pk().slice()); jv[jss::Amount] = mpt.getJson(JsonOptions::none); test(jv, jss::Amount.c_str()); } // PaymentChannelFund { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelFund; jv[jss::Account] = alice.human(); jv[sfChannel.fieldName] = to_string(uint256{1}); jv[jss::Amount] = mpt.getJson(JsonOptions::none); test(jv, jss::Amount.c_str()); } // PaymentChannelClaim { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; jv[jss::Account] = alice.human(); jv[sfChannel.fieldName] = to_string(uint256{1}); jv[jss::Amount] = mpt.getJson(JsonOptions::none); test(jv, jss::Amount.c_str()); } // NFTokenCreateOffer { Json::Value jv; jv[jss::TransactionType] = jss::NFTokenCreateOffer; jv[jss::Account] = alice.human(); jv[sfNFTokenID.fieldName] = to_string(uint256{1}); jv[jss::Amount] = mpt.getJson(JsonOptions::none); test(jv, jss::Amount.c_str()); } // NFTokenAcceptOffer { Json::Value jv; jv[jss::TransactionType] = jss::NFTokenAcceptOffer; jv[jss::Account] = alice.human(); jv[sfNFTokenBrokerFee.fieldName] = mpt.getJson(JsonOptions::none); test(jv, sfNFTokenBrokerFee.fieldName); } // NFTokenMint { Json::Value jv; jv[jss::TransactionType] = jss::NFTokenMint; jv[jss::Account] = alice.human(); jv[sfNFTokenTaxon.fieldName] = 1; jv[jss::Amount] = mpt.getJson(JsonOptions::none); test(jv, jss::Amount.c_str()); } // TrustSet auto trustSet = [&](SField const& field) { Json::Value jv; jv[jss::TransactionType] = jss::TrustSet; jv[jss::Account] = alice.human(); jv[jss::Flags] = 0; jv[field.fieldName] = mpt.getJson(JsonOptions::none); test(jv, field.fieldName); }; trustSet(sfLimitAmount); trustSet(sfFee); // XChainCommit { Json::Value const jv = xchain_commit(alice, jvb, 1, mpt); test(jv, jss::Amount.c_str()); } // XChainClaim { Json::Value const jv = xchain_claim(alice, jvb, 1, mpt, alice); test(jv, jss::Amount.c_str()); } // XChainCreateClaimID { Json::Value const jv = xchain_create_claim_id(alice, jvb, mpt, alice); test(jv, sfSignatureReward.fieldName); } // XChainAddClaimAttestation { Json::Value const jv = claim_attestation( alice, jvb, alice, mpt, alice, true, 1, alice, signer(alice)); test(jv, jss::Amount.c_str()); } // XChainAddAccountCreateAttestation { Json::Value jv = create_account_attestation( alice, jvb, alice, mpt, XRP(10), alice, false, 1, alice, signer(alice)); for (auto const& field : {sfAmount.fieldName, sfSignatureReward.fieldName}) { jv[field] = mpt.getJson(JsonOptions::none); test(jv, field); } } // XChainAccountCreateCommit { Json::Value jv = sidechain_xchain_account_create( alice, jvb, alice, mpt, XRP(10)); for (auto const& field : {sfAmount.fieldName, sfSignatureReward.fieldName}) { jv[field] = mpt.getJson(JsonOptions::none); test(jv, field); } } // XChain[Create|Modify]Bridge auto bridgeTx = [&](Json::StaticString const& tt, STAmount const& rewardAmount, STAmount const& minAccountAmount, std::string const& field) { Json::Value jv; jv[jss::TransactionType] = tt; jv[jss::Account] = alice.human(); jv[sfXChainBridge.fieldName] = jvb; jv[sfSignatureReward.fieldName] = rewardAmount.getJson(JsonOptions::none); jv[sfMinAccountCreateAmount.fieldName] = minAccountAmount.getJson(JsonOptions::none); test(jv, field); }; auto reward = STAmount{sfSignatureReward, mpt}; auto minAmount = STAmount{sfMinAccountCreateAmount, USD(10)}; for (SField const& field : {std::ref(sfSignatureReward), std::ref(sfMinAccountCreateAmount)}) { bridgeTx( jss::XChainCreateBridge, reward, minAmount, field.fieldName); bridgeTx( jss::XChainModifyBridge, reward, minAmount, field.fieldName); reward = STAmount{sfSignatureReward, USD(10)}; minAmount = STAmount{sfMinAccountCreateAmount, mpt}; } } BEAST_EXPECT(txWithAmounts.empty()); } void testTxJsonMetaFields(FeatureBitset features) { // checks synthetically injected mptissuanceid from `tx` response testcase("Test synthetic fields from tx response"); using namespace test::jtx; Account const alice{"alice"}; auto cfg = envconfig(); cfg->FEES.reference_fee = 10; Env env{*this, std::move(cfg), features}; MPTTester mptAlice(env, alice); mptAlice.create(); std::string const txHash{ env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; BEAST_EXPECTS( txHash == "E11F0E0CA14219922B7881F060B9CEE67CFBC87E4049A441ED2AE348FF8FAC" "0E", txHash); Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta]; auto const id = meta[jss::mpt_issuance_id].asString(); // Expect mpt_issuance_id field BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id)); BEAST_EXPECT(id == to_string(mptAlice.issuanceID())); BEAST_EXPECTS( id == "00000004AE123A8556F3CF91154711376AFB0F894F832B3D", id); } void testClawbackValidation(FeatureBitset features) { testcase("MPT clawback validations"); using namespace test::jtx; // Make sure clawback cannot work when featureMPTokensV1 is disabled { Env env(*this, features - featureMPTokensV1); Account const alice{"alice"}; Account const bob{"bob"}; env.fund(XRP(1000), alice, bob); env.close(); auto const USD = alice["USD"]; auto const mpt = ripple::test::jtx::MPT( alice.name(), makeMptID(env.seq(alice), alice)); env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); env.close(); env(claw(alice, mpt(5)), ter(temDISABLED)); env.close(); env(claw(alice, mpt(5), bob), ter(temDISABLED)); env.close(); } // Test preflight { Env env(*this, features); Account const alice{"alice"}; Account const bob{"bob"}; env.fund(XRP(1000), alice, bob); env.close(); auto const USD = alice["USD"]; auto const mpt = ripple::test::jtx::MPT( alice.name(), makeMptID(env.seq(alice), alice)); // clawing back IOU from a MPT holder fails env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED)); env.close(); // clawing back MPT without specifying a holder fails env(claw(alice, mpt(5)), ter(temMALFORMED)); env.close(); // clawing back zero amount fails env(claw(alice, mpt(0), bob), ter(temBAD_AMOUNT)); env.close(); // alice can't claw back from herself env(claw(alice, mpt(5), alice), ter(temMALFORMED)); env.close(); // can't clawback negative amount env(claw(alice, mpt(-1), bob), ter(temBAD_AMOUNT)); env.close(); } // Preclaim - clawback fails when MPTCanClawback is disabled on issuance { Env env(*this, features); Account const alice{"alice"}; Account const bob{"bob"}; MPTTester mptAlice(env, alice, {.holders = {bob}}); // enable asfAllowTrustLineClawback for alice env(fset(alice, asfAllowTrustLineClawback)); env.close(); env.require(flags(alice, asfAllowTrustLineClawback)); // Create issuance without enabling clawback mptAlice.create({.ownerCount = 1, .holderCount = 0}); mptAlice.authorize({.account = bob}); mptAlice.pay(alice, bob, 100); // alice cannot clawback before she didn't enable MPTCanClawback // asfAllowTrustLineClawback has no effect mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); } // Preclaim - test various scenarios { Env env(*this, features); Account const alice{"alice"}; Account const bob{"bob"}; Account const carol{"carol"}; env.fund(XRP(1000), carol); env.close(); MPTTester mptAlice(env, alice, {.holders = {bob}}); auto const fakeMpt = ripple::test::jtx::MPT( alice.name(), makeMptID(env.seq(alice), alice)); // issuer tries to clawback MPT where issuance doesn't exist env(claw(alice, fakeMpt(5), bob), ter(tecOBJECT_NOT_FOUND)); env.close(); // alice creates issuance mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback}); // alice tries to clawback from someone who doesn't have MPToken mptAlice.claw(alice, bob, 1, tecOBJECT_NOT_FOUND); // bob creates a MPToken mptAlice.authorize({.account = bob}); // clawback fails because bob currently has a balance of zero mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); // carol fails tries to clawback from bob because he is not the // issuer mptAlice.claw(carol, bob, 1, tecNO_PERMISSION); } // clawback more than max amount // fails in the json parser before // transactor is called { Env env(*this, features); Account const alice{"alice"}; Account const bob{"bob"}; env.fund(XRP(1000), alice, bob); env.close(); auto const mpt = ripple::test::jtx::MPT( alice.name(), makeMptID(env.seq(alice), alice)); Json::Value jv = claw(alice, mpt(1), bob); jv[jss::Amount][jss::value] = to_string(maxMPTokenAmount + 1); Json::Value jv1; jv1[jss::secret] = alice.name(); jv1[jss::tx_json] = jv; auto const jrr = env.rpc("json", "submit", to_string(jv1)); BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams"); } } void testClawback(FeatureBitset features) { testcase("MPT Clawback"); using namespace test::jtx; { Env env(*this, features); Account const alice{"alice"}; Account const bob{"bob"}; MPTTester mptAlice(env, alice, {.holders = {bob}}); // alice creates issuance mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback}); // bob creates a MPToken mptAlice.authorize({.account = bob}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); mptAlice.claw(alice, bob, 1); mptAlice.claw(alice, bob, 1000); // clawback fails because bob currently has a balance of zero mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS); } // Test that globally locked funds can be clawed { Env env(*this, features); Account const alice{"alice"}; Account const bob{"bob"}; MPTTester mptAlice(env, alice, {.holders = {bob}}); // alice creates issuance mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanClawback}); // bob creates a MPToken mptAlice.authorize({.account = bob}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); mptAlice.set({.account = alice, .flags = tfMPTLock}); mptAlice.claw(alice, bob, 100); } // Test that individually locked funds can be clawed { Env env(*this, features); Account const alice{"alice"}; Account const bob{"bob"}; MPTTester mptAlice(env, alice, {.holders = {bob}}); // alice creates issuance mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanClawback}); // bob creates a MPToken mptAlice.authorize({.account = bob}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); mptAlice.claw(alice, bob, 100); } // Test that unauthorized funds can be clawed back { Env env(*this, features); Account const alice{"alice"}; Account const bob{"bob"}; MPTTester mptAlice(env, alice, {.holders = {bob}}); // alice creates issuance mptAlice.create( {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback | tfMPTRequireAuth}); // bob creates a MPToken mptAlice.authorize({.account = bob}); // alice authorizes bob mptAlice.authorize({.account = alice, .holder = bob}); // alice pays bob 100 tokens mptAlice.pay(alice, bob, 100); // alice unauthorizes bob mptAlice.authorize( {.account = alice, .holder = bob, .flags = tfMPTUnauthorize}); mptAlice.claw(alice, bob, 100); } } void testTokensEquality() { using namespace test::jtx; testcase("Tokens Equality"); Currency const cur1{to_currency("CU1")}; Currency const cur2{to_currency("CU2")}; Account const gw1{"gw1"}; Account const gw2{"gw2"}; MPTID const mpt1 = makeMptID(1, gw1); MPTID const mpt1a = makeMptID(1, gw1); MPTID const mpt2 = makeMptID(1, gw2); MPTID const mpt3 = makeMptID(2, gw2); Asset const assetCur1Gw1{Issue{cur1, gw1}}; Asset const assetCur1Gw1a{Issue{cur1, gw1}}; Asset const assetCur2Gw1{Issue{cur2, gw1}}; Asset const assetCur2Gw2{Issue{cur2, gw2}}; Asset const assetMpt1Gw1{mpt1}; Asset const assetMpt1Gw1a{mpt1a}; Asset const assetMpt1Gw2{mpt2}; Asset const assetMpt2Gw2{mpt3}; // Assets holding Issue // Currencies are equal regardless of the issuer BEAST_EXPECT(equalTokens(assetCur1Gw1, assetCur1Gw1a)); BEAST_EXPECT(equalTokens(assetCur2Gw1, assetCur2Gw2)); // Currencies are different regardless of whether the issuers // are the same or not BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw1)); BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw2)); // Assets holding MPTIssue // MPTIDs are the same if the sequence and the issuer are the same BEAST_EXPECT(equalTokens(assetMpt1Gw1, assetMpt1Gw1a)); // MPTIDs are different if sequence and the issuer don't match BEAST_EXPECT(!equalTokens(assetMpt1Gw1, assetMpt1Gw2)); BEAST_EXPECT(!equalTokens(assetMpt1Gw2, assetMpt2Gw2)); // Assets holding Issue and MPTIssue BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetMpt1Gw1)); BEAST_EXPECT(!equalTokens(assetMpt2Gw2, assetCur2Gw2)); } void testHelperFunctions() { using namespace test::jtx; Account const gw{"gw"}; Asset const asset1{makeMptID(1, gw)}; Asset const asset2{makeMptID(2, gw)}; Asset const asset3{makeMptID(3, gw)}; STAmount const amt1{asset1, 100}; STAmount const amt2{asset2, 100}; STAmount const amt3{asset3, 10'000}; { testcase("Test STAmount MPT arithmetics"); using namespace std::string_literals; STAmount res = multiply(amt1, amt2, asset3); BEAST_EXPECT(res == amt3); res = mulRound(amt1, amt2, asset3, true); BEAST_EXPECT(res == amt3); res = mulRoundStrict(amt1, amt2, asset3, true); BEAST_EXPECT(res == amt3); // overflow, any value > 3037000499ull STAmount mptOverflow{asset2, UINT64_C(3037000500)}; try { res = multiply(mptOverflow, mptOverflow, asset3); fail("should throw runtime exception 1"); } catch (std::runtime_error const& e) { BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what()); } // overflow, (v1 >> 32) * v2 > 2147483648ull mptOverflow = STAmount{asset2, UINT64_C(2147483648)}; uint64_t const mantissa = (2ull << 32) + 2; try { res = multiply(STAmount{asset1, mantissa}, mptOverflow, asset3); fail("should throw runtime exception 2"); } catch (std::runtime_error const& e) { BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what()); } } { testcase("Test MPTAmount arithmetics"); MPTAmount mptAmt1{100}; MPTAmount const mptAmt2{100}; BEAST_EXPECT((mptAmt1 += mptAmt2) == MPTAmount{200}); BEAST_EXPECT(mptAmt1 == 200); BEAST_EXPECT((mptAmt1 -= mptAmt2) == mptAmt1); BEAST_EXPECT(mptAmt1 == mptAmt2); BEAST_EXPECT(mptAmt1 == 100); BEAST_EXPECT(MPTAmount::minPositiveAmount() == MPTAmount{1}); } { testcase("Test MPTIssue from/to Json"); MPTIssue const issue1{asset1.get()}; Json::Value const jv = to_json(issue1); BEAST_EXPECT( jv[jss::mpt_issuance_id] == to_string(asset1.get())); BEAST_EXPECT(issue1 == mptIssueFromJson(jv)); } { testcase("Test Asset from/to Json"); Json::Value const jv = to_json(asset1); BEAST_EXPECT( jv[jss::mpt_issuance_id] == to_string(asset1.get())); BEAST_EXPECT( to_string(jv) == "{\"mpt_issuance_id\":" "\"00000001A407AF5856CCF3C42619DAA925813FC955C72983\"}"); BEAST_EXPECT(asset1 == assetFromJson(jv)); } } 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 = tmfMPTCanMutateMetadata | tmfMPTCanMutateCanLock | tmfMPTCanMutateTransferFee}); // 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 = tmfMPTCanMutateTransferFee | tmfMPTCanMutateMetadata}); 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 = { tmfMPTSetCanLock | tmfMPTClearCanLock, tmfMPTSetRequireAuth | tmfMPTClearRequireAuth, tmfMPTSetCanEscrow | tmfMPTClearCanEscrow, tmfMPTSetCanTrade | tmfMPTClearCanTrade, tmfMPTSetCanTransfer | tmfMPTClearCanTransfer, tmfMPTSetCanClawback | tmfMPTClearCanClawback, tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTClearCanTrade, tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanEscrow | tmfMPTClearCanClawback}; 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 = { tmfMPTSetCanLock, tmfMPTClearCanLock, tmfMPTSetRequireAuth, tmfMPTClearRequireAuth, tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, tmfMPTSetCanTrade, tmfMPTClearCanTrade, tmfMPTSetCanTransfer, tmfMPTClearCanTransfer, tmfMPTSetCanClawback, tmfMPTClearCanClawback}; 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 = tmfMPTCanMutateMetadata}); 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 = tmfMPTCanMutateTransferFee}); 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 = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); // Can not set non-zero transfer fee and clear MPTCanTransfer at the // same time mptAlice.set( {.account = alice, .mutableFlags = tmfMPTClearCanTransfer, .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 = tmfMPTClearCanTransfer, .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 = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); 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 = tmfMPTSetCanTransfer, .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 = tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateMetadata}); // Can not mutate transfer fee mptAlice.set( {.account = alice, .transferFee = 100, .err = tecNO_PERMISSION}); auto const invalidFlags = { tmfMPTSetCanLock, tmfMPTClearCanLock, tmfMPTSetRequireAuth, tmfMPTClearRequireAuth, tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, tmfMPTSetCanClawback, tmfMPTClearCanClawback}; // 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 = tmfMPTSetCanTrade}); mptAlice.set( {.account = alice, .mutableFlags = tmfMPTClearCanTrade}); // Can mutate MPTCanTransfer mptAlice.set( {.account = alice, .mutableFlags = tmfMPTSetCanTransfer}); mptAlice.set( {.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); // 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 = tmfMPTCanMutateMetadata}); std::vector 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 = tmfMPTCanMutateTransferFee}); for (std::uint16_t const fee : std::initializer_list{ 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( tmfMPTCanMutateCanLock, tfMPTCanLock, tmfMPTClearCanLock); testFlagToggle( tmfMPTCanMutateRequireAuth, tmfMPTSetRequireAuth, tmfMPTClearRequireAuth); testFlagToggle( tmfMPTCanMutateCanEscrow, tmfMPTSetCanEscrow, tmfMPTClearCanEscrow); testFlagToggle( tmfMPTCanMutateCanTrade, tmfMPTSetCanTrade, tmfMPTClearCanTrade); testFlagToggle( tmfMPTCanMutateCanTransfer, tmfMPTSetCanTransfer, tmfMPTClearCanTransfer); testFlagToggle( tmfMPTCanMutateCanClawback, tmfMPTSetCanClawback, tmfMPTClearCanClawback); } } 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 = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanTrade | tmfMPTCanMutateTransferFee}); 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 = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); mptAlice.set( {.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade}); mptAlice.set( {.account = alice, .mutableFlags = tmfMPTClearCanTrade}); 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 = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata}); 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 = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); mptAlice.set( {.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set( {.account = alice, .mutableFlags = tmfMPTSetCanClawback}); mptAlice.set( {.account = alice, .mutableFlags = tmfMPTClearCanClawback}); 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 = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata}); 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 = tmfMPTClearCanLock}); // 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 = tmfMPTSetCanLock}); // 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 = tmfMPTCanMutateRequireAuth}); 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 = tmfMPTClearRequireAuth}); // Can pay to bob mptAlice.pay(alice, bob, 1000); // Set RequireAuth again mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth}); // 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 = tmfMPTCanMutateCanEscrow}); 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 = tmfMPTSetCanEscrow}); 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 = tmfMPTClearCanEscrow}); 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 = tmfMPTCanMutateCanTransfer | tmfMPTCanMutateTransferFee}); 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 = tmfMPTSetCanTransfer, .transferFee = 100, .err = tecNO_PERMISSION}); // Alice sets MPTCanTransfer mptAlice.set( {.account = alice, .mutableFlags = tmfMPTSetCanTransfer}); // 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 = tmfMPTClearCanTransfer}); // 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 // tmfMPTCanMutateTransferFee is set. { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.transferFee = 100, .ownerCount = 1, .flags = tfMPTCanTransfer, .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); BEAST_EXPECT(mptAlice.checkTransferFee(100)); // Clear MPTCanTransfer and transfer fee is removed mptAlice.set( {.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); 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 = tmfMPTCanMutateCanClawback}); // 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 = tmfMPTSetCanClawback}); // Can clawback now mptAlice.claw(alice, bob, 1); // Clear MPTCanClawback mptAlice.set( {.account = alice, .mutableFlags = tmfMPTClearCanClawback}); // Can not clawback mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); } public: void run() override { using namespace test::jtx; FeatureBitset const all{testable_amendments()}; // MPTokenIssuanceCreate testCreateValidation(all - featureSingleAssetVault); testCreateValidation(all - featurePermissionedDomains); testCreateValidation(all); testCreateEnabled(all - featureSingleAssetVault); testCreateEnabled(all); // MPTokenIssuanceDestroy testDestroyValidation(all - featureSingleAssetVault); testDestroyValidation(all); testDestroyEnabled(all - featureSingleAssetVault); testDestroyEnabled(all); // MPTokenAuthorize testAuthorizeValidation(all - featureSingleAssetVault); testAuthorizeValidation(all); testAuthorizeEnabled(all - featureSingleAssetVault); testAuthorizeEnabled(all); // MPTokenIssuanceSet testSetValidation(all - featureSingleAssetVault - featureDynamicMPT); testSetValidation(all - featureSingleAssetVault); testSetValidation(all - featureDynamicMPT); testSetValidation(all - featurePermissionedDomains); testSetValidation(all); testSetEnabled(all - featureSingleAssetVault); testSetEnabled(all); // MPT clawback testClawbackValidation(all); testClawback(all); // Test Direct Payment testPayment(all); testDepositPreauth(all); testDepositPreauth(all - featureCredentials); // Test MPT Amount is invalid in Tx, which don't support MPT testMPTInvalidInTx(all); // Test parsed MPTokenIssuanceID in API response metadata testTxJsonMetaFields(all); // Test tokens equality testTokensEquality(); // Test helpers testHelperFunctions(); // Dynamic MPT testInvalidCreateDynamic(all); testInvalidSetDynamic(all); testMutateMPT(all); testMutateCanLock(all); testMutateRequireAuth(all); testMutateCanEscrow(all); testMutateCanTransfer(all); testMutateCanClawback(all); } }; BEAST_DEFINE_TESTSUITE_PRIO(MPToken, app, ripple, 2); } // namespace test } // namespace ripple