#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { class CheckMPT_test : public beast::unit_test::Suite { // Helper function that returns the Checks on an account. static std::vector> checksOnAccount(test::jtx::Env& env, test::jtx::Account account) { std::vector> result; forEachItem(*env.current(), account, [&result](std::shared_ptr const& sle) { if (sle && sle->getType() == ltCHECK) result.push_back(sle); }); return result; } // Helper function that verifies the expected DeliveredAmount is present. // // NOTE: the function _infers_ the transaction to operate on by calling // env.tx(), which returns the result from the most recent transaction. void verifyDeliveredAmount(test::jtx::Env& env, STAmount const& amount) { // Get the hash for the most recent transaction. std::string const txHash{ env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()}; // Verify DeliveredAmount and delivered_amount metadata are correct. env.close(); json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta]; // Expect there to be a DeliveredAmount field. if (!BEAST_EXPECT(meta.isMember(sfDeliveredAmount.jsonName))) return; // DeliveredAmount and delivered_amount should both be present and // equal amount. BEAST_EXPECT(meta[sfDeliveredAmount.jsonName] == amount.getJson(JsonOptions::Values::None)); BEAST_EXPECT(meta[jss::delivered_amount] == amount.getJson(JsonOptions::Values::None)); } void testCreateValid(FeatureBitset features) { // Explore many of the valid ways to create a check. testcase("Create valid"); using namespace test::jtx; Account const gw{"gateway"}; Account const alice{"alice"}; Account const bob{"bob"}; Env env{*this, features}; STAmount const startBalance{XRP(1'000).value()}; env.fund(startBalance, gw, alice, bob); MPT const usd = MPTTester({.env = env, .issuer = gw}); // Note that no MPToken has been set up for alice, but alice can // still write a check for USD. You don't have to have the funds // necessary to cover a check in order to write a check. auto writeTwoChecks = [&env, &usd, this](Account const& from, Account const& to) { std::uint32_t const fromOwnerCount{ownerCount(env, from)}; std::uint32_t const toOwnerCount{ownerCount(env, to)}; std::size_t const fromCkCount{checksOnAccount(env, from).size()}; std::size_t const toCkCount{checksOnAccount(env, to).size()}; env(check::create(from, to, XRP(2000))); env.close(); env(check::create(from, to, usd(50))); env.close(); BEAST_EXPECT(checksOnAccount(env, from).size() == fromCkCount + 2); BEAST_EXPECT(checksOnAccount(env, to).size() == toCkCount + 2); env.require(Owners(from, fromOwnerCount + 2)); env.require(Owners(to, to == from ? fromOwnerCount + 2 : toOwnerCount)); }; // from to writeTwoChecks(alice, bob); writeTwoChecks(gw, alice); writeTwoChecks(alice, gw); // Now try adding the various optional fields. There's no // expected interaction between these optional fields; other than // the expiration, they are just plopped into the ledger. So I'm // not looking at interactions. using namespace std::chrono_literals; std::size_t const aliceCount{checksOnAccount(env, alice).size()}; std::size_t const bobCount{checksOnAccount(env, bob).size()}; env(check::create(alice, bob, usd(50)), Expiration(env.now() + 1s)); env.close(); env(check::create(alice, bob, usd(50)), SourceTag(2)); env.close(); env(check::create(alice, bob, usd(50)), DestTag(3)); env.close(); env(check::create(alice, bob, usd(50)), InvoiceId(uint256{4})); env.close(); env(check::create(alice, bob, usd(50)), Expiration(env.now() + 1s), SourceTag(12), DestTag(13), InvoiceId(uint256{4})); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == aliceCount + 5); BEAST_EXPECT(checksOnAccount(env, bob).size() == bobCount + 5); // Use a regular key and also multisign to create a check. Account const alie{"alie", KeyType::Ed25519}; env(regkey(alice, alie)); env.close(); Account const bogie{"bogie", KeyType::Secp256k1}; Account const demon{"demon", KeyType::Ed25519}; env(signers(alice, 2, {{bogie, 1}, {demon, 1}}), Sig(alie)); env.close(); // alice uses her regular key to create a check. env(check::create(alice, bob, usd(50)), Sig(alie)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == aliceCount + 6); BEAST_EXPECT(checksOnAccount(env, bob).size() == bobCount + 6); // alice uses multisigning to create a check. XRPAmount const baseFeeDrops{env.current()->fees().base}; env(check::create(alice, bob, usd(50)), Msig(bogie, demon), Fee(3 * baseFeeDrops)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == aliceCount + 7); BEAST_EXPECT(checksOnAccount(env, bob).size() == bobCount + 7); } void testCreateDisallowIncoming(FeatureBitset features) { testcase("Create valid with disallow incoming"); using namespace test::jtx; Account const gw{"gateway"}; Account const alice{"alice"}; Account const bob{"bob"}; Env env{*this, features}; STAmount const startBalance{XRP(1'000).value()}; env.fund(startBalance, gw, alice, bob); MPT const usd = MPTTester({.env = env, .issuer = gw}); /* * Attempt to create two checks from `from` to `to` and * require they both result in error/success code `expected` */ auto writeTwoChecksDI = [&env, &usd, this]( Account const& from, Account const& to, TER expected) { std::uint32_t const fromOwnerCount{ownerCount(env, from)}; std::uint32_t const toOwnerCount{ownerCount(env, to)}; std::size_t const fromCkCount{checksOnAccount(env, from).size()}; std::size_t const toCkCount{checksOnAccount(env, to).size()}; env(check::create(from, to, XRP(2000)), Ter(expected)); env.close(); env(check::create(from, to, usd(50)), Ter(expected)); env.close(); if (expected == tesSUCCESS) { BEAST_EXPECT(checksOnAccount(env, from).size() == fromCkCount + 2); BEAST_EXPECT(checksOnAccount(env, to).size() == toCkCount + 2); env.require(Owners(from, fromOwnerCount + 2)); env.require(Owners(to, to == from ? fromOwnerCount + 2 : toOwnerCount)); return; } BEAST_EXPECT(checksOnAccount(env, from).size() == fromCkCount); BEAST_EXPECT(checksOnAccount(env, to).size() == toCkCount); env.require(Owners(from, fromOwnerCount)); env.require(Owners(to, to == from ? fromOwnerCount : toOwnerCount)); }; // enable the DisallowIncoming flag on both bob and alice env(fset(bob, asfDisallowIncomingCheck)); env(fset(alice, asfDisallowIncomingCheck)); env.close(); // both alice and bob can't receive checks writeTwoChecksDI(alice, bob, tecNO_PERMISSION); writeTwoChecksDI(gw, alice, tecNO_PERMISSION); // remove the flag from alice but not from bob env(fclear(alice, asfDisallowIncomingCheck)); env.close(); // now bob can send alice a cheque but not visa-versa writeTwoChecksDI(bob, alice, tesSUCCESS); writeTwoChecksDI(alice, bob, tecNO_PERMISSION); // remove bob's flag too env(fclear(bob, asfDisallowIncomingCheck)); env.close(); // now they can send checks freely writeTwoChecksDI(bob, alice, tesSUCCESS); writeTwoChecksDI(alice, bob, tesSUCCESS); } void testCreateInvalid(FeatureBitset features) { // Explore many of the invalid ways to create a check. testcase("Create invalid"); using namespace test::jtx; Account const gw1{"gateway1"}; Account const gwF{"gatewayFrozen"}; Account const alice{"alice"}; Account const bob{"bob"}; Env env{*this, features}; STAmount const startBalance{XRP(1'000).value()}; env.fund(startBalance, gw1, gwF, alice, bob); auto usdm = MPTTester({.env = env, .issuer = gw1, .flags = kMptDexFlags | tfMPTCanLock}); MPT const usd = usdm; // Bad fee. env(check::create(alice, bob, usd(50)), Fee(drops(-10)), Ter(temBAD_FEE)); env.close(); // Bad flags. env(check::create(alice, bob, usd(50)), Txflags(tfImmediateOrCancel), Ter(temINVALID_FLAG)); env.close(); // Check to self. env(check::create(alice, alice, XRP(10)), Ter(temREDUNDANT)); env.close(); // Bad amount. env(check::create(alice, bob, drops(-1)), Ter(temBAD_AMOUNT)); env.close(); env(check::create(alice, bob, drops(0)), Ter(temBAD_AMOUNT)); env.close(); env(check::create(alice, bob, drops(1))); env.close(); env(check::create(alice, bob, usd(-1)), Ter(temBAD_AMOUNT)); env.close(); env(check::create(alice, bob, usd(0)), Ter(temBAD_AMOUNT)); env.close(); env(check::create(alice, bob, usd(1))); env.close(); { MPT const bad(makeMptID(0, xrpAccount())); env(check::create(alice, bob, bad(2)), Ter(temBAD_CURRENCY)); env.close(); } // Bad expiration. env(check::create(alice, bob, usd(50)), Expiration(NetClock::time_point{}), Ter(temBAD_EXPIRATION)); env.close(); // Destination does not exist. Account const bogie{"bogie"}; env(check::create(alice, bogie, usd(50)), Ter(tecNO_DST)); env.close(); // Require destination tag. env(fset(bob, asfRequireDest)); env.close(); env(check::create(alice, bob, usd(50)), Ter(tecDST_TAG_NEEDED)); env.close(); env(check::create(alice, bob, usd(50)), DestTag(11)); env.close(); env(fclear(bob, asfRequireDest)); env.close(); { // Globally frozen asset. env.close(); auto usfm = MPTTester({.env = env, .issuer = gwF, .flags = kMptDexFlags | tfMPTCanLock}); MPT const usf = usfm; usfm.set({.flags = tfMPTLock}); env(check::create(alice, bob, usf(50)), Ter(tecLOCKED)); env.close(); usfm.set({.flags = tfMPTUnlock}); env(check::create(alice, bob, usf(50))); env.close(); } { // Frozen MPT. Check creation should be similar to payment // behavior in the face of locked MPT. usdm.authorizeHolders({alice, bob}); env(pay(gw1, alice, usd(25))); env(pay(gw1, bob, usd(25))); env.close(); usdm.set({.holder = alice, .flags = tfMPTLock}); // Setting MPT locked prevents alice from // creating a check for USD ore receiving a check. This is different // from IOU where alice can receive checks from bob or gw. env.close(); env(check::create(alice, bob, usd(50)), Ter(tecLOCKED)); env.close(); // Note that IOU returns tecPATH_DRY in this case. // IOU's internal error is terNO_LINE, which is // considered ter re-triable and changed to tecPATH_DRY. env(pay(alice, bob, usd(1)), Ter(tecPATH_DRY)); env.close(); env(check::create(bob, alice, usd(50)), Ter(tecLOCKED)); env.close(); env(pay(bob, alice, usd(1)), Ter(tecPATH_DRY)); env.close(); env(check::create(gw1, alice, usd(50)), Ter(tecLOCKED)); env.close(); env(pay(gw1, alice, usd(1))); env.close(); // Clear that lock. Now check creation works. usdm.set({.holder = alice, .flags = tfMPTUnlock}); env(check::create(alice, bob, usd(50))); env.close(); env(check::create(bob, alice, usd(50))); env.close(); env(check::create(gw1, alice, usd(50))); env.close(); } // Expired expiration. env(check::create(alice, bob, usd(50)), Expiration(env.now()), Ter(tecEXPIRED)); env.close(); using namespace std::chrono_literals; env(check::create(alice, bob, usd(50)), Expiration(env.now() + 1s)); env.close(); // Insufficient reserve. Account const cheri{"cheri"}; env.fund(env.current()->fees().accountReserve(1) - drops(1), cheri); env(check::create(cheri, bob, usd(50)), Fee(drops(env.current()->fees().base)), Ter(tecINSUFFICIENT_RESERVE)); env.close(); env(pay(bob, cheri, drops(env.current()->fees().base + 1))); env.close(); env(check::create(cheri, bob, usd(50))); env.close(); } void testCashMPT(FeatureBitset features) { // Explore many of the valid ways to cash a check for an MPT. testcase("Cash MPT"); using namespace test::jtx; Account const gw{"gateway"}; Account const alice{"alice"}; Account const bob{"bob"}; { // Simple MPT check cashed with Amount (with failures). Env env{*this, features}; env.fund(XRP(1'000), gw, alice, bob); MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 105}); // alice writes the check before she gets the funds. uint256 const chkId1{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(100))); env.close(); // bob attempts to cash the check. Should fail. env(check::cash(bob, chkId1, usd(100)), Ter(tecPATH_PARTIAL)); env.close(); // alice gets almost enough funds. bob tries and fails again. env(pay(gw, alice, usd(95))); env.close(); env(check::cash(bob, chkId1, usd(100)), Ter(tecPATH_PARTIAL)); env.close(); // alice gets the last of the necessary funds. env(pay(gw, alice, usd(5))); env.close(); // bob for more than the check's SendMax. env.close(); env(check::cash(bob, chkId1, usd(105)), Ter(tecPATH_PARTIAL)); env.close(); // bob asks for exactly the check amount and the check clears. // MPT is authorized automatically env(check::cash(bob, chkId1, usd(100))); env.close(); env.require(Balance(alice, usd(0))); env.require(Balance(bob, usd(100))); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(checksOnAccount(env, bob).empty()); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); // bob tries to cash the same check again, which fails. env(check::cash(bob, chkId1, usd(100)), Ter(tecNO_ENTRY)); env.close(); // bob pays alice USD(70) so he can try another case. env(pay(bob, alice, usd(70))); env.close(); uint256 const chkId2{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(70))); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); // bob cashes the check for less than the face amount. That works, // consumes the check, and bob receives as much as he asked for. env(check::cash(bob, chkId2, usd(50))); env.close(); env.require(Balance(alice, usd(20))); env.require(Balance(bob, usd(80))); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(checksOnAccount(env, bob).empty()); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); // alice writes two checks for USD(20), although she only has // USD(20). uint256 const chkId3{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(20))); env.close(); uint256 const chkId4{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(20))); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 2); BEAST_EXPECT(checksOnAccount(env, bob).size() == 2); // bob cashes the second check for the face amount. env(check::cash(bob, chkId4, usd(20))); env.close(); env.require(Balance(alice, usd(0))); env.require(Balance(bob, usd(100))); BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == 2); BEAST_EXPECT(ownerCount(env, bob) == 1); // bob is not allowed to cash the last check for USD(0), he must // use check::cancel instead. env(check::cash(bob, chkId3, usd(0)), Ter(temBAD_AMOUNT)); env.close(); env.require(Balance(alice, usd(0))); env.require(Balance(bob, usd(100))); BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == 2); BEAST_EXPECT(ownerCount(env, bob) == 1); { // Unlike IOU, cashing a check exceeding the MPT limit doesn't // work. Show that at work. // // MPT limit is USD(105). Show that // neither a payment to bob or caching can exceed that limit. // Payment of 200 USD fails. env(pay(gw, bob, usd(200)), Ter(tecPATH_PARTIAL)); env.close(); uint256 const chkId20{getCheckIndex(gw, env.seq(gw))}; env(check::create(gw, bob, usd(200))); env.close(); // Cashing a check for 200 USD fails. env(check::cash(bob, chkId20, usd(200)), Ter(tecPATH_PARTIAL)); env.close(); env.require(Balance(bob, usd(100))); // Clean up this most recent experiment so the rest of the // tests work. env(pay(bob, gw, usd(100))); env(check::cancel(bob, chkId20)); } // ... so bob cancels alice's remaining check. env(check::cancel(bob, chkId3)); env.close(); env.require(Balance(alice, usd(0))); env.require(Balance(bob, usd(0))); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(checksOnAccount(env, bob).empty()); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); } { // Simple MPT check cashed with DeliverMin (with failures). Env env{*this, features}; env.fund(XRP(1'000), gw, alice, bob); MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 20}); env(pay(gw, alice, usd(8))); env.close(); // alice creates several checks ahead of time. uint256 const chkId9{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(9))); env.close(); uint256 const chkId8{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(8))); env.close(); uint256 const chkId7{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(7))); env.close(); uint256 const chkId6{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(6))); env.close(); // bob attempts to cash a check for the amount on the check. // Should fail, since alice doesn't have the funds. env(check::cash(bob, chkId9, check::DeliverMin(usd(9))), Ter(tecPATH_PARTIAL)); env.close(); // bob sets a DeliverMin of 7 and gets all that alice has. env(check::cash(bob, chkId9, check::DeliverMin(usd(7)))); verifyDeliveredAmount(env, usd(8)); env.require(Balance(alice, usd(0))); env.require(Balance(bob, usd(8))); BEAST_EXPECT(checksOnAccount(env, alice).size() == 3); BEAST_EXPECT(checksOnAccount(env, bob).size() == 3); BEAST_EXPECT(ownerCount(env, alice) == 4); BEAST_EXPECT(ownerCount(env, bob) == 1); // bob pays alice USD(7) so he can use another check. env(pay(bob, alice, usd(7))); env.close(); // Using DeliverMin for the SendMax value of the check (and no // transfer fees) should work just like setting Amount. env(check::cash(bob, chkId7, check::DeliverMin(usd(7)))); verifyDeliveredAmount(env, usd(7)); env.require(Balance(alice, usd(0))); env.require(Balance(bob, usd(8))); BEAST_EXPECT(checksOnAccount(env, alice).size() == 2); BEAST_EXPECT(checksOnAccount(env, bob).size() == 2); BEAST_EXPECT(ownerCount(env, alice) == 3); BEAST_EXPECT(ownerCount(env, bob) == 1); // bob pays alice USD(8) so he can use the last two checks. env(pay(bob, alice, usd(8))); env.close(); // alice has USD(8). If bob uses the check for USD(6) and uses a // DeliverMin of 4, he should get the SendMax value of the check. env(check::cash(bob, chkId6, check::DeliverMin(usd(4)))); verifyDeliveredAmount(env, usd(6)); env.require(Balance(alice, usd(2))); env.require(Balance(bob, usd(6))); BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == 2); BEAST_EXPECT(ownerCount(env, bob) == 1); // bob cashes the last remaining check setting a DeliverMin. // of exactly alice's remaining USD. env(check::cash(bob, chkId8, check::DeliverMin(usd(2)))); verifyDeliveredAmount(env, usd(2)); env.require(Balance(alice, usd(0))); env.require(Balance(bob, usd(8))); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(checksOnAccount(env, bob).empty()); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); } { // Examine the effects of the asfRequireAuth flag. Env env(*this, features); env.fund(XRP(1000), gw, alice, bob); auto usdm = MPTTester( {.env = env, .issuer = gw, .holders = {alice}, .flags = kMptDexFlags | tfMPTRequireAuth, .maxAmt = 20}); MPT const usd = usdm; usdm.authorize({.holder = alice}); env.close(); env(pay(gw, alice, usd(8))); env.close(); // alice writes a check to bob for USD. bob can't cash it // because he is not authorized to hold gw["USD"]. uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(7))); env.close(); env(check::cash(bob, chkId, usd(7)), Ter(tecNO_AUTH)); env.close(); // Now give bob MPT for USD. bob still can't cash the // check because he is not authorized. usdm.authorize({.account = bob}); env.close(); env(check::cash(bob, chkId, usd(7)), Ter(tecNO_AUTH)); env.close(); // bob gets authorization to hold USD. usdm.authorize({.holder = bob}); env.close(); env(check::cash(bob, chkId, check::DeliverMin(usd(4)))); STAmount const bobGot = usd(7); verifyDeliveredAmount(env, bobGot); env.require(Balance(alice, usd(8) - bobGot)); env.require(Balance(bob, bobGot)); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(checksOnAccount(env, bob).empty()); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); } { Env env{*this, features}; env.fund(XRP(1'000), gw, alice, bob); MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 20}); // alice creates her checks ahead of time. uint256 const chkId1{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(1))); env.close(); uint256 const chkId2{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(2))); env.close(); env(pay(gw, alice, usd(8))); env.close(); // Give bob a regular key and signers Account const bobby{"bobby", KeyType::Secp256k1}; env(regkey(bob, bobby)); env.close(); Account const bogie{"bogie", KeyType::Secp256k1}; Account const demon{"demon", KeyType::Ed25519}; env(signers(bob, 2, {{bogie, 1}, {demon, 1}}), Sig(bobby)); env.close(); int const signersCount = 1; BEAST_EXPECT(ownerCount(env, bob) == signersCount + 1); // bob uses his regular key to cash a check. env(check::cash(bob, chkId1, (usd(1))), Sig(bobby)); env.close(); env.require(Balance(alice, usd(7))); env.require(Balance(bob, usd(1))); BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == 2); BEAST_EXPECT(ownerCount(env, bob) == signersCount + 1); // bob uses multisigning to cash a check. XRPAmount const baseFeeDrops{env.current()->fees().base}; env(check::cash(bob, chkId2, (usd(2))), Msig(bogie, demon), Fee(3 * baseFeeDrops)); env.close(); env.require(Balance(alice, usd(5))); env.require(Balance(bob, usd(3))); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(checksOnAccount(env, bob).empty()); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == signersCount + 1); } } void testCashXferFee(FeatureBitset features) { // Look at behavior when the issuer charges a transfer fee. testcase("Cash with transfer fee"); using namespace test::jtx; Account const gw{"gateway"}; Account const alice{"alice"}; Account const bob{"bob"}; Env env{*this, features}; env.fund(XRP(1'000), gw, alice, bob); // Set gw's transfer rate and see the consequences when cashing a check. MPT const usd = MPTTester( {.env = env, .issuer = gw, .holders = {alice, bob}, .transferFee = 25'000, .maxAmt = 1'000}); env.close(); env(pay(gw, alice, usd(1'000))); env.close(); // alice writes a check with a SendMax of USD(125). The most bob // can get is USD(100) because of the transfer rate. uint256 const chkId125{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(125))); env.close(); // alice writes another check that won't get cashed until the transfer // rate changes so we can see the rate applies when the check is // cashed, not when it is created. #if 0 uint256 const chkId120{getCheckIndex(alice, env.Seq(alice))}; env(check::create(alice, bob, USD(120))); env.close(); #endif // bob attempts to cash the check for face value. Should fail. env(check::cash(bob, chkId125, usd(125)), Ter(tecPATH_PARTIAL)); env.close(); env(check::cash(bob, chkId125, check::DeliverMin(usd(101))), Ter(tecPATH_PARTIAL)); env.close(); // bob decides that he'll accept anything USD(75) or up. // He gets USD(100). env(check::cash(bob, chkId125, check::DeliverMin(usd(75)))); verifyDeliveredAmount(env, usd(100)); env.require(Balance(alice, usd(1'000 - 125))); env.require(Balance(bob, usd(0 + 100))); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(checksOnAccount(env, bob).empty()); #if 0 // Adjust gw's rate... env(rate(gw, 1.2)); env.close(); // bob cashes the second check for less than the face value. The new // rate applies to the actual value transferred. env(check::cash(bob, chkId120, USD(50))); env.close(); env.Require(Balance(alice, USD(1000 - 125 - 60))); env.Require(Balance(bob, USD(0 + 100 + 50))); BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); #endif } void testCashInvalid(FeatureBitset features) { // Explore many of the ways to fail at cashing a check. testcase("Cash invalid"); using namespace test::jtx; Account const gw{"gateway"}; Account const alice{"alice"}; Account const bob{"bob"}; Account const zoe{"zoe"}; std::int64_t maxAmt{20}; Env env(*this, features); env.fund(XRP(1000), gw, alice, bob, zoe); auto usdm = MPTTester( {.env = env, .issuer = gw, .holders = {alice}, .flags = kMptDexFlags | tfMPTCanLock, .maxAmt = maxAmt}); MPT const usd = usdm; env(pay(gw, alice, usd(20))); env.close(); usdm.authorize({.account = bob}); // bob tries to cash a non-existent check from alice. { uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; env(check::cash(bob, chkId, usd(20)), Ter(tecNO_ENTRY)); env.close(); } // alice creates her checks ahead of time. uint256 const chkIdU{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(20))); env.close(); uint256 const chkIdX{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10))); env.close(); using namespace std::chrono_literals; uint256 const chkIdExp{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10)), Expiration(env.now() + 1s)); env.close(); uint256 const chkIdFroz1{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(1))); env.close(); uint256 const chkIdFroz2{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(2))); env.close(); uint256 const chkIdFroz3{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(3))); env.close(); uint256 const chkIdNoDest1{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(1))); env.close(); uint256 const chkIdHasDest2{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(2)), DestTag(7)); env.close(); // Same set of failing cases for both MPT and XRP check cashing. auto failingCases = [&env, &gw, &alice, &bob]( uint256 const& chkId, STAmount const& amount) { // Bad fee. env(check::cash(bob, chkId, amount), Fee(drops(-10)), Ter(temBAD_FEE)); env.close(); // Bad flags. env(check::cash(bob, chkId, amount), Txflags(tfImmediateOrCancel), Ter(temINVALID_FLAG)); env.close(); // Missing both Amount and DeliverMin. { json::Value tx{check::cash(bob, chkId, amount)}; tx.removeMember(sfAmount.jsonName); env(tx, Ter(temMALFORMED)); env.close(); } // Both Amount and DeliverMin present. { json::Value tx{check::cash(bob, chkId, amount)}; tx[sfDeliverMin.jsonName] = amount.getJson(JsonOptions::Values::None); env(tx, Ter(temMALFORMED)); env.close(); } // Negative or zero amount. { STAmount neg{amount}; neg.negate(); env(check::cash(bob, chkId, neg), Ter(temBAD_AMOUNT)); env.close(); env(check::cash(bob, chkId, amount.zeroed()), Ter(temBAD_AMOUNT)); env.close(); } // Bad currency. if (!amount.native()) { Issue const badIssue{badCurrency(), amount.getIssuer()}; STAmount badAmount{amount}; badAmount.setIssue(Issue{badCurrency(), amount.getIssuer()}); env(check::cash(bob, chkId, badAmount), Ter(temBAD_CURRENCY)); env.close(); } // Not destination cashing check. env(check::cash(alice, chkId, amount), Ter(tecNO_PERMISSION)); env.close(); env(check::cash(gw, chkId, amount), Ter(tecNO_PERMISSION)); env.close(); // Currency mismatch. { MPT const eur = MPTTester({.env = env, .issuer = gw}); STAmount const badAmount{eur, amount}; env(check::cash(bob, chkId, badAmount), Ter(temMALFORMED)); env.close(); } // Issuer mismatch. // Every MPT is unique. There is no USD MPT with different issuers. // Amount bigger than SendMax. env(check::cash(bob, chkId, amount + amount), Ter(tecPATH_PARTIAL)); env.close(); // DeliverMin bigger than SendMax. env(check::cash(bob, chkId, check::DeliverMin(amount + amount)), Ter(tecPATH_PARTIAL)); env.close(); }; failingCases(chkIdX, XRP(10)); failingCases(chkIdU, usd(20)); // Verify that those two checks really were cashable. env(check::cash(bob, chkIdU, usd(20))); env.close(); env(check::cash(bob, chkIdX, check::DeliverMin(XRP(10)))); verifyDeliveredAmount(env, XRP(10)); // Try to cash an expired check. env(check::cash(bob, chkIdExp, XRP(10)), Ter(tecEXPIRED)); env.close(); // Cancel the expired check. Anyone can cancel an expired check. env(check::cancel(zoe, chkIdExp)); env.close(); // Can we cash a check with frozen MPT? { env(pay(bob, alice, usd(20))); env.close(); env.require(Balance(alice, usd(20))); env.require(Balance(bob, usd(0))); // Global freeze usdm.set({.flags = tfMPTLock}); // MPTLocked flag is set and the account is not the issuer of MPT env(check::cash(bob, chkIdFroz1, usd(1)), Ter(tecPATH_PARTIAL)); env.close(); env(check::cash(bob, chkIdFroz1, check::DeliverMin(usd(1))), Ter(tecPATH_PARTIAL)); env.close(); usdm.set({.flags = tfMPTUnlock}); // No longer frozen. Success. env(check::cash(bob, chkIdFroz1, usd(1))); env.close(); env.require(Balance(alice, usd(19))); env.require(Balance(bob, usd(1))); // Freeze individual MPT. usdm.set({.holder = alice, .flags = tfMPTLock}); env(check::cash(bob, chkIdFroz2, usd(2)), Ter(tecPATH_PARTIAL)); env.close(); env(check::cash(bob, chkIdFroz2, check::DeliverMin(usd(1))), Ter(tecPATH_PARTIAL)); env.close(); // Clear that freeze. Now check cashing works. usdm.set({.holder = alice, .flags = tfMPTUnlock}); env(check::cash(bob, chkIdFroz2, usd(2))); env.close(); env.require(Balance(alice, usd(17))); env.require(Balance(bob, usd(3))); // Freeze bob's MPT. bob can't cash the check. usdm.set({.holder = bob, .flags = tfMPTLock}); env(check::cash(bob, chkIdFroz3, usd(3)), Ter(tecLOCKED)); env.close(); env(check::cash(bob, chkIdFroz3, check::DeliverMin(usd(1))), Ter(tecLOCKED)); env.close(); // Clear that freeze. Now check cashing works again. usdm.set({.holder = bob, .flags = tfMPTUnlock}); env.close(); env(check::cash(bob, chkIdFroz3, check::DeliverMin(usd(1)))); verifyDeliveredAmount(env, usd(3)); env.require(Balance(alice, usd(14))); env.require(Balance(bob, usd(6))); } { // Set the RequireDest flag on bob's account (after the check // was created) then cash a check without a destination tag. env(fset(bob, asfRequireDest)); env.close(); env(check::cash(bob, chkIdNoDest1, usd(1)), Ter(tecDST_TAG_NEEDED)); env.close(); env(check::cash(bob, chkIdNoDest1, check::DeliverMin(usd(1))), Ter(tecDST_TAG_NEEDED)); env.close(); // bob can cash a check with a destination tag. env(check::cash(bob, chkIdHasDest2, usd(2))); env.close(); env.require(Balance(alice, usd(12))); env.require(Balance(bob, usd(8))); // Clear the RequireDest flag on bob's account so he can // cash the check with no DestinationTag. env(fclear(bob, asfRequireDest)); env.close(); env(check::cash(bob, chkIdNoDest1, usd(1))); env.close(); env.require(Balance(alice, usd(11))); env.require(Balance(bob, usd(9))); } // OutstandingAmount exceeds MaximumAmount { // Already at maximum BEAST_EXPECT(env.balance(gw, usdm) == usdm(-maxAmt)); uint256 const chkId{getCheckIndex(gw, env.seq(gw))}; env(check::create(gw, bob, usdm(10))); env.close(); // Exceeds MaximumAmount (20 + 10) = 30 > 20 env(check::cash(bob, chkId, usdm(10)), Ter(tecPATH_PARTIAL)); env.close(); // Redeem some tokens (20 - 9) = 11 env(pay(alice, gw, usdm(9))); env.close(); // Still exceeds MaximumAmount (11 + 10) = 21 > 20 env(check::cash(bob, chkId, usdm(10)), Ter(tecPATH_PARTIAL)); env.close(); } } void testCancelValid(FeatureBitset features) { // Explore many of the ways to cancel a check. testcase("Cancel valid"); using namespace test::jtx; Account const gw{"gateway"}; Account const alice{"alice"}; Account const bob{"bob"}; Account const zoe{"zoe"}; { Env env{*this, features}; env.fund(XRP(1'000), gw, alice, bob, zoe); MPT const usd = MPTTester({.env = env, .issuer = gw}); // alice creates her checks ahead of time. // Three ordinary checks with no expiration. uint256 const chkId1{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(10))); env.close(); uint256 const chkId2{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10))); env.close(); uint256 const chkId3{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(10))); env.close(); // Three checks that expire in 10 minutes. using namespace std::chrono_literals; uint256 const chkIdNotExp1{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10)), Expiration(env.now() + 600s)); env.close(); uint256 const chkIdNotExp2{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(10)), Expiration(env.now() + 600s)); env.close(); uint256 const chkIdNotExp3{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10)), Expiration(env.now() + 600s)); env.close(); // Three checks that expire in one second. uint256 const chkIdExp1{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(10)), Expiration(env.now() + 1s)); env.close(); uint256 const chkIdExp2{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10)), Expiration(env.now() + 1s)); env.close(); uint256 const chkIdExp3{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(10)), Expiration(env.now() + 1s)); env.close(); // Two checks to cancel using a regular key and using multisigning. uint256 const chkIdReg{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, usd(10))); env.close(); uint256 const chkIdMSig{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10))); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 11); BEAST_EXPECT(ownerCount(env, alice) == 11); // Creator, destination, and an outsider cancel the checks. env(check::cancel(alice, chkId1)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 10); BEAST_EXPECT(ownerCount(env, alice) == 10); env(check::cancel(bob, chkId2)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 9); BEAST_EXPECT(ownerCount(env, alice) == 9); env(check::cancel(zoe, chkId3), Ter(tecNO_PERMISSION)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 9); BEAST_EXPECT(ownerCount(env, alice) == 9); // Creator, destination, and an outsider cancel unexpired checks. env(check::cancel(alice, chkIdNotExp1)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 8); BEAST_EXPECT(ownerCount(env, alice) == 8); env(check::cancel(bob, chkIdNotExp2)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 7); BEAST_EXPECT(ownerCount(env, alice) == 7); env(check::cancel(zoe, chkIdNotExp3), Ter(tecNO_PERMISSION)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 7); BEAST_EXPECT(ownerCount(env, alice) == 7); // Creator, destination, and an outsider cancel expired checks. env(check::cancel(alice, chkIdExp1)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 6); BEAST_EXPECT(ownerCount(env, alice) == 6); env(check::cancel(bob, chkIdExp2)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 5); BEAST_EXPECT(ownerCount(env, alice) == 5); env(check::cancel(zoe, chkIdExp3)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 4); BEAST_EXPECT(ownerCount(env, alice) == 4); // Use a regular key and also multisign to cancel checks. Account const alie{"alie", KeyType::Ed25519}; env(regkey(alice, alie)); env.close(); Account const bogie{"bogie", KeyType::Secp256k1}; Account const demon{"demon", KeyType::Ed25519}; env(signers(alice, 2, {{bogie, 1}, {demon, 1}}), Sig(alie)); env.close(); int const signersCount{1}; // alice uses her regular key to cancel a check. env(check::cancel(alice, chkIdReg), Sig(alie)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 3); BEAST_EXPECT(ownerCount(env, alice) == signersCount + 3); // alice uses multisigning to cancel a check. XRPAmount const baseFeeDrops{env.current()->fees().base}; env(check::cancel(alice, chkIdMSig), Msig(bogie, demon), Fee(3 * baseFeeDrops)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 2); BEAST_EXPECT(ownerCount(env, alice) == signersCount + 2); // Creator and destination cancel the remaining unexpired checks. env(check::cancel(alice, chkId3), Sig(alice)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == signersCount + 1); env(check::cancel(bob, chkIdNotExp3)); env.close(); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(ownerCount(env, alice) == signersCount + 0); } } void testWithTickets(FeatureBitset features) { testcase("With Tickets"); using namespace test::jtx; Account const gw{"gw"}; Account const alice{"alice"}; Account const bob{"bob"}; Env env{*this, features}; env.fund(XRP(1'000), gw, alice, bob); env.close(); MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 1'000}); // alice and bob grab enough tickets for all the following // transactions. Note that once the tickets are acquired alice's // and bob's account sequence numbers should not advance. std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; env(ticket::create(alice, 10)); std::uint32_t const aliceSeq{env.seq(alice)}; std::uint32_t bobTicketSeq{env.seq(bob) + 1}; env(ticket::create(bob, 10)); std::uint32_t const bobSeq{env.seq(bob)}; env.close(); // MPT + 10 tickets env.require(Owners(alice, 11)); env.require(Owners(bob, 11)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); env(pay(gw, alice, usd(900))); env.close(); // alice creates four checks; two XRP, two MPT. Bob will cash // one of each and cancel one of each. uint256 const chkIdXrp1{getCheckIndex(alice, aliceTicketSeq)}; env(check::create(alice, bob, XRP(200)), ticket::Use(aliceTicketSeq++)); uint256 const chkIdXrp2{getCheckIndex(alice, aliceTicketSeq)}; env(check::create(alice, bob, XRP(300)), ticket::Use(aliceTicketSeq++)); uint256 const chkIdUsd1{getCheckIndex(alice, aliceTicketSeq)}; env(check::create(alice, bob, usd(200)), ticket::Use(aliceTicketSeq++)); uint256 const chkIdUsd2{getCheckIndex(alice, aliceTicketSeq)}; env(check::create(alice, bob, usd(300)), ticket::Use(aliceTicketSeq++)); env.close(); // Alice used four tickets but created four checks. env.require(Owners(alice, 11)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(checksOnAccount(env, alice).size() == 4); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(Owners(bob, 11)); BEAST_EXPECT(env.seq(bob) == bobSeq); // Bob cancels two of alice's checks. env(check::cancel(bob, chkIdXrp1), ticket::Use(bobTicketSeq++)); env(check::cancel(bob, chkIdUsd2), ticket::Use(bobTicketSeq++)); env.close(); env.require(Owners(alice, 9)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(checksOnAccount(env, alice).size() == 2); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(Owners(bob, 9)); BEAST_EXPECT(env.seq(bob) == bobSeq); // Bob cashes alice's two remaining checks. env(check::cash(bob, chkIdXrp2, XRP(300)), ticket::Use(bobTicketSeq++)); env(check::cash(bob, chkIdUsd1, usd(200)), ticket::Use(bobTicketSeq++)); env.close(); auto const baseFee = env.current()->fees().base; env.require(Owners(alice, 7)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(Balance(alice, usd(700))); env.require(Balance(alice, XRP(700) - 6 * baseFee)); env.require(Owners(bob, 7)); BEAST_EXPECT(env.seq(bob) == bobSeq); env.require(Balance(bob, usd(200))); env.require(Balance(bob, XRP(1'300) - 6 * baseFee)); } void testMPTCreation(FeatureBitset features) { // Explore automatic MPT creation when a check is cashed. testcase("MPT Creation"); using namespace test::jtx; Env env{*this, features}; // An account that independently tracks its owner count. struct AccountOwns { using iterator = hash_map::iterator; beast::unit_test::Suite& suite; Env& env; Account const acct; std::size_t owners{0}; hash_map mpts; bool const isIssuer; bool const requireAuth; AccountOwns( beast::unit_test::Suite& s, Env& e, Account a, bool isIssuer, bool requireAuth = false) : suite(s), env(e), acct(std::move(a)), isIssuer(isIssuer), requireAuth(requireAuth) { } void verifyOwners(std::uint32_t line, bool print = false) const { if (print) { std::cout << acct.name() << " " << ownerCount(env, acct) << " " << owners << std::endl; } suite.expect( ownerCount(env, acct) == owners, "Owner count mismatch", __FILE__, line); } // Operators to make using the class more convenient. operator Account() const { return acct; } operator xrpl::AccountID() const { return acct.id(); } /** Create MPTTester if it doesn't exist for the given MPT. * Increment owners if created since it creates MPTokenIssuance */ MPT operator[](std::string const& s) { if (!isIssuer) Throw("AccountOwns: must be issuer"); if (auto const& it = mpts.find(s); it != mpts.end()) return it->second[s]; auto flags = kMptDexFlags | tfMPTCanLock; if (requireAuth) flags |= tfMPTRequireAuth; auto [it, _] = mpts.emplace(s, MPTTester({.env = env, .issuer = acct, .flags = flags})); (void)_; ++owners; return it->second[s]; } iterator getIt(MPT const& mpt) { if (!isIssuer) Throw("AccountOwns::set must be issuer"); auto it = mpts.find(mpt.name); if (it == mpts.end()) Throw("AccountOwns::set mpt doesn't exist"); return it; } void set(MPT const& mpt, std::uint32_t flag) { auto it = getIt(mpt); it->second.set({.flags = flag}); } void authorize(MPT const& mpt, AccountOwns& id) { auto it = getIt(mpt); it->second.authorize({.account = id}); ++id.owners; } void cleanup(MPT const& mpt, AccountOwns& id) { auto it = getIt(mpt); // redeem to the issuer if (auto const redeem = it->second.getBalance(id)) pay(it, id, acct, redeem); // delete mptoken it->second.authorize({.account = id, .flags = tfMPTUnauthorize}); --id.owners; } void pay(iterator& it, Account const& src, Account const& dst, std::uint64_t amount) { if (env.le(keylet::account(dst))->isFlag(lsfDepositAuth)) { env(fclear(dst, asfDepositAuth)); it->second.pay(src, dst, amount); env(fset(dst, asfDepositAuth)); } else { it->second.pay(src, dst, amount); } } void pay(Account const& src, Account const& dst, PrettyAmount amount) { auto it = getIt(amount.name()); pay(it, src, dst, amount.value().mpt().value()); } }; AccountOwns alice{*this, env, "alice", false}; AccountOwns bob{*this, env, "bob", false}; AccountOwns gw1{*this, env, "gw1", true}; // Fund with noripple so the accounts do not have any flags set. env.fund(XRP(5000), noripple(alice, bob)); env.close(); // Automatic MPT creation should fail if the check destination // can't afford the reserve for the trust line. { // Fund gw1 with noripple (even though that's atypical for a // gateway) so it does not have any flags set. We'll set flags // on gw1 later. env.fund(XRP(5'000), noripple(gw1)); env.close(); MPT const cK8 = gw1["CK8"]; gw1.verifyOwners(__LINE__); Account const yui{"yui"}; // Note the reserve in unit tests is 200 XRP, not 20. So here // we're just barely giving yui enough XRP to meet the // account reserve. env.fund(XRP(200), yui); env.close(); uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))}; env(check::create(gw1, yui, cK8(99))); env.close(); env(check::cash(yui, chkId, cK8(99)), Ter(tecINSUFFICIENT_RESERVE)); env.close(); alice.verifyOwners(__LINE__); // Give yui enough XRP to meet the trust line's reserve. Cashing // the check succeeds and creates the trust line. env(pay(env.master, yui, XRP(51))); env.close(); env(check::cash(yui, chkId, cK8(99))); verifyDeliveredAmount(env, cK8(99)); env.close(); BEAST_EXPECT(ownerCount(env, yui) == 1); // The automatic trust line does not take a reserve from gw1. // Since gw1's check was consumed it has no owners. gw1.verifyOwners(__LINE__); } // We'll be looking at the effects of various account root flags and // MPT flags. // Automatically create MPT using // o Offers and // o Check cashing //----------- No account root flags, check written by issuer ----------- { // No account root flags on any participant. // Automatic trust line from issuer to destination. BEAST_EXPECT((*env.le(gw1))[sfFlags] == 0); BEAST_EXPECT((*env.le(alice))[sfFlags] == 0); BEAST_EXPECT((*env.le(bob))[sfFlags] == 0); // Use offers to automatically create MPT MPT const oF1 = gw1["OF1"]; env(offer(gw1, XRP(98), oF1(98))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF1.issuanceID, alice)) == nullptr); env(offer(alice, oF1(98), XRP(98))); ++alice.owners; env.close(); // Both offers should be consumed. // Since gw1's offer was consumed and the trust line was not // created by gw1, gw1's owner count should be 0. gw1.verifyOwners(__LINE__); // alice's automatically created MPT bumps her owner count. alice.verifyOwners(__LINE__); // Use check cashing to automatically create the trust line. MPT const cK1 = gw1["CK1"]; uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))}; env(check::create(gw1, alice, cK1(98))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK1.issuanceID, alice)) == nullptr); env(check::cash(alice, chkId, cK1(98))); ++alice.owners; verifyDeliveredAmount(env, cK1(98)); env.close(); // gw1's check should be consumed. // Since gw1's check was consumed and the trust line was not // created by gw1, gw1's owner count should be 0. gw1.verifyOwners(__LINE__); // alice's automatically created trust line bumps her owner count. alice.verifyOwners(__LINE__); // cmpTrustLines(gw1, alice, OF1, CK1); } //--------- No account root flags, check written by non-issuer --------- { // No account root flags on any participant. // Use offers to automatically create MPT. // Transfer of assets using offers does not require rippling. // So bob's offer is successfully crossed which creates MPT. MPT const oF1 = gw1["OF1"]; env(offer(alice, XRP(97), oF1(97))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF1, bob)) == nullptr); env(offer(bob, oF1(97), XRP(97))); ++bob.owners; env.close(); // Both offers should be consumed. env.require(Balance(alice, oF1(1))); env.require(Balance(bob, oF1(97))); // bob now has an owner count of 1 due to new MPT. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create MPT. // // Unlike IOU where cashing a check (unlike crossing offers) // requires rippling through the currency's issuer, rippling doesn't // impact MPT. Even though gw1 does not have rippling enabled, the // check cash succeeds for MPT and MPT is created. MPT const cK1 = gw1["CK1"]; uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, cK1(97))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK1, bob)) == nullptr); env(check::cash(bob, chkId, cK1(97))); ++bob.owners; env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF1, bob)) != nullptr); gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); } //------------- lsfDefaultRipple, check written by issuer -------------- { // gw1 enables rippling. // This doesn't impact automatic MPT creation. env(fset(gw1, asfDefaultRipple)); env.close(); // Use offers to automatically create the trust line. MPT const oF2 = gw1["OF2"]; env(offer(gw1, XRP(96), oF2(96))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF2, alice)) == nullptr); env(offer(alice, oF2(96), XRP(96))); ++alice.owners; env.close(); // Both offers should be consumed. // Since gw1's offer was consumed, gw1 owner count doesn't change. gw1.verifyOwners(__LINE__); // alice's automatically created MPT bumps her owner count. alice.verifyOwners(__LINE__); // Use check cashing to automatically create MPT. MPT const cK2 = gw1["CK2"]; uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))}; env(check::create(gw1, alice, cK2(96))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK2, alice)) == nullptr); env(check::cash(alice, chkId, cK2(96))); ++alice.owners; verifyDeliveredAmount(env, cK2(96)); env.close(); // gw1's check should be consumed. // Since gw1's check was consumed and MPT was not // created by gw1, gw1's owner count doesn't change. gw1.verifyOwners(__LINE__); // alice's automatically created trust line bumps her owner count. alice.verifyOwners(__LINE__); } //----------- lsfDefaultRipple, check written by non-issuer ------------ { // gw1 enabled rippling doesn't impact MPT, so automatic MPT from // non-issuer to non-issuer should work. // Use offers to automatically create MPT. MPT const oF2 = gw1["OF2"]; env(offer(alice, XRP(95), oF2(95))); env.close(); // alice already has OF2 MPT BEAST_EXPECT(env.le(keylet::mptoken(oF2, alice)) != nullptr); env(offer(bob, oF2(95), XRP(95))); ++bob.owners; env.close(); // bob's owner count should increase due to the new MPT. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create MPT. MPT const cK2 = gw1["CK2"]; uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, cK2(95))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK2, bob)) == nullptr); env(check::cash(bob, chkId, cK2(95))); ++bob.owners; verifyDeliveredAmount(env, cK2(95)); env.close(); // bob's owner count should increase due to the new MPT. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); } //-------------- lsfDepositAuth, check written by issuer --------------- { // Both offers and checks ignore the lsfDepositAuth flag, since // the destination signs the transaction that delivers their funds. // So setting lsfDepositAuth on all the participants should not // change any outcomes. // // Automatic MPT from issuer to non-issuer should still work. env(fset(gw1, asfDepositAuth)); env(fset(alice, asfDepositAuth)); env(fset(bob, asfDepositAuth)); env.close(); // Use offers to automatically create MPT. MPT const oF3 = gw1["OF3"]; env(offer(gw1, XRP(94), oF3(94))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF3, alice)) == nullptr); env(offer(alice, oF3(94), XRP(94))); ++alice.owners; env.close(); // Both offers should be consumed. // Since gw1's offer was consumed and MPT was not // created by gw1, gw1's owner count doesn't change. gw1.verifyOwners(__LINE__); // alice's automatically created MPT bumps her owner count. alice.verifyOwners(__LINE__); // Use check cashing to automatically create MPT. MPT const cK3 = gw1["CK3"]; uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))}; env(check::create(gw1, alice, cK3(94))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK3, alice)) == nullptr); env(check::cash(alice, chkId, cK3(94))); ++alice.owners; verifyDeliveredAmount(env, cK3(94)); env.close(); // gw1's check should be consumed. // Since gw1's check was consumed and MPT was not // created by gw1, gw1's owner count doesn't change. gw1.verifyOwners(__LINE__); // alice's automatically created trust line bumps her owner count. alice.verifyOwners(__LINE__); } //------------ lsfDepositAuth, check written by non-issuer ------------- { // The presence of the lsfDepositAuth flag should not affect // automatic MPT creation. // Use offers to automatically create MPT. MPT const oF3 = gw1["OF3"]; env(offer(alice, XRP(93), oF3(93))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF3, alice)) != nullptr); env(offer(bob, oF3(93), XRP(93))); ++bob.owners; env.close(); // bob's owner count should increase due to the new MPT. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create MPT. MPT const cK3 = gw1["CK3"]; uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, cK3(93))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK3, bob)) == nullptr); env(check::cash(bob, chkId, cK3(93))); ++bob.owners; verifyDeliveredAmount(env, cK3(93)); env.close(); // bob's owner count should increase due to the new MPT. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); } //-------------- lsfGlobalFreeze, check written by issuer -------------- { // Set lsfGlobalFreeze on gw1. That should not stop any automatic // MPT from being created. env(fset(gw1, asfGlobalFreeze)); env.close(); // Use offers to automatically create MPT. MPT const oF4 = gw1["OF4"]; env(offer(gw1, XRP(92), oF4(92))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF4, alice)) == nullptr); env(offer(alice, oF4(92), XRP(92))); ++alice.owners; env.close(); // alice's owner count should increase do to the new MPT. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create MPT. MPT const cK4 = gw1["CK4"]; uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))}; env(check::create(gw1, bob, cK4(92))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK4, bob)) == nullptr); env(check::cash(bob, chkId, cK4(92))); verifyDeliveredAmount(env, cK4(92)); ++bob.owners; env.close(); // bob's owner count should increase due to the new MPT. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // clean up gw1.cleanup(oF4, alice); gw1.cleanup(cK4, bob); } //-------------- lsfMPTLock, check written by issuer -------------- { // Set lsfMPTLock on gw1. That should stop any automatic // MPT from being created. // Use offers to automatically create MPT. MPT const oF4 = gw1["OF4"]; gw1.set(oF4, tfMPTLock); env(offer(gw1, XRP(92), oF4(92)), Ter(tecFROZEN)); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF4, alice)) == nullptr); env(offer(alice, oF4(92), XRP(92)), Ter(tecFROZEN)); env.close(); // No one's owner count should have changed. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create MPT. MPT const cK4 = gw1["CK4"]; gw1.set(cK4, tfMPTLock); uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))}; env(check::create(gw1, alice, cK4(92)), Ter(tecLOCKED)); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK4, alice)) == nullptr); env(check::cash(alice, chkId, cK4(92)), Ter(tecNO_ENTRY)); env.close(); // No one's owner count should have changed. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Because gw1 has set tfMPTLock, neither MPT // is created. BEAST_EXPECT(env.le(keylet::mptoken(oF4, alice)) == nullptr); BEAST_EXPECT(env.le(keylet::mptoken(cK4, alice)) == nullptr); // clear global freeze gw1.set(oF4, tfMPTUnlock); gw1.set(cK4, tfMPTUnlock); } //------------ lsfGlobalFreeze, check written by non-issuer ------------ { // lsfGlobalFreeze flag set on gw1 should not stop // automatic MPT creation between non-issuers. // Use offers to automatically create MPT. MPT const oF4 = gw1["OF4"]; gw1.authorize(oF4, alice); gw1.pay(gw1, alice, oF4(91)); env(offer(alice, XRP(91), oF4(91))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF4, alice)) != nullptr); env(offer(bob, oF4(91), XRP(91))); ++bob.owners; env.close(); // alice's owner count should increase since it created MPT. // bob's owner count should increase due to the new MPT. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create the trust line. MPT const cK4 = gw1["CK4"]; uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, cK4(91))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK4, bob)) == nullptr); gw1.authorize(cK4, alice); gw1.pay(gw1, alice, cK4(91)); env(check::cash(bob, chkId, cK4(91))); ++bob.owners; env.close(); // alice's owner count should increase since it created MPT. // bob's owner count should increase due to the new MPT. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // cleanup gw1.cleanup(oF4, alice); gw1.cleanup(cK4, alice); gw1.cleanup(oF4, bob); gw1.cleanup(cK4, bob); } //------------ lsfMPTLock, check written by non-issuer ------------ { // Since gw1 has the lsfMPTLock flag set, there should be // no automatic MPT creation between non-issuers. // Use offers to automatically create MPT. MPT const oF4 = gw1["OF4"]; gw1.set(oF4, tfMPTLock); env(offer(alice, XRP(91), oF4(91)), Ter(tecFROZEN)); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF4, alice)) == nullptr); env(offer(bob, oF4(91), XRP(91)), Ter(tecFROZEN)); env.close(); // No one's owner count should have changed. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create the trust line. MPT const cK4 = gw1["CK4"]; gw1.set(cK4, tfMPTLock); uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, cK4(91)), Ter(tecLOCKED)); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK4, bob)) == nullptr); env(check::cash(bob, chkId, cK4(91)), Ter(tecNO_ENTRY)); env.close(); // No one's owner count should have changed. gw1.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Because gw1 has set lsfGlobalFreeze, neither trust line // is created. BEAST_EXPECT(env.le(keylet::mptoken(oF4, bob)) == nullptr); BEAST_EXPECT(env.le(keylet::mptoken(cK4, bob)) == nullptr); gw1.set(oF4, tfMPTUnlock); gw1.set(cK4, tfMPTUnlock); } //-------------- lsfRequireAuth, check written by issuer --------------- // We want to test the lsfRequireAuth flag, but we can't set that // flag on an account that already has MPT. So we'll fund // a new gateway and use that. AccountOwns gw2{*this, env, "gw2", true}; { env.fund(XRP(5'000), gw2); env.close(); // Set lsfRequireAuth on gw2. That should not stop any automatic // MPT from being created. env(fset(gw2, asfRequireAuth)); env.close(); // Use offers to automatically create MPT. MPT const oF5 = gw2["OF5"]; env(offer(gw2, XRP(92), oF5(92))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF5, alice)) == nullptr); env(offer(alice, oF5(92), XRP(92))); ++alice.owners; env.close(); // alice's owner count should increase due to the new MPT. gw2.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create MPT. MPT const cK5 = gw2["CK5"]; uint256 const chkId{getCheckIndex(gw2, env.seq(gw2))}; env(check::create(gw2, alice, cK5(92))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK5, alice)) == nullptr); env(check::cash(alice, chkId, cK5(92))); verifyDeliveredAmount(env, cK5(92)); ++alice.owners; env.close(); // alice's owner count should increase due to the new MPT. gw2.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // cleanup gw2.cleanup(oF5, alice); gw2.cleanup(cK5, alice); } // Fund new gw to test since gw2 has MPTokenIssuance already created. // Set RequireAuth flag. AccountOwns gw3{*this, env, "gw3", true, true}; { env.fund(XRP(5'000), gw3); env.close(); // Use offers to automatically create the trust line. MPT const oF5 = gw3["OF5"]; std::uint32_t const gw3OfferSeq = {env.seq(gw3)}; env(offer(gw3, XRP(92), oF5(92))); ++gw3.owners; env.close(); BEAST_EXPECT(env.le(keylet::mptoken(oF5, alice)) == nullptr); env(offer(alice, oF5(92), XRP(92)), Ter(tecNO_AUTH)); env.close(); // gw3 should still own the offer, but no one else's owner // count should have changed. gw3.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Since we don't need it anymore, remove gw3's offer. env(offerCancel(gw3, gw3OfferSeq)); --gw3.owners; env.close(); gw3.verifyOwners(__LINE__); // Use check cashing to automatically create the trust line. MPT const cK5 = gw3["CK5"]; uint256 const chkId{getCheckIndex(gw3, env.seq(gw3))}; env(check::create(gw3, alice, cK5(92))); ++gw3.owners; env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK5, alice)) == nullptr); env(check::cash(alice, chkId, cK5(92)), Ter(tecNO_AUTH)); env.close(); // gw3 should still own the check, but no one else's owner // count should have changed. gw3.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Because gw3 has set lsfRequireAuth, neither trust line // is created. BEAST_EXPECT(env.le(keylet::mptoken(oF5, alice)) == nullptr); BEAST_EXPECT(env.le(keylet::mptoken(cK5, alice)) == nullptr); // Since we don't need it anymore, remove gw3's check. env(check::cancel(gw3, chkId)); --gw3.owners; env.close(); gw3.verifyOwners(__LINE__); } //------------ lsfRequireAuth, check written by non-issuer ------------- { // gw2 lsfRequireAuth flag set should not affect // automatic MPT creation between non-issuers. // Use offers to automatically create MPT. MPT const oF5 = gw2["OF5"]; gw2.authorize(oF5, alice); gw2.pay(gw2, alice, oF5(91)); env(offer(alice, XRP(91), oF5(91))); env.close(); env(offer(bob, oF5(91), XRP(91))); ++bob.owners; env.close(); // bob's owner count should increase due to the new MPT. gw2.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create the trust line. MPT const cK5 = gw2["CK5"]; gw2.authorize(cK5, alice); gw2.pay(gw2, alice, cK5(91)); uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, cK5(91))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK5, bob)) == nullptr); env(check::cash(bob, chkId, cK5(91))); ++bob.owners; env.close(); // bob's owner count should increase due to the new MPT. gw2.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); } //------------ lsfMPTRequireAuth, check written by non-issuer //------------- { // Since gw3 has the lsfMPTRequireAuth flag set, there should be // no automatic MPT creation between non-issuers. // Use offers to automatically create the trust line. MPT const oF5 = gw3["OF5"]; env(offer(alice, XRP(91), oF5(91)), Ter(tecUNFUNDED_OFFER)); env.close(); env(offer(bob, oF5(91), XRP(91)), Ter(tecNO_AUTH)); BEAST_EXPECT(env.le(keylet::mptoken(oF5, bob)) == nullptr); env.close(); gw3.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Use check cashing to automatically create the trust line. MPT const cK5 = gw3["CK5"]; uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, cK5(91))); env.close(); BEAST_EXPECT(env.le(keylet::mptoken(cK5, bob)) == nullptr); env(check::cash(bob, chkId, cK5(91)), Ter(tecPATH_PARTIAL)); env.close(); // Delete alice's check since it is no longer needed. env(check::cancel(alice, chkId)); env.close(); // No one's owner count should have changed. gw3.verifyOwners(__LINE__); alice.verifyOwners(__LINE__); bob.verifyOwners(__LINE__); // Because gw3 has set lsfRequireAuth, neither trust line // is created. BEAST_EXPECT(env.le(keylet::mptoken(oF5, bob)) == nullptr); BEAST_EXPECT(env.le(keylet::mptoken(cK5, bob)) == nullptr); } } void testWithFeats(FeatureBitset features) { testCreateValid(features); testCreateDisallowIncoming(features); testCreateInvalid(features); testCashMPT(features); testCashXferFee(features); testCashInvalid(features); testCancelValid(features); testWithTickets(features); } public: void run() override { using namespace test::jtx; auto const sa = testableAmendments(); testWithFeats(sa); testMPTCreation(sa); } }; BEAST_DEFINE_TESTSUITE(CheckMPT, tx, xrpl); } // namespace xrpl