diff --git a/src/ripple/app/tx/impl/Payment.cpp b/src/ripple/app/tx/impl/Payment.cpp index 51220f3dabd..bc4eb4d410b 100644 --- a/src/ripple/app/tx/impl/Payment.cpp +++ b/src/ripple/app/tx/impl/Payment.cpp @@ -453,6 +453,11 @@ Payment::doApply() ter != tesSUCCESS) return ter; + if (auto const ter = + canTransfer(view(), saDstAmount.mptIssue(), account_, uDstAccountID); + ter != tesSUCCESS) + return ter; + auto const& mpt = saDstAmount.mptIssue(); auto const& issuer = mpt.account(); // If globally/individually locked then diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index c0356bbb310..a18c347e672 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -508,6 +508,17 @@ requireAuth( MPTIssue const& mpt, AccountID const& account); +/** Check if the destination account is allowed + * to receive MPT. Return tecNO_AUTH if it doesn't + * and tesSUCCESS otherwise. + */ +[[nodiscard]] TER +canTransfer( + ReadView const& view, + MPTIssue const& mpt, + AccountID const& from, + AccountID const& to); + /** Deleter function prototype. Returns the status of the entry deletion * (if should not be skipped) and if the entry should be skipped. The status * is always tesSUCCESS if the entry should be skipped. diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index 50e58ab86e3..9859639bd74 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -1698,6 +1698,19 @@ requireAuth(ReadView const& view, MPTIssue const& mpt, AccountID const& account) return tesSUCCESS; } +TER +canTransfer(ReadView const& view, MPTIssue const& mpt, AccountID const& from, AccountID const& to) +{ + auto const mptID = keylet::mptIssuance(mpt.mpt()); + if (auto const sle = view.read(mptID); + sle && !(sle->getFieldU32(sfFlags) & lsfMPTCanTransfer)) + { + if(from != (*sle)[sfIssuer] && to != (*sle)[sfIssuer]) + return TER{tecNO_AUTH}; + } + return tesSUCCESS; +} + TER cleanupOnAccountDelete( ApplyView& view, diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index ad814a1fd61..5760b4735bf 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -619,7 +619,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); - mptAlice.create({.ownerCount = 1, .holderCount = 0}); + mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); // env(mpt::authorize(alice, id.key, std::nullopt)); // env.close(); @@ -646,7 +646,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob}}); mptAlice.create( - {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth}); + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer}); mptAlice.authorize({.account = &bob}); @@ -661,7 +661,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob}}); mptAlice.create( - {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth}); + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer}); // bob creates an empty MPToken mptAlice.authorize({.account = &bob}); @@ -687,7 +687,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); - mptAlice.create({.ownerCount = 1}); + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); mptAlice.authorize({.account = &bob}); mptAlice.authorize({.account = &carol}); @@ -707,7 +707,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob, &carol}}); - mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock}); + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanTransfer}); mptAlice.authorize({.account = &bob}); mptAlice.authorize({.account = &carol}); @@ -745,7 +745,7 @@ class MPToken_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {&bob}}); - mptAlice.create({.maxAmt = 100, .ownerCount = 1, .holderCount = 0}); + mptAlice.create({.maxAmt = 100, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); mptAlice.authorize({.account = &bob}); @@ -806,6 +806,36 @@ class MPToken_test : public beast::unit_test::suite // Payment between the holders. The sender pays 10% transfer fee. mptAlice.pay(bob, carol, 100); } + + // Test that 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); + } } void