From e5d61cf0215f122ce19f7f87e0cd57b41bd2f7ef Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 17 Jan 2023 17:05:58 -0500 Subject: [PATCH 01/38] initial commit --- src/ripple/protocol/Feature.h | 3 +- src/ripple/protocol/impl/Feature.cpp | 1 + src/test/app/AccountDelete_test.cpp | 91 +++++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index d4e65a31af8..1b3f4871a08 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 54; +static constexpr std::size_t numFeatures = 55; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -341,6 +341,7 @@ extern uint256 const fixTrustLinesToSelf; extern uint256 const fixRemoveNFTokenAutoTrustLine; extern uint256 const featureImmediateOfferKilled; extern uint256 const featureDisallowIncoming; +extern uint256 const fixNFTokenRemint; } // namespace ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 5903603f975..4496818546c 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -451,6 +451,7 @@ REGISTER_FIX (fixTrustLinesToSelf, Supported::yes, DefaultVote::no) REGISTER_FIX (fixRemoveNFTokenAutoTrustLine, Supported::yes, DefaultVote::yes); REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no); REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no); +REGISTER_FEATURE(fixNFTokenRemint, Supported::yes, DefaultVote::no); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 73a0ccbf9e0..d0f59206245 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -89,7 +89,7 @@ class AccountDelete_test : public beast::unit_test::suite incLgrSeqForAccDel( jtx::Env& env, jtx::Account const& acc, - std::uint32_t margin = 0) + std::uint32_t margin = 0.) { int const delta = [&]() -> int { if (env.seq(acc) + 255 > openLedgerSeq(env)) @@ -102,6 +102,8 @@ class AccountDelete_test : public beast::unit_test::suite BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); } + + public: void testBasics() @@ -907,6 +909,93 @@ class AccountDelete_test : public beast::unit_test::suite verifyDeliveredAmount(env, beckyOldBalance - acctDelFee); env.close(); } + void + testRemintAmendmentEnable() + { + // Start with the featureDeletableAccounts amendment disabled. + // Then enable the amendment and delete an account. + using namespace jtx; + + testcase("Remint Amendment enable"); + + Env env{*this, supported_amendments() - featureDeletableAccounts}; + Account const alice("alice"); + Account const becky("becky"); + + env.fund(XRP(10000), alice, becky); + env.close(); + + std::vector nftIDs; + nftIDs.reserve(200); + for(int i = 0; i<200; i++){ + uint256 const nftokenID = + token::getNextID(env, alice, 0, tfTransferable); + nftIDs.push_back(nftokenID); + env(token::mint(alice, 0), + token::uri(std::string(maxTokenURILength, 'u')), + txflags(tfTransferable)); + env.close(); + } + + for(auto const nftokenID: nftIDs){ + env(token::burn(alice, nftokenID)); + } + + env.close(); + + { + // Close enough ledgers to be able to delete alice's account. + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + + auto const alicePreDelBal{env.balance(alice)}; + auto const beckyPreDelBal{env.balance(becky)}; + + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee)); + env.close(); + + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + } + + { + env.enableFeature(featureDeletableAccounts); + env.close(); + // Close enough ledgers to be able to delete alice's account. + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + + auto const alicePreDelBal{env.balance(alice)}; + auto const beckyPreDelBal{env.balance(becky)}; + + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee), ter(tecTOO_SOON)); + env.close(); + + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + + auto incLgrSeqForNFTokenAccDel = [&]( jtx::Account const& acc) { + int delta = 0; + auto t = (*env.le(acc))[sfSequence]; + auto t2 = (*env.le(acc))[sfMintedNFTokens] + if((*env.le(acc))[sfSequence] + (*env.le(acc))[sfMintedNFTokens]+ 255 > openLedgerSeq(env)) + delta = (*env.le(acc))[sfSequence] + (*env.le(acc))[sfMintedNFTokens]+ 255 - openLedgerSeq(env); + + BEAST_EXPECT(margin == 0 || delta >= 0); + for (int i = 0; i < delta; ++i) + env.close(); + BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); + }; + } + + } void run() override From 95f644fd0b0aa2e92421991545fe34b09e8cc176 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 18 Jan 2023 10:45:29 -0500 Subject: [PATCH 02/38] wip --- src/test/app/AccountDelete_test.cpp | 53 +++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index d0f59206245..a1d9880bdb9 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -964,6 +964,7 @@ class AccountDelete_test : public beast::unit_test::suite { env.enableFeature(featureDeletableAccounts); env.close(); + // Close enough ledgers to be able to delete alice's account. incLgrSeqForAccDel(env, alice); @@ -974,25 +975,57 @@ class AccountDelete_test : public beast::unit_test::suite auto const alicePreDelBal{env.balance(alice)}; auto const beckyPreDelBal{env.balance(becky)}; - auto const acctDelFee{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee), ter(tecTOO_SOON)); + auto const acctDelFee1{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); env.close(); BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + auto const alicePreDelBal{env.balance(alice)}; + auto const beckyPreDelBal{env.balance(becky)}; + + // Verify that alice's account root is still present and alice and + // becky both have their XRP. + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT(env.balance(alice) == alicePreDelBal); + BEAST_EXPECT(env.balance(becky) == beckyPreDelBal); - auto incLgrSeqForNFTokenAccDel = [&]( jtx::Account const& acc) { + // Close the ledger until the ledger sequence is large enough to close + // the account, which has minted NFTs + auto incLgrSeqForNFTokenAccDel = [&](jtx::Account const& acc) { int delta = 0; - auto t = (*env.le(acc))[sfSequence]; - auto t2 = (*env.le(acc))[sfMintedNFTokens] - if((*env.le(acc))[sfSequence] + (*env.le(acc))[sfMintedNFTokens]+ 255 > openLedgerSeq(env)) - delta = (*env.le(acc))[sfSequence] + (*env.le(acc))[sfMintedNFTokens]+ 255 - openLedgerSeq(env); + auto const deletableLgrSeq = (*env.le(acc))[sfSequence] + (*env.le(acc))[sfMintedNFTokens]+ 255; + + if(deletableLgrSeq > openLedgerSeq(env)) + delta = deletableLgrSeq - openLedgerSeq(env); - BEAST_EXPECT(margin == 0 || delta >= 0); + BEAST_EXPECT(delta >= 0); for (int i = 0; i < delta; ++i) env.close(); - BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); - }; + + BEAST_EXPECT(openLedgerSeq(env) == deletableLgrSeq + 255); + }; + + // Close more ledgers to be able to delete alice's account + incLgrSeqForNFTokenAccDel(alice); + + auto const acctDelFee2{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee2)); + env.close(); + + + // alice's account is still in the most recently closed ledger. + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + + // Verify that alice's account root is gone from the current ledger + // and becky has alice's XRP. + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + BEAST_EXPECT( + env.balance(becky) == alicePreDelBal + beckyPreDelBal - acctDelFee2); + + env.close(); + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + } } From 60ab256f75ee847ea3417e1980a13809ec5a120d Mon Sep 17 00:00:00 2001 From: ledhed2222 Date: Wed, 18 Jan 2023 16:37:39 -0500 Subject: [PATCH 03/38] Add fixUnburnableNFToken feature (#4391) --- src/ripple/protocol/Feature.h | 3 ++- src/ripple/protocol/impl/Feature.cpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index d4e65a31af8..291c7c65a15 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 54; +static constexpr std::size_t numFeatures = 55; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -341,6 +341,7 @@ extern uint256 const fixTrustLinesToSelf; extern uint256 const fixRemoveNFTokenAutoTrustLine; extern uint256 const featureImmediateOfferKilled; extern uint256 const featureDisallowIncoming; +extern uint256 const fixUnburnableNFToken; } // namespace ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 5903603f975..31d9999511d 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -451,6 +451,7 @@ REGISTER_FIX (fixTrustLinesToSelf, Supported::yes, DefaultVote::no) REGISTER_FIX (fixRemoveNFTokenAutoTrustLine, Supported::yes, DefaultVote::yes); REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no); REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no); +REGISTER_FIX (fixUnburnableNFToken, Supported::yes, DefaultVote::no); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. From 956126832d16a5e559ad4ddd46310f1241042225 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 18 Jan 2023 13:57:45 -0500 Subject: [PATCH 04/38] NFToken new sequence construct --- src/ripple/app/tx/impl/DeleteAccount.cpp | 19 + src/ripple/app/tx/impl/NFTokenMint.cpp | 48 +- src/ripple/protocol/Feature.h | 2 +- src/ripple/protocol/SField.h | 1 + src/ripple/protocol/impl/Feature.cpp | 2 +- src/ripple/protocol/impl/LedgerFormats.cpp | 1 + src/ripple/protocol/impl/SField.cpp | 1 + src/test/app/AccountDelete_test.cpp | 124 +---- src/test/app/NFTokenBurn_test.cpp | 17 +- src/test/app/NFTokenDir_test.cpp | 27 +- src/test/app/NFToken_test.cpp | 563 ++++++++++++++++++++- src/test/jtx/impl/token.cpp | 18 +- 12 files changed, 664 insertions(+), 159 deletions(-) diff --git a/src/ripple/app/tx/impl/DeleteAccount.cpp b/src/ripple/app/tx/impl/DeleteAccount.cpp index da2244bca5e..7aaf87d070d 100644 --- a/src/ripple/app/tx/impl/DeleteAccount.cpp +++ b/src/ripple/app/tx/impl/DeleteAccount.cpp @@ -223,6 +223,25 @@ DeleteAccount::preclaim(PreclaimContext const& ctx) if ((*sleAccount)[sfSequence] + seqDelta > ctx.view.seq()) return tecTOO_SOON; + // When fixUnburnableNFToken is enabled, we don't allow an account to be + // deleted if is within 256 of the + // current ledger. This is to prevent having duplicate NFTokenIDs after + // account re-creation. + // + // Without this restriction, duplicate NFTokenIDs can be reproduced when + // authorized minting is involved. Because when the minter mints a NFToken, + // the issuer's sequence does not change. So when the issuer re-creates + // their account and mints a NFToken, it is possible that the + // NFTokenSequence of this NFToken is the same as the one that the + // authorized minter minted in a previous ledger. + if (ctx.view.rules().enabled(fixUnburnableNFToken) && + ((*sleAccount)[~sfFirstNFTokenSequence].value_or(0) + + (*sleAccount)[sfMintedNFTokens] + seqDelta > + ctx.view.seq())) + { + return tecTOO_SOON; + } + // Verify that the account does not own any objects that would prevent // the account from being deleted. Keylet const ownerDirKeylet{keylet::ownerDir(account)}; diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index f4d3eb85676..f7bab0e963a 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -160,14 +160,48 @@ NFTokenMint::doApply() // Should not happen. Checked in preclaim. return Unexpected(tecNO_ISSUER); - // Get the unique sequence number for this token: - std::uint32_t const tokenSeq = (*root)[~sfMintedNFTokens].value_or(0); + std::uint32_t tokenSeq; + if (ctx_.view().rules().enabled(fixUnburnableNFToken)) { - std::uint32_t const nextTokenSeq = tokenSeq + 1; - if (nextTokenSeq < tokenSeq) - return Unexpected(tecMAX_SEQUENCE_REACHED); - - (*root)[sfMintedNFTokens] = nextTokenSeq; + auto accSeq = (*root)[sfSequence]; + + // If the issuer hasn't minted a NFT before, we must + // initialize sfFirstNFTokenSequence to equal to the current account + // sequence. In general, we must subtract account sequence by + // one, since it is incremented by the transactor beforehand. In + // scenarios of AuthorizedMinting or Tickets, we use the + // account sequence as it is because it has not been incremented + std::uint32_t const firstNFTokenSeq = + (*root)[~sfFirstNFTokenSequence].value_or( + (*root)[~sfNFTokenMinter] == ctx_.tx[sfAccount] || + ctx_.tx.getSeqProxy().isTicket() + ? accSeq + : accSeq - 1); + + // Get the unique sequence number of this token by + // sfFirstNFTokenSequence + sfMintedNFTokens + tokenSeq = firstNFTokenSeq + (*root)[~sfMintedNFTokens].value_or(0); + { + std::uint32_t const nextTokenSeq = tokenSeq + 1; + if (nextTokenSeq < tokenSeq) + return Unexpected(tecMAX_SEQUENCE_REACHED); + + (*root)[sfMintedNFTokens] = + (*root)[~sfMintedNFTokens].value_or(0) + 1; + (*root)[sfFirstNFTokenSequence] = firstNFTokenSeq; + } + } + else + { + // Get the unique sequence number for this token: + tokenSeq = (*root)[~sfMintedNFTokens].value_or(0); + { + std::uint32_t const nextTokenSeq = tokenSeq + 1; + if (nextTokenSeq < tokenSeq) + return Unexpected(tecMAX_SEQUENCE_REACHED); + + (*root)[sfMintedNFTokens] = nextTokenSeq; + } } ctx_.view().update(root); return tokenSeq; diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 1b3f4871a08..291c7c65a15 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -341,7 +341,7 @@ extern uint256 const fixTrustLinesToSelf; extern uint256 const fixRemoveNFTokenAutoTrustLine; extern uint256 const featureImmediateOfferKilled; extern uint256 const featureDisallowIncoming; -extern uint256 const fixNFTokenRemint; +extern uint256 const fixUnburnableNFToken; } // namespace ripple diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 253d956408f..bcc23518ee6 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -400,6 +400,7 @@ extern SF_UINT32 const sfMintedNFTokens; extern SF_UINT32 const sfBurnedNFTokens; extern SF_UINT32 const sfHookStateCount; extern SF_UINT32 const sfEmitGeneration; +extern SF_UINT32 const sfFirstNFTokenSequence; // 64-bit integers (common) extern SF_UINT64 const sfIndexNext; diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 4496818546c..c797939dbb2 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -451,7 +451,7 @@ REGISTER_FIX (fixTrustLinesToSelf, Supported::yes, DefaultVote::no) REGISTER_FIX (fixRemoveNFTokenAutoTrustLine, Supported::yes, DefaultVote::yes); REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no); REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no); -REGISTER_FEATURE(fixNFTokenRemint, Supported::yes, DefaultVote::no); +REGISTER_FIX(fixUnburnableNFToken, Supported::yes, DefaultVote::no); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 7d5cf9d21aa..f77227c546f 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -55,6 +55,7 @@ LedgerFormats::LedgerFormats() {sfNFTokenMinter, soeOPTIONAL}, {sfMintedNFTokens, soeDEFAULT}, {sfBurnedNFTokens, soeDEFAULT}, + {sfFirstNFTokenSequence, soeOPTIONAL}, }, commonFields); diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 73098319b28..f548445548a 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -150,6 +150,7 @@ CONSTRUCT_TYPED_SFIELD(sfMintedNFTokens, "MintedNFTokens", UINT32, CONSTRUCT_TYPED_SFIELD(sfBurnedNFTokens, "BurnedNFTokens", UINT32, 44); CONSTRUCT_TYPED_SFIELD(sfHookStateCount, "HookStateCount", UINT32, 45); CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32, 46); +CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 47); // 64-bit integers (common) CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1); diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index a1d9880bdb9..73a0ccbf9e0 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -89,7 +89,7 @@ class AccountDelete_test : public beast::unit_test::suite incLgrSeqForAccDel( jtx::Env& env, jtx::Account const& acc, - std::uint32_t margin = 0.) + std::uint32_t margin = 0) { int const delta = [&]() -> int { if (env.seq(acc) + 255 > openLedgerSeq(env)) @@ -102,8 +102,6 @@ class AccountDelete_test : public beast::unit_test::suite BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); } - - public: void testBasics() @@ -909,126 +907,6 @@ class AccountDelete_test : public beast::unit_test::suite verifyDeliveredAmount(env, beckyOldBalance - acctDelFee); env.close(); } - void - testRemintAmendmentEnable() - { - // Start with the featureDeletableAccounts amendment disabled. - // Then enable the amendment and delete an account. - using namespace jtx; - - testcase("Remint Amendment enable"); - - Env env{*this, supported_amendments() - featureDeletableAccounts}; - Account const alice("alice"); - Account const becky("becky"); - - env.fund(XRP(10000), alice, becky); - env.close(); - - std::vector nftIDs; - nftIDs.reserve(200); - for(int i = 0; i<200; i++){ - uint256 const nftokenID = - token::getNextID(env, alice, 0, tfTransferable); - nftIDs.push_back(nftokenID); - env(token::mint(alice, 0), - token::uri(std::string(maxTokenURILength, 'u')), - txflags(tfTransferable)); - env.close(); - } - - for(auto const nftokenID: nftIDs){ - env(token::burn(alice, nftokenID)); - } - - env.close(); - - { - // Close enough ledgers to be able to delete alice's account. - incLgrSeqForAccDel(env, alice); - - // Verify that alice's account root is present. - Keylet const aliceAcctKey{keylet::account(alice.id())}; - BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); - - auto const alicePreDelBal{env.balance(alice)}; - auto const beckyPreDelBal{env.balance(becky)}; - - auto const acctDelFee{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee)); - env.close(); - - BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); - } - - { - env.enableFeature(featureDeletableAccounts); - env.close(); - - // Close enough ledgers to be able to delete alice's account. - incLgrSeqForAccDel(env, alice); - - // Verify that alice's account root is present. - Keylet const aliceAcctKey{keylet::account(alice.id())}; - BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); - - auto const alicePreDelBal{env.balance(alice)}; - auto const beckyPreDelBal{env.balance(becky)}; - - auto const acctDelFee1{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); - env.close(); - - BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - - auto const alicePreDelBal{env.balance(alice)}; - auto const beckyPreDelBal{env.balance(becky)}; - - // Verify that alice's account root is still present and alice and - // becky both have their XRP. - BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - BEAST_EXPECT(env.balance(alice) == alicePreDelBal); - BEAST_EXPECT(env.balance(becky) == beckyPreDelBal); - - // Close the ledger until the ledger sequence is large enough to close - // the account, which has minted NFTs - auto incLgrSeqForNFTokenAccDel = [&](jtx::Account const& acc) { - int delta = 0; - auto const deletableLgrSeq = (*env.le(acc))[sfSequence] + (*env.le(acc))[sfMintedNFTokens]+ 255; - - if(deletableLgrSeq > openLedgerSeq(env)) - delta = deletableLgrSeq - openLedgerSeq(env); - - BEAST_EXPECT(delta >= 0); - for (int i = 0; i < delta; ++i) - env.close(); - - BEAST_EXPECT(openLedgerSeq(env) == deletableLgrSeq + 255); - }; - - // Close more ledgers to be able to delete alice's account - incLgrSeqForNFTokenAccDel(alice); - - auto const acctDelFee2{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee2)); - env.close(); - - - // alice's account is still in the most recently closed ledger. - BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); - - // Verify that alice's account root is gone from the current ledger - // and becky has alice's XRP. - BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); - BEAST_EXPECT( - env.balance(becky) == alicePreDelBal + beckyPreDelBal - acctDelFee2); - - env.close(); - BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); - - } - - } void run() override diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 00124731cb9..3de7ba0119a 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -349,8 +349,20 @@ class NFTokenBurn_test : public beast::unit_test::suite auto internalTaxon = [&env]( Account const& acct, std::uint32_t taxon) -> std::uint32_t { - std::uint32_t const tokenSeq = { - env.le(acct)->at(~sfMintedNFTokens).value_or(0)}; + std::uint32_t tokenSeq; + + // If fixUnburnableNFToken amendment is on, we must + // generate the NFT sequence using new construct + if (env.current()->rules().enabled(fixUnburnableNFToken)) + tokenSeq = { + env.le(acct) + ->at(~sfFirstNFTokenSequence) + .value_or(env.seq(acct)) + + env.le(acct)->at(~sfMintedNFTokens).value_or(0)}; + else + tokenSeq = { + env.le(acct)->at(~sfMintedNFTokens).value_or(0)}; + return toUInt32( nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon))); }; @@ -599,6 +611,7 @@ class NFTokenBurn_test : public beast::unit_test::suite FeatureBitset const fixNFTDir{fixNFTokenDirV1}; testWithFeats(all - fixNFTDir); + testWithFeats(all - fixUnburnableNFToken); testWithFeats(all); } }; diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index d50bd1584d6..ef201c5d826 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -647,13 +647,23 @@ class NFTokenDir_test : public beast::unit_test::suite // Create accounts for all of the seeds and fund those accounts. std::vector accounts; accounts.reserve(seeds.size()); + + // If fixUnburnableNFToken is not enabled, accounts can be created in + // different ledgers. + // If fixUnburnableNFToken is enabled, all accounts must be created in the + // same ledger in order to initialize all accounts with the same + // account sequence. for (std::string_view const& seed : seeds) { Account const& account = accounts.emplace_back(Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - env.close(); + + // Only advance the ledger if fixUnburnableNFToken is disabled + if (!features[fixUnburnableNFToken]) + env.close(); } + env.close(); // All of the accounts create one NFT and and offer that NFT to buyer. std::vector nftIDs; @@ -822,13 +832,23 @@ class NFTokenDir_test : public beast::unit_test::suite // Create accounts for all of the seeds and fund those accounts. std::vector accounts; accounts.reserve(seeds.size()); + + // If fixUnburnableNFToken is not enabled, accounts can be created in + // different ledgers. + // If fixUnburnableNFToken is enabled, accounts must be created in the + // same ledger in order to initialize all accounts with the same + // account sequence. for (std::string_view const& seed : seeds) { Account const& account = accounts.emplace_back(Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - env.close(); + + // Only advance the ledger if fixUnburnableNFToken is disabled + if (!features[fixUnburnableNFToken]) + env.close(); } + env.close(); // All of the accounts create seven consecutive NFTs and and offer // those NFTs to buyer. @@ -1078,7 +1098,8 @@ class NFTokenDir_test : public beast::unit_test::suite FeatureBitset const fixNFTDir{ fixNFTokenDirV1, featureNonFungibleTokensV1_1}; - testWithFeats(all - fixNFTDir); + testWithFeats(all - fixNFTDir - fixUnburnableNFToken); + testWithFeats(all - fixUnburnableNFToken); testWithFeats(all); } }; diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 42a6eb4d3ce..a00fc4ecd00 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -215,8 +215,8 @@ class NFToken_test : public beast::unit_test::suite Account const minter{"minter"}; // Fund alice and minter enough to exist, but not enough to meet - // the reserve for creating their first NFT. Account reserve for unit - // tests is 200 XRP, not 20. + // the reserve for creating their first NFT. Account reserve for + // unit tests is 200 XRP, not 20. env.fund(XRP(200), alice, minter); env.close(); BEAST_EXPECT(env.balance(alice) == XRP(200)); @@ -224,7 +224,8 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(ownerCount(env, alice) == 0); BEAST_EXPECT(ownerCount(env, minter) == 0); - // alice does not have enough XRP to cover the reserve for an NFT page. + // alice does not have enough XRP to cover the reserve for an NFT + // page. env(token::mint(alice, 0u), ter(tecINSUFFICIENT_RESERVE)); env.close(); BEAST_EXPECT(ownerCount(env, alice) == 0); @@ -260,7 +261,8 @@ class NFToken_test : public beast::unit_test::suite oneCheck("burned", burnedCount(env, alice), burned); }; - // alice still does not have enough XRP for the reserve of an NFT page. + // alice still does not have enough XRP for the reserve of an NFT + // page. env(token::mint(alice, 0u), ter(tecINSUFFICIENT_RESERVE)); env.close(); checkAliceOwnerMintedBurned(0, 0, 0, __LINE__); @@ -292,7 +294,8 @@ class NFToken_test : public beast::unit_test::suite env(pay(env.master, alice, XRP(50) + drops(329))); env.close(); - // alice still does not have enough XRP for the reserve of an NFT page. + // alice still does not have enough XRP for the reserve of an NFT + // page. env(token::mint(alice), ter(tecINSUFFICIENT_RESERVE)); env.close(); checkAliceOwnerMintedBurned(1, 32, 0, __LINE__); @@ -311,7 +314,19 @@ class NFToken_test : public beast::unit_test::suite while (seq < 33) { - env(token::burn(alice, token::getID(alice, 0, seq++))); + if (features[fixUnburnableNFToken]) + // If fixUnburnableNFToken is enabled, we must add + // FirstNFTokenSequence to offset the starting NFT sequence + // number + env(token::burn( + alice, + token::getID( + alice, + 0, + (*env.le(alice))[sfFirstNFTokenSequence] + seq++))); + else + env(token::burn(alice, token::getID(alice, 0, seq++))); + env.close(); checkAliceOwnerMintedBurned((33 - seq) ? 1 : 0, 33, seq, __LINE__); } @@ -321,8 +336,9 @@ class NFToken_test : public beast::unit_test::suite env.close(); checkAliceOwnerMintedBurned(0, 33, 33, __LINE__); - // That was fun! Now let's see what happens when we let someone else - // mint NFTs on alice's behalf. alice gives permission to minter. + // That was fun! Now let's see what happens when we let someone + // else mint NFTs on alice's behalf. alice gives permission to + // minter. env(token::setMinter(alice, minter)); env.close(); BEAST_EXPECT( @@ -373,9 +389,9 @@ class NFToken_test : public beast::unit_test::suite env.close(); checkMintersOwnerMintedBurned(0, 33, nftSeq, 0, 0, 0, __LINE__); - // minter still does not have enough XRP for the reserve of an NFT page. - // Just for grins (and code coverage), minter mints NFTs that include - // a URI. + // minter still does not have enough XRP for the reserve of an NFT + // page. Just for grins (and code coverage), minter mints NFTs that + // include a URI. env(token::mint(minter), token::issuer(alice), token::uri("uri"), @@ -400,7 +416,8 @@ class NFToken_test : public beast::unit_test::suite checkMintersOwnerMintedBurned(0, i + 34, nftSeq, 1, 0, 0, __LINE__); } - // Pay minter almost enough for the reserve of an additional NFT page. + // Pay minter almost enough for the reserve of an additional NFT + // page. env(pay(env.master, minter, XRP(50) + drops(319))); env.close(); @@ -425,14 +442,37 @@ class NFToken_test : public beast::unit_test::suite // minter burns the NFTs she created. while (nftSeq < 65) { - env(token::burn(minter, token::getID(alice, 0, nftSeq++))); + if (features[fixUnburnableNFToken]) + // If fixUnburnableNFToken is enabled, we must add + // FirstNFTokenSequence to offset the starting NFT sequence + // number + env(token::burn( + minter, + token::getID( + alice, + 0, + (*env.le(alice))[sfFirstNFTokenSequence] + nftSeq++))); + else + env(token::burn(minter, token::getID(alice, 0, nftSeq++))); + env.close(); checkMintersOwnerMintedBurned( 0, 66, nftSeq, (65 - seq) ? 1 : 0, 0, 0, __LINE__); } - // minter has one more NFT to burn. Should take her owner count to 0. - env(token::burn(minter, token::getID(alice, 0, nftSeq++))); + // minter has one more NFT to burn. Should take her owner count to + // 0. + if (features[fixUnburnableNFToken]) + // If fixUnburnableNFToken is enabled, we must add FirstNFTokenSequence + // to offset the starting NFT sequence number + env(token::burn( + minter, + token::getID( + alice, + 0, + (*env.le(alice))[sfFirstNFTokenSequence] + nftSeq++))); + else + env(token::burn(minter, token::getID(alice, 0, nftSeq++))); env.close(); checkMintersOwnerMintedBurned(0, 66, nftSeq, 0, 0, 0, __LINE__); @@ -475,7 +515,7 @@ class NFToken_test : public beast::unit_test::suite // checks with this modify() call. If you call close() between // here and the end of the test all the effort will be lost. env.app().openLedger().modify( - [&alice](OpenView& view, beast::Journal j) { + [&alice, &env](OpenView& view, beast::Journal j) { // Get the account root we want to hijack. auto const sle = view.read(keylet::account(alice.id())); if (!sle) @@ -487,8 +527,23 @@ class NFToken_test : public beast::unit_test::suite if (replacement->getFieldU32(sfMintedNFTokens) != 1) return false; // Unexpected test conditions. - // Now replace sfMintedNFTokens with the largest valid value. - (*replacement)[sfMintedNFTokens] = 0xFFFF'FFFE; + if (env.current()->rules().enabled(fixUnburnableNFToken)) + { + // If fixUnburnableNFToken is enabled, sequence number is + // generated by sfFirstNFTokenSequence + sfMintedNFTokens. + // We can replace the two fields with any numbers as long as + // they add up to the largest valid number. In our case, + // sfFirstNFTokenSequence is set to the largest valid + // number, and sfMintedNFTokens is set to zero. + (*replacement)[sfFirstNFTokenSequence] = 0xFFFF'FFFE; + (*replacement)[sfMintedNFTokens] = 0x0000'0000; + } + else + { + // Now replace sfMintedNFTokens with the largest valid + // value. + (*replacement)[sfMintedNFTokens] = 0xFFFF'FFFE; + } view.rawReplace(replacement); return true; }); @@ -5041,6 +5096,476 @@ class NFToken_test : public beast::unit_test::suite } } + void + testFixNFTokenRemint(FeatureBitset features) + { + using namespace test::jtx; + + testcase("testfixNFTokenRemint"); + + // Returns the current ledger sequence + auto openLedgerSeq = [](Env& env) { return env.current()->seq(); }; + + // Close the ledger until the ledger sequence is large enough to close + // the account (no longer within ) + // This is enforced by the featureDeletableAccounts amendment + auto incLgrSeqForAccDel = [&](Env& env, Account const& acc) { + int const delta = [&]() -> int { + if (env.seq(acc) + 255 > openLedgerSeq(env)) + return env.seq(acc) - openLedgerSeq(env) + 255; + return 0; + }(); + BEAST_EXPECT(delta >= 0); + for (int i = 0; i < delta; ++i) + env.close(); + BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255); + }; + + // Close the ledger until the ledger sequence is no longer + // within . + // This is enforced by the fixUnburnableNFToken amendment. + auto incLgrSeqForNFTokenAccDel = [&](Env& env, Account const& acc) { + int delta = 0; + auto const deletableLgrSeq = + (*env.le(acc))[~sfFirstNFTokenSequence].value_or(0) + + (*env.le(acc))[sfMintedNFTokens] + 255; + + if (deletableLgrSeq > openLedgerSeq(env)) + delta = deletableLgrSeq - openLedgerSeq(env); + + BEAST_EXPECT(delta >= 0); + for (int i = 0; i < delta; ++i) + env.close(); + BEAST_EXPECT(openLedgerSeq(env) == deletableLgrSeq); + }; + + // If fixUnburnableNFToken is not enabled, we test if the issuer account can + // be deleted after an authorized minter mints and burns a batch of + // NFTokens. + if (!features[fixUnburnableNFToken]) + { + Env env{*this, features}; + Account const alice("alice"); + Account const becky("becky"); + Account const minter{"minter"}; + + env.fund(XRP(10000), alice, becky, minter); + env.close(); + + // alice sets minter as her authorized minter + env(token::setMinter(alice, minter)); + env.close(); + + // minter mints 500 NFTs for alice + std::vector nftIDs; + nftIDs.reserve(500); + for (int i = 0; i < 500; i++) + { + uint256 const nftokenID = token::getNextID(env, alice, 0u); + nftIDs.push_back(nftokenID); + env(token::mint(minter), token::issuer(alice)); + } + env.close(); + + // minter burns 500 NFTs + for (auto const nftokenID : nftIDs) + { + env(token::burn(minter, nftokenID)); + } + env.close(); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + auto const acctDelFee1{drops(env.current()->fees().increment)}; + + // alice's account can be successfully deleted. + env(acctdelete(alice, becky), fee(acctDelFee1)); + env.close(); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + } + + // If fixUnburnableNFToken is not enabled, + // when an account mints and burns a batch of NFTokens using tickets, + // the account should be able to be deleted. + if (!features[fixUnburnableNFToken]) + { + Env env{*this, features}; + + Account const alice{"alice"}; + Account const becky{"becky"}; + env.fund(XRP(10000), alice, becky); + env.close(); + + // alice grab enough tickets for all of the following + // transactions. Note that once the tickets are acquired alice's + // account sequence number should not advance. + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 100)); + env.close(); + + BEAST_EXPECT(ticketCount(env, alice) == 100); + BEAST_EXPECT(ownerCount(env, alice) == 100); + + // alice mints 50 NFTs using tickets + std::vector nftIDs; + nftIDs.reserve(50); + for (int i = 0; i < 50; i++) + { + nftIDs.push_back( + token::getNextID(env, alice, 0u, tfTransferable)); + env(token::mint(alice, 0u), + txflags(tfTransferable), + ticket::use(aliceTicketSeq++)); + env.close(); + } + + // alice burns 50 NFTs using tickets + for (auto const nftokenID : nftIDs) + { + env(token::burn(alice, nftokenID), + ticket::use(aliceTicketSeq++)); + } + env.close(); + + BEAST_EXPECT(ticketCount(env, alice) == 0); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // alice tries to delete her account, and is successful. + auto const acctDelFee1{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee1)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + + // Verify that alice's account root is gone from the current ledger + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + } + + // If fixUnburnableNFToken is enabled, + // when an authorized minter mints and burns a batch of NFTokens, + // issuer's account needs to wait a longer time before it can deleted + if (features[fixUnburnableNFToken]) + { + Env env{*this, features}; + Account const alice("alice"); + Account const becky("becky"); + Account const minter{"minter"}; + + env.fund(XRP(10000), alice, becky, minter); + env.close(); + + // alice sets minter as her authorized minter + env(token::setMinter(alice, minter)); + env.close(); + + // minter mints 500 NFTs for alice + std::vector nftIDs; + nftIDs.reserve(500); + for (int i = 0; i < 500; i++) + { + uint256 const nftokenID = token::getNextID(env, alice, 0u); + nftIDs.push_back(nftokenID); + env(token::mint(minter), token::issuer(alice)); + } + env.close(); + + // minter burns 500 NFTs + for (auto const nftokenID : nftIDs) + { + env(token::burn(minter, nftokenID)); + } + env.close(); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // alice tries to delete her account, but is unsuccessful. + // Due to authorized minting, alice's account sequence does not + // advance while minter mints NFTokens for her. + // The new account deletion retriction enabled by this amendment will enforce + // alice to wait for more ledgers to close before she can delete her + // account, to prevent duplicate NFTokenIDs + auto const acctDelFee1{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); + env.close(); + + // alice's account is still present + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // Close more ledgers until it is no longer within + // + // to be able to delete alice's account + incLgrSeqForNFTokenAccDel(env, alice); + + // alice's account is deleted + auto const acctDelFee2{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee2)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + + // Verify that alice's account root is gone from the current ledger + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + } + + // If fixUnburnableNFToken is enabled, + // when an account mints and burns a batch of NFTokens using tickets, + // the account needs to wait a longer time before it can deleted + if (features[fixUnburnableNFToken]) + { + Env env{*this, features}; + + Account const alice{"alice"}; + Account const becky{"becky"}; + env.fund(XRP(10000), alice, becky); + env.close(); + + // alice grab enough tickets for all of the following + // transactions. Note that once the tickets are acquired alice's + // account sequence number should not advance. + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 100)); + env.close(); + + BEAST_EXPECT(ticketCount(env, alice) == 100); + BEAST_EXPECT(ownerCount(env, alice) == 100); + + // alice mints 50 NFTs using tickets + std::vector nftIDs; + nftIDs.reserve(50); + for (int i = 0; i < 50; i++) + { + nftIDs.push_back( + token::getNextID(env, alice, 0u, tfTransferable)); + env(token::mint(alice, 0u), + txflags(tfTransferable), + ticket::use(aliceTicketSeq++)); + env.close(); + } + + // alice burns 50 NFTs using tickets + for (auto const nftokenID : nftIDs) + { + env(token::burn(alice, nftokenID), + ticket::use(aliceTicketSeq++)); + } + env.close(); + + BEAST_EXPECT(ticketCount(env, alice) == 0); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // alice tries to delete her account, but is unsuccessful. + // Because alice used tickets to mint and burn NFTs, her account + // sequence did not change while while submitting these + // transactions. Hence, her is still greater than the current ledger sequence + auto const acctDelFee1{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); + env.close(); + + // Close more ledgers until it is no longer within + // + // to be able to delete alice's account + incLgrSeqForNFTokenAccDel(env, alice); + + // alice's account is deleted + auto const acctDelFee2{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee2)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + + // Verify that alice's account root is gone from the current ledger + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + } + + // If fixUnburnableNFToken is enabled, + // when an authorized minter mints and burns a batch of NFTokens using + // tickets, issuer's account needs to wait a longer time before it can + // deleted + if (features[fixUnburnableNFToken]) + { + Env env{*this, features}; + Account const alice("alice"); + Account const becky("becky"); + Account const minter{"minter"}; + + env.fund(XRP(10000), alice, becky, minter); + env.close(); + + // alice sets minter as her authorized minter + env(token::setMinter(alice, minter)); + env.close(); + + // minter creates 100 tickets + std::uint32_t minterTicketSeq{env.seq(minter) + 1}; + env(ticket::create(minter, 100)); + env.close(); + + BEAST_EXPECT(ticketCount(env, minter) == 100); + BEAST_EXPECT(ownerCount(env, minter) == 100); + + // minter mints 50 NFTs for alice using tickets + std::vector nftIDs; + nftIDs.reserve(50); + for (int i = 0; i < 50; i++) + { + uint256 const nftokenID = token::getNextID(env, alice, 0u); + nftIDs.push_back(nftokenID); + env(token::mint(minter), + token::issuer(alice), + ticket::use(minterTicketSeq++)); + } + env.close(); + + // minter burns 50 NFTs using tickets + for (auto const nftokenID : nftIDs) + { + env(token::burn(minter, nftokenID), + ticket::use(minterTicketSeq++)); + } + env.close(); + + BEAST_EXPECT(ticketCount(env, minter) == 0); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // alice tries to delete her account, but is unsuccessful. + // Due to authorized minting, alice's account sequence does not + // advance while minter mints NFTokens for her using tickets. + // The new account deletion retriction enabled by this amendment will enforce + // alice to wait for more ledgers to close before she can delete her + // account, to prevent duplicate NFTokenIDs + auto const acctDelFee1{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); + env.close(); + + // alice's account is still present + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // Close more ledgers until it is no longer within + // + // to be able to delete alice's account + incLgrSeqForNFTokenAccDel(env, alice); + + // alice's account is deleted + auto const acctDelFee2{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee2)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + + // Verify that alice's account root is gone from the current ledger + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + } + + // We check if NFTokenIDs can be duplicated by + // re-creation of an account + { + Env env{*this, features}; + Account const alice("alice"); + Account const becky("becky"); + + env.fund(XRP(10000), alice, becky); + env.close(); + + // alice mint and burn a NFT + uint256 const prevNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + env(token::burn(alice, prevNftokenID)); + env.close(); + + // alice has minted 1 NFToken + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 1); + + // Close enough ledgers to delete alice's account + incLgrSeqForAccDel(env, alice); + + // alice's account is deleted + Keylet const aliceAcctKey{keylet::account(alice.id())}; + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + if(features[fixUnburnableNFToken]) + // Check that two NFTs don't have the same ID + BEAST_EXPECT(remintNftokenID != prevNftokenID); + else + // Check that two NFTs have the same ID + BEAST_EXPECT(remintNftokenID == prevNftokenID); + } + } + void testWithFeats(FeatureBitset features) { @@ -5070,6 +5595,7 @@ class NFToken_test : public beast::unit_test::suite testNFTokenDeleteAccount(features); testNftXxxOffers(features); testFixNFTokenNegOffer(features); + testFixNFTokenRemint(features); } public: @@ -5082,6 +5608,7 @@ class NFToken_test : public beast::unit_test::suite testWithFeats(all - fixNFTDir); testWithFeats(all - disallowIncoming); + testWithFeats(all - fixUnburnableNFToken); testWithFeats(all); } }; diff --git a/src/test/jtx/impl/token.cpp b/src/test/jtx/impl/token.cpp index cfbcfe11c98..dcb0b19f9cc 100644 --- a/src/test/jtx/impl/token.cpp +++ b/src/test/jtx/impl/token.cpp @@ -65,10 +65,20 @@ getNextID( std::uint16_t flags, std::uint16_t xferFee) { - // Get the nftSeq from the account root of the issuer. - std::uint32_t const nftSeq = { - env.le(issuer)->at(~sfMintedNFTokens).value_or(0)}; - return getID(issuer, nfTokenTaxon, nftSeq, flags, xferFee); + std::uint32_t nftSeq; + + // If fixUnburnableNFToken amendment is on, we must + // generate the NFT sequence using new construct + if (env.current()->rules().enabled(fixUnburnableNFToken)) + nftSeq = { + env.le(issuer) + ->at(~sfFirstNFTokenSequence) + .value_or(env.seq(issuer)) + + env.le(issuer)->at(~sfMintedNFTokens).value_or(0)}; + else + nftSeq = {env.le(issuer)->at(~sfMintedNFTokens).value_or(0)}; + + return token::getID(issuer, nfTokenTaxon, nftSeq, flags, xferFee); } uint256 From dc4f146cc1a106223a96b11999b8700d0c707708 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 1 Feb 2023 10:31:03 -0500 Subject: [PATCH 05/38] clang --- src/test/app/NFTokenDir_test.cpp | 4 ++-- src/test/app/NFToken_test.cpp | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index ef201c5d826..6a1a7f1f953 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -650,8 +650,8 @@ class NFTokenDir_test : public beast::unit_test::suite // If fixUnburnableNFToken is not enabled, accounts can be created in // different ledgers. - // If fixUnburnableNFToken is enabled, all accounts must be created in the - // same ledger in order to initialize all accounts with the same + // If fixUnburnableNFToken is enabled, all accounts must be created in + // the same ledger in order to initialize all accounts with the same // account sequence. for (std::string_view const& seed : seeds) { diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index a00fc4ecd00..4ce9717b550 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -463,8 +463,8 @@ class NFToken_test : public beast::unit_test::suite // minter has one more NFT to burn. Should take her owner count to // 0. if (features[fixUnburnableNFToken]) - // If fixUnburnableNFToken is enabled, we must add FirstNFTokenSequence - // to offset the starting NFT sequence number + // If fixUnburnableNFToken is enabled, we must add + // FirstNFTokenSequence to offset the starting NFT sequence number env(token::burn( minter, token::getID( @@ -5139,8 +5139,8 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(openLedgerSeq(env) == deletableLgrSeq); }; - // If fixUnburnableNFToken is not enabled, we test if the issuer account can - // be deleted after an authorized minter mints and burns a batch of + // If fixUnburnableNFToken is not enabled, we test if the issuer account + // can be deleted after an authorized minter mints and burns a batch of // NFTokens. if (!features[fixUnburnableNFToken]) { @@ -5557,7 +5557,7 @@ class NFToken_test : public beast::unit_test::suite env(token::burn(alice, remintNftokenID)); env.close(); - if(features[fixUnburnableNFToken]) + if (features[fixUnburnableNFToken]) // Check that two NFTs don't have the same ID BEAST_EXPECT(remintNftokenID != prevNftokenID); else From 6211841bb4fb7987140bdd33daadc5173efe4cb3 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 1 Feb 2023 11:45:46 -0500 Subject: [PATCH 06/38] update test --- src/ripple/app/tx/impl/DeleteAccount.cpp | 4 +- src/test/app/NFTokenBurn_test.cpp | 2 +- src/test/app/NFTokenDir_test.cpp | 20 +- src/test/app/NFToken_test.cpp | 252 +++++++++++++++++------ 4 files changed, 204 insertions(+), 74 deletions(-) diff --git a/src/ripple/app/tx/impl/DeleteAccount.cpp b/src/ripple/app/tx/impl/DeleteAccount.cpp index 7aaf87d070d..51d42fff59a 100644 --- a/src/ripple/app/tx/impl/DeleteAccount.cpp +++ b/src/ripple/app/tx/impl/DeleteAccount.cpp @@ -236,11 +236,9 @@ DeleteAccount::preclaim(PreclaimContext const& ctx) // authorized minter minted in a previous ledger. if (ctx.view.rules().enabled(fixUnburnableNFToken) && ((*sleAccount)[~sfFirstNFTokenSequence].value_or(0) + - (*sleAccount)[sfMintedNFTokens] + seqDelta > + (*sleAccount)[~sfMintedNFTokens].value_or(0) + seqDelta > ctx.view.seq())) - { return tecTOO_SOON; - } // Verify that the account does not own any objects that would prevent // the account from being deleted. diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 3de7ba0119a..1b44b3ec9c4 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -610,7 +610,7 @@ class NFTokenBurn_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixNFTDir); + testWithFeats(all - fixNFTDir - fixUnburnableNFToken); testWithFeats(all - fixUnburnableNFToken); testWithFeats(all); } diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index 6a1a7f1f953..6c17869b779 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -190,8 +190,16 @@ class NFTokenDir_test : public beast::unit_test::suite Account const& account = accounts.emplace_back( Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - env.close(); + + // Should not advance ledger if fixUnburnableNFToken is + // enabled. Because otherwise, accounts are initialized at + // different ledgers, and will have different account + // sequences, which then cause the sequence number of + // NFTokenIDs to be different + if (!features[fixUnburnableNFToken]) + env.close(); } + env.close(); // All of the accounts create one NFT and and offer that NFT to // buyer. @@ -408,8 +416,16 @@ class NFTokenDir_test : public beast::unit_test::suite Account const& account = accounts.emplace_back( Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - env.close(); + + // Should not advance ledger if fixUnburnableNFToken is + // enabled. Because otherwise, accounts are initialized at + // different ledgers, and will have different account + // sequences, which then cause the sequence number of + // NFTokenIDs to be different + if (!features[fixUnburnableNFToken]) + env.close(); } + env.close(); // All of the accounts create one NFT and and offer that NFT to // buyer. diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 4ce9717b550..ada1948c9b8 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -5101,7 +5101,7 @@ class NFToken_test : public beast::unit_test::suite { using namespace test::jtx; - testcase("testfixNFTokenRemint"); + testcase("fixNFTokenRemint"); // Returns the current ledger sequence auto openLedgerSeq = [](Env& env) { return env.current()->seq(); }; @@ -5139,9 +5139,71 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(openLedgerSeq(env) == deletableLgrSeq); }; + // We check if NFTokenIDs can be duplicated by + // re-creation of an account + { + Env env{*this, features}; + Account const alice("alice"); + Account const becky("becky"); + + env.fund(XRP(10000), alice, becky); + env.close(); + + // alice mint and burn a NFT + uint256 const prevNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + env(token::burn(alice, prevNftokenID)); + env.close(); + + // alice has minted 1 NFToken + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 1); + + // Close enough ledgers to delete alice's account + incLgrSeqForAccDel(env, alice); + + // alice's account is deleted + Keylet const aliceAcctKey{keylet::account(alice.id())}; + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + if (features[fixUnburnableNFToken]) + // Check that two NFTs don't have the same ID + BEAST_EXPECT(remintNftokenID != prevNftokenID); + else + // Check that two NFTs have the same ID + BEAST_EXPECT(remintNftokenID == prevNftokenID); + } + // If fixUnburnableNFToken is not enabled, we test if the issuer account // can be deleted after an authorized minter mints and burns a batch of // NFTokens. + // After the issuer's account is re-created and mints a NFT, it should + // have the same NFTokenID as the one minted before. if (!features[fixUnburnableNFToken]) { Env env{*this, features}; @@ -5189,11 +5251,37 @@ class NFToken_test : public beast::unit_test::suite env(acctdelete(alice, becky), fee(acctDelFee1)); env.close(); BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + // The new NFT minted has the same ID as one of the NFTs + // authorized minter minted for alice + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) != + nftIDs.end()); } // If fixUnburnableNFToken is not enabled, // when an account mints and burns a batch of NFTokens using tickets, // the account should be able to be deleted. + // After the issuer's account is re-created and mints a NFT, it should + // have the same NFTokenID as the one minted before. if (!features[fixUnburnableNFToken]) { Env env{*this, features}; @@ -5218,11 +5306,8 @@ class NFToken_test : public beast::unit_test::suite nftIDs.reserve(50); for (int i = 0; i < 50; i++) { - nftIDs.push_back( - token::getNextID(env, alice, 0u, tfTransferable)); - env(token::mint(alice, 0u), - txflags(tfTransferable), - ticket::use(aliceTicketSeq++)); + nftIDs.push_back(token::getNextID(env, alice, 0u)); + env(token::mint(alice, 0u), ticket::use(aliceTicketSeq++)); env.close(); } @@ -5251,16 +5336,40 @@ class NFToken_test : public beast::unit_test::suite env.close(); // alice's account account root is gone from the most recently - // closed ledger. + // closed ledger and the current ledger. BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); - - // Verify that alice's account root is gone from the current ledger BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + // The new NFT minted will have the same ID + // as one of NFTs minted using tickets + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) != + nftIDs.end()); } // If fixUnburnableNFToken is enabled, // when an authorized minter mints and burns a batch of NFTokens, - // issuer's account needs to wait a longer time before it can deleted + // issuer's account needs to wait a longer time before it can deleted. + // After the issuer's account is re-created and mints a NFT, it should + // not have the same NFTokenID as the ones authorized minter minted. if (features[fixUnburnableNFToken]) { Env env{*this, features}; @@ -5327,16 +5436,40 @@ class NFToken_test : public beast::unit_test::suite env.close(); // alice's account account root is gone from the most recently - // closed ledger. + // closed ledger and the current ledger. BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); - - // Verify that alice's account root is gone from the current ledger BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + // The new NFT minted will not have the same ID + // as one of NFTs authorized minter minted + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) == + nftIDs.end()); } // If fixUnburnableNFToken is enabled, // when an account mints and burns a batch of NFTokens using tickets, - // the account needs to wait a longer time before it can deleted + // the account needs to wait a longer time before it can deleted. + // After the issuer's account is re-created and mints a NFT, it should + // not have the same NFTokenID as the ones minted using tickets. if (features[fixUnburnableNFToken]) { Env env{*this, features}; @@ -5361,11 +5494,8 @@ class NFToken_test : public beast::unit_test::suite nftIDs.reserve(50); for (int i = 0; i < 50; i++) { - nftIDs.push_back( - token::getNextID(env, alice, 0u, tfTransferable)); - env(token::mint(alice, 0u), - txflags(tfTransferable), - ticket::use(aliceTicketSeq++)); + nftIDs.push_back(token::getNextID(env, alice, 0u)); + env(token::mint(alice, 0u), ticket::use(aliceTicketSeq++)); env.close(); } @@ -5408,17 +5538,41 @@ class NFToken_test : public beast::unit_test::suite env.close(); // alice's account account root is gone from the most recently - // closed ledger. + // closed ledger and the current ledger. BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); - - // Verify that alice's account root is gone from the current ledger BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + // The new NFT minted will not have the same ID + // as one of NFTs alice minted using tickets + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) == + nftIDs.end()); } // If fixUnburnableNFToken is enabled, // when an authorized minter mints and burns a batch of NFTokens using // tickets, issuer's account needs to wait a longer time before it can - // deleted + // deleted. + // After the issuer's account is re-created and mints a NFT, it should + // not have the same NFTokenID as the ones authorized minter minted. if (features[fixUnburnableNFToken]) { Env env{*this, features}; @@ -5497,43 +5651,6 @@ class NFToken_test : public beast::unit_test::suite env(acctdelete(alice, becky), fee(acctDelFee2)); env.close(); - // alice's account account root is gone from the most recently - // closed ledger. - BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); - - // Verify that alice's account root is gone from the current ledger - BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); - } - - // We check if NFTokenIDs can be duplicated by - // re-creation of an account - { - Env env{*this, features}; - Account const alice("alice"); - Account const becky("becky"); - - env.fund(XRP(10000), alice, becky); - env.close(); - - // alice mint and burn a NFT - uint256 const prevNftokenID = token::getNextID(env, alice, 0u); - env(token::mint(alice)); - env.close(); - env(token::burn(alice, prevNftokenID)); - env.close(); - - // alice has minted 1 NFToken - BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 1); - - // Close enough ledgers to delete alice's account - incLgrSeqForAccDel(env, alice); - - // alice's account is deleted - Keylet const aliceAcctKey{keylet::account(alice.id())}; - auto const acctDelFee{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee)); - env.close(); - // alice's account account root is gone from the most recently // closed ledger and the current ledger. BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); @@ -5557,12 +5674,11 @@ class NFToken_test : public beast::unit_test::suite env(token::burn(alice, remintNftokenID)); env.close(); - if (features[fixUnburnableNFToken]) - // Check that two NFTs don't have the same ID - BEAST_EXPECT(remintNftokenID != prevNftokenID); - else - // Check that two NFTs have the same ID - BEAST_EXPECT(remintNftokenID == prevNftokenID); + // The new NFT minted will not have the same ID + // as one of NFTs authorized minter minted using tickets + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) == + nftIDs.end()); } } @@ -5606,8 +5722,8 @@ class NFToken_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixNFTDir); - testWithFeats(all - disallowIncoming); + testWithFeats(all - fixNFTDir - fixUnburnableNFToken); + testWithFeats(all - disallowIncoming - fixUnburnableNFToken); testWithFeats(all - fixUnburnableNFToken); testWithFeats(all); } From fa19aa250414af44504acf7f5f584a0e10f1aeb6 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Thu, 2 Feb 2023 18:48:03 -0500 Subject: [PATCH 07/38] [Amendment] Allow NFT to be burned when number of offers is greater than 500 (#4346) * Allow offers to be removable * Delete sell offers first Signed-off-by: Shawn Xie --- src/ripple/app/tx/impl/NFTokenBurn.cpp | 46 ++- .../app/tx/impl/details/NFTokenUtils.cpp | 67 ++-- src/ripple/app/tx/impl/details/NFTokenUtils.h | 10 +- src/test/app/NFTokenBurn_test.cpp | 323 ++++++++++++++---- 4 files changed, 347 insertions(+), 99 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenBurn.cpp b/src/ripple/app/tx/impl/NFTokenBurn.cpp index da23d78bdbd..e8693c7c6fb 100644 --- a/src/ripple/app/tx/impl/NFTokenBurn.cpp +++ b/src/ripple/app/tx/impl/NFTokenBurn.cpp @@ -77,9 +77,14 @@ NFTokenBurn::preclaim(PreclaimContext const& ctx) } } - // If there are too many offers, then burning the token would produce too - // much metadata. Disallow burning a token with too many offers. - return nft::notTooManyOffers(ctx.view, ctx.tx[sfNFTokenID]); + if (!ctx.view.rules().enabled(fixUnburnableNFToken)) + { + // If there are too many offers, then burning the token would produce + // too much metadata. Disallow burning a token with too many offers. + return nft::notTooManyOffers(ctx.view, ctx.tx[sfNFTokenID]); + } + + return tesSUCCESS; } TER @@ -104,9 +109,38 @@ NFTokenBurn::doApply() view().update(issuer); } - // Optimized deletion of all offers. - nft::removeAllTokenOffers(view(), keylet::nft_sells(ctx_.tx[sfNFTokenID])); - nft::removeAllTokenOffers(view(), keylet::nft_buys(ctx_.tx[sfNFTokenID])); + if (ctx_.view().rules().enabled(fixUnburnableNFToken)) + { + // Delete up to 500 offers in total. + // Because the number of sell offers is likely to be less than + // the number of buy offers, we prioritize the deletion of sell + // offers in order to clean up sell offer directory + std::size_t const deletedSellOffers = nft::removeTokenOffersWithLimit( + view(), + keylet::nft_sells(ctx_.tx[sfNFTokenID]), + maxDeletableTokenOfferEntries); + + if (maxDeletableTokenOfferEntries > deletedSellOffers) + { + nft::removeTokenOffersWithLimit( + view(), + keylet::nft_buys(ctx_.tx[sfNFTokenID]), + maxDeletableTokenOfferEntries - deletedSellOffers); + } + } + else + { + // Deletion of all offers. + nft::removeTokenOffersWithLimit( + view(), + keylet::nft_sells(ctx_.tx[sfNFTokenID]), + std::numeric_limits::max()); + + nft::removeTokenOffersWithLimit( + view(), + keylet::nft_buys(ctx_.tx[sfNFTokenID]), + std::numeric_limits::max()); + } return tesSUCCESS; } diff --git a/src/ripple/app/tx/impl/details/NFTokenUtils.cpp b/src/ripple/app/tx/impl/details/NFTokenUtils.cpp index d1214a98ee8..db2c3ae62f7 100644 --- a/src/ripple/app/tx/impl/details/NFTokenUtils.cpp +++ b/src/ripple/app/tx/impl/details/NFTokenUtils.cpp @@ -520,34 +520,55 @@ findTokenAndPage( } return std::nullopt; } -void -removeAllTokenOffers(ApplyView& view, Keylet const& directory) -{ - view.dirDelete(directory, [&view](uint256 const& id) { - auto offer = view.peek(Keylet{ltNFTOKEN_OFFER, id}); - if (!offer) - Throw( - "Offer " + to_string(id) + " not found in ledger!"); +std::size_t +removeTokenOffersWithLimit( + ApplyView& view, + Keylet const& directory, + std::size_t maxDeletableOffers) +{ + if (maxDeletableOffers == 0) + return 0; - auto const owner = (*offer)[sfOwner]; + std::optional pageIndex{0}; + std::size_t deletedOffersCount = 0; - if (!view.dirRemove( - keylet::ownerDir(owner), - (*offer)[sfOwnerNode], - offer->key(), - false)) - Throw( - "Offer " + to_string(id) + " not found in owner directory!"); + do + { + auto const page = view.peek(keylet::page(directory, *pageIndex)); + if (!page) + break; + + // We get the index of the next page in case the current + // page is deleted after all of its entries have been removed + pageIndex = (*page)[~sfIndexNext]; + + auto offerIndexes = page->getFieldV256(sfIndexes); + + // We reverse-iterate the offer directory page to delete all entries. + // Deleting an entry in a NFTokenOffer directory page won't cause + // entries from other pages to move to the current, so, it is safe to + // delete entries one by one in the page. It is required to iterate + // backwards to handle iterator invalidation for vector, as we are + // deleting during iteration. + for (int i = offerIndexes.size() - 1; i >= 0; --i) + { + if (auto const offer = view.peek(keylet::nftoffer(offerIndexes[i]))) + { + if (deleteTokenOffer(view, offer)) + ++deletedOffersCount; + else + Throw( + "Offer " + to_string(offerIndexes[i]) + + " cannot be deleted!"); + } - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - -1, - beast::Journal{beast::Journal::getNullSink()}); + if (maxDeletableOffers == deletedOffersCount) + break; + } + } while (pageIndex.value_or(0) && maxDeletableOffers != deletedOffersCount); - view.erase(offer); - }); + return deletedOffersCount; } TER diff --git a/src/ripple/app/tx/impl/details/NFTokenUtils.h b/src/ripple/app/tx/impl/details/NFTokenUtils.h index fa8c43b5877..db7cf00be10 100644 --- a/src/ripple/app/tx/impl/details/NFTokenUtils.h +++ b/src/ripple/app/tx/impl/details/NFTokenUtils.h @@ -53,9 +53,13 @@ constexpr std::uint16_t const flagOnlyXRP = 0x0002; constexpr std::uint16_t const flagCreateTrustLines = 0x0004; constexpr std::uint16_t const flagTransferable = 0x0008; -/** Deletes all offers from the specified token offer directory. */ -void -removeAllTokenOffers(ApplyView& view, Keylet const& directory); +/** Delete up to a specified number of offers from the specified token offer + * directory. */ +std::size_t +removeTokenOffersWithLimit( + ApplyView& view, + Keylet const& directory, + std::size_t maxDeletableOffers); /** Returns tesSUCCESS if NFToken has few enough offers that it can be burned */ TER diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 00124731cb9..4896932acd2 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -49,6 +49,37 @@ class NFTokenBurn_test : public beast::unit_test::suite return nfts[jss::result][jss::account_nfts].size(); }; + // Helper function that returns new nft id for an account and create + // specified number of sell offers + uint256 + createNftAndOffers( + test::jtx::Env& env, + test::jtx::Account const& owner, + std::vector& offerIndexes, + size_t const tokenCancelCount) + { + using namespace test::jtx; + uint256 const nftokenID = + token::getNextID(env, owner, 0, tfTransferable); + env(token::mint(owner, 0), + token::uri(std::string(maxTokenURILength, 'u')), + txflags(tfTransferable)); + env.close(); + + offerIndexes.reserve(tokenCancelCount); + + for (uint32_t i = 0; i < tokenCancelCount; ++i) + { + // Create sell offer + offerIndexes.push_back(keylet::nftoffer(owner, env.seq(owner)).key); + env(token::createOffer(owner, nftokenID, drops(1)), + txflags(tfSellNFToken)); + env.close(); + } + + return nftokenID; + }; + void testBurnRandom(FeatureBitset features) { @@ -492,94 +523,251 @@ class NFTokenBurn_test : public beast::unit_test::suite using namespace test::jtx; - Env env{*this, features}; + // Test what happens if a NFT is unburnable when there are + // more than 500 offers, before fixUnburnableNFToken goes live + if (!features[fixUnburnableNFToken]) + { + Env env{*this, features}; - Account const alice("alice"); - Account const becky("becky"); - env.fund(XRP(1000), alice, becky); - env.close(); + Account const alice("alice"); + Account const becky("becky"); + env.fund(XRP(1000), alice, becky); + env.close(); - // We structure the test to try and maximize the metadata produced. - // This verifies that we don't create too much metadata during a - // maximal burn operation. - // - // 1. alice mints an nft with a full-sized URI. - // 2. We create 1000 new accounts, each of which creates an offer for - // alice's nft. - // 3. becky creates one more offer for alice's NFT - // 4. Attempt to burn the nft which fails because there are too - // many offers. - // 5. Cancel becky's offer and the nft should become burnable. - uint256 const nftokenID = - token::getNextID(env, alice, 0, tfTransferable); - env(token::mint(alice, 0), - token::uri(std::string(maxTokenURILength, 'u')), - txflags(tfTransferable)); - env.close(); + // We structure the test to try and maximize the metadata produced. + // This verifies that we don't create too much metadata during a + // maximal burn operation. + // + // 1. alice mints an nft with a full-sized URI. + // 2. We create 500 new accounts, each of which creates an offer + // for alice's nft. + // 3. becky creates one more offer for alice's NFT + // 4. Attempt to burn the nft which fails because there are too + // many offers. + // 5. Cancel becky's offer and the nft should become burnable. + uint256 const nftokenID = + token::getNextID(env, alice, 0, tfTransferable); + env(token::mint(alice, 0), + token::uri(std::string(maxTokenURILength, 'u')), + txflags(tfTransferable)); + env.close(); - std::vector offerIndexes; - offerIndexes.reserve(maxTokenOfferCancelCount); - for (uint32_t i = 0; i < maxTokenOfferCancelCount; ++i) + std::vector offerIndexes; + offerIndexes.reserve(maxTokenOfferCancelCount); + for (std::uint32_t i = 0; i < maxTokenOfferCancelCount; ++i) + { + Account const acct(std::string("acct") + std::to_string(i)); + env.fund(XRP(1000), acct); + env.close(); + + offerIndexes.push_back( + keylet::nftoffer(acct, env.seq(acct)).key); + env(token::createOffer(acct, nftokenID, drops(1)), + token::owner(alice)); + env.close(); + } + + // Verify all offers are present in the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); + } + + // Create one too many offers. + uint256 const beckyOfferIndex = + keylet::nftoffer(becky, env.seq(becky)).key; + env(token::createOffer(becky, nftokenID, drops(1)), + token::owner(alice)); + + // Attempt to burn the nft which should fail. + env(token::burn(alice, nftokenID), ter(tefTOO_BIG)); + + // Close enough ledgers that the burn transaction is no longer + // retried. + for (int i = 0; i < 10; ++i) + env.close(); + + // Cancel becky's offer, but alice adds a sell offer. The token + // should still not be burnable. + env(token::cancelOffer(becky, {beckyOfferIndex})); + env.close(); + + uint256 const aliceOfferIndex = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftokenID, drops(1)), + txflags(tfSellNFToken)); + env.close(); + + env(token::burn(alice, nftokenID), ter(tefTOO_BIG)); + env.close(); + + // Cancel alice's sell offer. Now the token should be burnable. + env(token::cancelOffer(alice, {aliceOfferIndex})); + env.close(); + + env(token::burn(alice, nftokenID)); + env.close(); + + // Burning the token should remove all the offers from the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex))); + } + + // Both alice and becky should have ownerCounts of zero. + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, becky) == 0); + } + + // Test that up to 499 buy/sell offers will be removed when NFT is + // burned after fixUnburnableNFToken is enabled. This is to test that we + // can successfully remove all offers if the number of offers is less + // than 500. + if (features[fixUnburnableNFToken]) { - Account const acct(std::string("acct") + std::to_string(i)); - env.fund(XRP(1000), acct); + Env env{*this, features}; + + Account const alice("alice"); + Account const becky("becky"); + env.fund(XRP(100000), alice, becky); env.close(); - offerIndexes.push_back(keylet::nftoffer(acct, env.seq(acct)).key); - env(token::createOffer(acct, nftokenID, drops(1)), + // alice creates 498 sell offers and becky creates 1 buy offers. + // When the token is burned, 498 sell offers and 1 buy offer are + // removed. In total, 499 offers are removed + std::vector offerIndexes; + auto const nftokenID = createNftAndOffers( + env, alice, offerIndexes, maxDeletableTokenOfferEntries - 2); + + // Verify all sell offers are present in the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); + } + + // Becky creates a buy offer + uint256 const beckyOfferIndex = + keylet::nftoffer(becky, env.seq(becky)).key; + env(token::createOffer(becky, nftokenID, drops(1)), token::owner(alice)); env.close(); + + // Burn the token + env(token::burn(alice, nftokenID)); + env.close(); + + // Burning the token should remove all 498 sell offers + // that alice created + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex))); + } + + // Burning the token should also remove the one buy offer + // that becky created + BEAST_EXPECT(!env.le(keylet::nftoffer(beckyOfferIndex))); + + // alice and becky should have ownerCounts of zero + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, becky) == 0); } - // Verify all offers are present in the ledger. - for (uint256 const& offerIndex : offerIndexes) + // Test that up to 500 buy offers are removed when NFT is burned + // after fixUnburnableNFToken is enabled + if (features[fixUnburnableNFToken]) { - BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); - } + Env env{*this, features}; - // Create one too many offers. - uint256 const beckyOfferIndex = - keylet::nftoffer(becky, env.seq(becky)).key; - env(token::createOffer(becky, nftokenID, drops(1)), - token::owner(alice)); + Account const alice("alice"); + Account const becky("becky"); + env.fund(XRP(100000), alice, becky); + env.close(); - // Attempt to burn the nft which should fail. - env(token::burn(alice, nftokenID), ter(tefTOO_BIG)); + // alice creates 501 sell offers for the token + // After we burn the token, 500 of the sell offers should be + // removed, and one is left over + std::vector offerIndexes; + auto const nftokenID = createNftAndOffers( + env, alice, offerIndexes, maxDeletableTokenOfferEntries + 1); - // Close enough ledgers that the burn transaction is no longer retried. - for (int i = 0; i < 10; ++i) - env.close(); + // Verify all sell offers are present in the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); + } - // Cancel becky's offer, but alice adds a sell offer. The token - // should still not be burnable. - env(token::cancelOffer(becky, {beckyOfferIndex})); - env.close(); + // Burn the token + env(token::burn(alice, nftokenID)); + env.close(); - uint256 const aliceOfferIndex = - keylet::nftoffer(alice, env.seq(alice)).key; - env(token::createOffer(alice, nftokenID, drops(1)), - txflags(tfSellNFToken)); - env.close(); + uint32_t offerDeletedCount = 0; + // Count the number of sell offers that have been deleted + for (uint256 const& offerIndex : offerIndexes) + { + if (!env.le(keylet::nftoffer(offerIndex))) + offerDeletedCount++; + } - env(token::burn(alice, nftokenID), ter(tefTOO_BIG)); - env.close(); + BEAST_EXPECT(offerIndexes.size() == maxTokenOfferCancelCount + 1); - // Cancel alice's sell offer. Now the token should be burnable. - env(token::cancelOffer(alice, {aliceOfferIndex})); - env.close(); + // 500 sell offers should be removed + BEAST_EXPECT(offerDeletedCount == maxTokenOfferCancelCount); - env(token::burn(alice, nftokenID)); - env.close(); + // alice should have ownerCounts of one for the orphaned sell offer + BEAST_EXPECT(ownerCount(env, alice) == 1); + } - // Burning the token should remove all the offers from the ledger. - for (uint256 const& offerIndex : offerIndexes) + // Test that up to 500 buy/sell offers are removed when NFT is burned + // after fixUnburnableNFToken is enabled + if (features[fixUnburnableNFToken]) { - BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex))); - } + Env env{*this, features}; - // Both alice and becky should have ownerCounts of zero. - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(ownerCount(env, becky) == 0); + Account const alice("alice"); + Account const becky("becky"); + env.fund(XRP(100000), alice, becky); + env.close(); + + // alice creates 499 sell offers and becky creates 2 buy offers. + // When the token is burned, 499 sell offers and 1 buy offer + // are removed. + // In total, 500 offers are removed + std::vector offerIndexes; + auto const nftokenID = createNftAndOffers( + env, alice, offerIndexes, maxDeletableTokenOfferEntries - 1); + + // Verify all sell offers are present in the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); + } + + // becky creates 2 buy offers + env(token::createOffer(becky, nftokenID, drops(1)), + token::owner(alice)); + env.close(); + env(token::createOffer(becky, nftokenID, drops(1)), + token::owner(alice)); + env.close(); + + // Burn the token + env(token::burn(alice, nftokenID)); + env.close(); + + // Burning the token should remove all 499 sell offers from the + // ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex))); + } + + // alice should have ownerCount of zero because all her + // sell offers have been deleted + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // becky has ownerCount of one due to an orphaned buy offer + BEAST_EXPECT(ownerCount(env, becky) == 1); + } } void @@ -598,7 +786,8 @@ class NFTokenBurn_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixNFTDir); + testWithFeats(all - fixUnburnableNFToken - fixNFTDir); + testWithFeats(all - fixUnburnableNFToken); testWithFeats(all); } }; From 5425a32d5f10ab0c8122bbe81b3e1591b70f51de Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Fri, 3 Feb 2023 09:38:18 -0500 Subject: [PATCH 08/38] [FOLD]minor comment --- src/test/app/NFTokenDir_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index 6c17869b779..d2b2a840116 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -194,7 +194,7 @@ class NFTokenDir_test : public beast::unit_test::suite // Should not advance ledger if fixUnburnableNFToken is // enabled. Because otherwise, accounts are initialized at // different ledgers, and will have different account - // sequences, which then cause the sequence number of + // sequences, which then causes the sequence number of // NFTokenIDs to be different if (!features[fixUnburnableNFToken]) env.close(); @@ -420,7 +420,7 @@ class NFTokenDir_test : public beast::unit_test::suite // Should not advance ledger if fixUnburnableNFToken is // enabled. Because otherwise, accounts are initialized at // different ledgers, and will have different account - // sequences, which then cause the sequence number of + // sequences, which then causes the sequence number of // NFTokenIDs to be different if (!features[fixUnburnableNFToken]) env.close(); From a48a4376cd1fbadf3a4478c8b5e9afe4c5829333 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Thu, 9 Feb 2023 16:13:49 -0500 Subject: [PATCH 09/38] add const --- src/ripple/app/tx/impl/NFTokenMint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index f7bab0e963a..0c6ff74897f 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -163,7 +163,7 @@ NFTokenMint::doApply() std::uint32_t tokenSeq; if (ctx_.view().rules().enabled(fixUnburnableNFToken)) { - auto accSeq = (*root)[sfSequence]; + auto const accSeq = (*root)[sfSequence]; // If the issuer hasn't minted a NFT before, we must // initialize sfFirstNFTokenSequence to equal to the current account From ce42b741d2c7acf5fd9a7e9c892aaff24107be0f Mon Sep 17 00:00:00 2001 From: ledhed2222 Date: Thu, 9 Feb 2023 16:57:51 -0500 Subject: [PATCH 10/38] [Amendment] Fix 3 issues around NFToken offer acceptance (#4380) Fixes 3 issues: ## Issue 1 In the following scenario, an account cannot perform NFTokenAcceptOffer even though it should be allowed to: - BROKER has < S - ALICE offers to sell token for S - BOB offers to buy token for > S - BROKER tries to bridge the two offers This currently results in `tecINSUFFICIENT_FUNDS`, but should not because BROKER is not spending any funds in this transaction, beyond the transaction fee. ## Issue 2 When trading an NFT using IOUs, and when the issuer of the IOU has any non-zero value set for TransferFee on their account via AccountSet (not a TransferFee on the NFT), and when the sale amount is equal to the total balance of that IOU that the buyer has, the resulting balance for the issuer of the IOU will become positive. This means that the buyer of the NFT was supposed to have caused a certain amount of IOU to be burned. That amount was unable to be burned because the buyer couldn't cover it. This results in the buyer owing this amount back to the issuer. In a real world scenario, this is appropriate and can be settled off-chain. ## Issue 3 Currency issuers could not make offers for NFTs using their own currency, receiving `tecINSUFFICIENT_FUNDS` if they tried to do so. With this fix, they are now able to buy/sell NFTs using their own currency. --- src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp | 74 +- src/ripple/app/tx/impl/NFTokenCreateOffer.cpp | 31 +- src/ripple/ledger/View.h | 5 + src/test/app/NFToken_test.cpp | 1650 +++++++++++++---- 4 files changed, 1339 insertions(+), 421 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp index 07fe9957a76..c335f8d28fd 100644 --- a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp @@ -168,10 +168,21 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) dest.has_value() && *dest != ctx.tx[sfAccount]) return tecNO_PERMISSION; } + // The account offering to buy must have funds: + // + // After this amendment, we allow an IOU issuer to buy an NFT with their + // own currency auto const needed = bo->at(sfAmount); - - if (accountHolds( + if (ctx.view.rules().enabled(fixUnburnableNFToken)) + { + if (accountFunds( + ctx.view, (*bo)[sfOwner], needed, fhZERO_IF_FROZEN, ctx.j) < + needed) + return tecINSUFFICIENT_FUNDS; + } + else if ( + accountHolds( ctx.view, (*bo)[sfOwner], needed.getCurrency(), @@ -206,15 +217,39 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // The account offering to buy must have funds: auto const needed = so->at(sfAmount); - - if (accountHolds( - ctx.view, - ctx.tx[sfAccount], - needed.getCurrency(), - needed.getIssuer(), - fhZERO_IF_FROZEN, - ctx.j) < needed) - return tecINSUFFICIENT_FUNDS; + if (!ctx.view.rules().enabled(fixUnburnableNFToken)) + { + if (accountHolds( + ctx.view, + ctx.tx[sfAccount], + needed.getCurrency(), + needed.getIssuer(), + fhZERO_IF_FROZEN, + ctx.j) < needed) + return tecINSUFFICIENT_FUNDS; + } + else if (!bo) + { + // After this amendment, we allow buyers to buy with their own + // issued currency. + // + // In the case of brokered mode, this check is essentially + // redundant, since we have already confirmed that buy offer is > + // than the sell offer, and that the buyer can cover the buy + // offer. + // + // We also _must not_ check the tx submitter in brokered + // mode, because then we are confirming that the broker can + // cover what the buyer will pay, which doesn't make sense, causes + // an unncessary tec, and is also resolved with this amendment. + if (accountFunds( + ctx.view, + ctx.tx[sfAccount], + needed, + fhZERO_IF_FROZEN, + ctx.j) < needed) + return tecINSUFFICIENT_FUNDS; + } } return tesSUCCESS; @@ -230,7 +265,22 @@ NFTokenAcceptOffer::pay( if (amount < beast::zero) return tecINTERNAL; - return accountSend(view(), from, to, amount, j_); + auto const result = accountSend(view(), from, to, amount, j_); + + // After this amendment, if any payment would cause a non-IOU-issuer to + // have a negative balance, or an IOU-issuer to have a positive balance in + // their own currency, we know that something went wrong. This was + // originally found in the context of IOU transfer fees. Since there are + // several payouts in this tx, just confirm that the end state is OK. + if (!view().rules().enabled(fixUnburnableNFToken)) + return result; + if (result != tesSUCCESS) + return result; + if (accountFunds(view(), from, amount, fhZERO_IF_FROZEN, j_).signum() < 0) + return tecINSUFFICIENT_FUNDS; + if (accountFunds(view(), to, amount, fhZERO_IF_FROZEN, j_).signum() < 0) + return tecINSUFFICIENT_FUNDS; + return tesSUCCESS; } TER diff --git a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp index 695efdd0aa4..ff8668e4488 100644 --- a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp @@ -153,15 +153,28 @@ NFTokenCreateOffer::preclaim(PreclaimContext const& ctx) // offer may later become unfunded. if (!isSellOffer) { - auto const funds = accountHolds( - ctx.view, - ctx.tx[sfAccount], - amount.getCurrency(), - amount.getIssuer(), - FreezeHandling::fhZERO_IF_FROZEN, - ctx.j); - - if (funds.signum() <= 0) + // After this amendment, we allow an IOU issuer to make a buy offer + // using their own currency. + if (ctx.view.rules().enabled(fixUnburnableNFToken)) + { + if (accountFunds( + ctx.view, + ctx.tx[sfAccount], + amount, + FreezeHandling::fhZERO_IF_FROZEN, + ctx.j) + .signum() <= 0) + return tecUNFUNDED_OFFER; + } + else if ( + accountHolds( + ctx.view, + ctx.tx[sfAccount], + amount.getCurrency(), + amount.getIssuer(), + FreezeHandling::fhZERO_IF_FROZEN, + ctx.j) + .signum() <= 0) return tecUNFUNDED_OFFER; } diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index ee917115515..24a647c768d 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -97,6 +97,11 @@ accountHolds( FreezeHandling zeroIfFrozen, beast::Journal j); +// Returns the amount an account can spend of the currency type saDefault, or +// returns saDefault if this account is the issuer of the the currency in +// question. Should be used in favor of accountHolds when questioning how much +// an account can spend while also allowing currency issuers to spend +// unlimited amounts of their own currency (since they can always issue more). [[nodiscard]] STAmount accountFunds( ReadView const& view, diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 42a6eb4d3ce..e44ce136f72 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -3821,497 +3821,586 @@ class NFToken_test : public beast::unit_test::suite using namespace test::jtx; - Env env{*this, features}; - - // The most important thing to explore here is the way funds are - // assigned from the buyer to... - // o the Seller, - // o the Broker, and - // o the Issuer (in the case of a transfer fee). - - Account const issuer{"issuer"}; - Account const minter{"minter"}; - Account const buyer{"buyer"}; - Account const broker{"broker"}; - Account const gw{"gw"}; - IOU const gwXAU(gw["XAU"]); - - env.fund(XRP(1000), issuer, minter, buyer, broker, gw); - env.close(); - - env(trust(issuer, gwXAU(2000))); - env(trust(minter, gwXAU(2000))); - env(trust(buyer, gwXAU(2000))); - env(trust(broker, gwXAU(2000))); - env.close(); - - env(token::setMinter(issuer, minter)); - env.close(); - - // Lambda to check owner count of all accounts is one. - auto checkOwnerCountIsOne = - [this, &env]( - std::initializer_list> - accounts, - int line) { - for (Account const& acct : accounts) - { - if (std::uint32_t ownerCount = this->ownerCount(env, acct); - ownerCount != 1) - { - std::stringstream ss; - ss << "Account " << acct.human() - << " expected ownerCount == 1. Got " << ownerCount; - fail(ss.str(), __FILE__, line); - } - } - }; - - // Lambda that mints an NFT and returns the nftID. - auto mintNFT = [&env, &issuer, &minter](std::uint16_t xferFee = 0) { - uint256 const nftID = - token::getNextID(env, issuer, 0, tfTransferable, xferFee); - env(token::mint(minter, 0), - token::issuer(issuer), - token::xferFee(xferFee), - txflags(tfTransferable)); - env.close(); - return nftID; - }; - - // o Seller is selling for zero XRP. - // o Broker charges no fee. - // o No transfer fee. - // - // Since minter is selling for zero the currency must be XRP. + for (auto const& tweakedFeatures : + {features - fixUnburnableNFToken, features | fixUnburnableNFToken}) { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + Env env{*this, tweakedFeatures}; - uint256 const nftID = mintNFT(); + // The most important thing to explore here is the way funds are + // assigned from the buyer to... + // o the Seller, + // o the Broker, and + // o the Issuer (in the case of a transfer fee). - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); + Account const issuer{"issuer"}; + Account const minter{"minter"}; + Account const buyer{"buyer"}; + Account const broker{"broker"}; + Account const gw{"gw"}; + IOU const gwXAU(gw["XAU"]); - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); + env.fund(XRP(1000), issuer, minter, buyer, broker, gw); env.close(); - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); - - // Broker charges no brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex)); + env(trust(issuer, gwXAU(2000))); + env(trust(minter, gwXAU(2000))); + env(trust(buyer, gwXAU(2000))); + env(trust(broker, gwXAU(2000))); env.close(); - // Note that minter's XRP balance goes up even though they - // requested XRP(0). - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(1)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance); - - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); + env(token::setMinter(issuer, minter)); env.close(); - } - // o Seller is selling for zero XRP. - // o Broker charges a fee. - // o No transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + // Lambda to check owner count of all accounts is one. + auto checkOwnerCountIsOne = + [this, &env]( + std::initializer_list> + accounts, + int line) { + for (Account const& acct : accounts) + { + if (std::uint32_t ownerCount = + this->ownerCount(env, acct); + ownerCount != 1) + { + std::stringstream ss; + ss << "Account " << acct.human() + << " expected ownerCount == 1. Got " + << ownerCount; + fail(ss.str(), __FILE__, line); + } + } + }; - uint256 const nftID = mintNFT(); + // Lambda that mints an NFT and returns the nftID. + auto mintNFT = [&env, &issuer, &minter](std::uint16_t xferFee = 0) { + uint256 const nftID = + token::getNextID(env, issuer, 0, tfTransferable, xferFee); + env(token::mint(minter, 0), + token::issuer(issuer), + token::xferFee(xferFee), + txflags(tfTransferable)); + env.close(); + return nftID; + }; - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); + // o Seller is selling for zero XRP. + // o Broker charges no fee. + // o No transfer fee. + // + // Since minter is selling for zero the currency must be XRP. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); + uint256 const nftID = mintNFT(); - // Broker attempts to charge a 1.1 XRP brokerFee and fails. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(XRP(1.1)), - ter(tecINSUFFICIENT_PAYMENT)); - env.close(); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); + // buyer creates their offer. Note: a buy offer can never + // offer zero. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, XRP(1)), + token::owner(minter)); + env.close(); - // Broker charges a 0.5 XRP brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(XRP(0.5))); - env.close(); + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); - // Note that minter's XRP balance goes up even though they - // requested XRP(0). - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT( - env.balance(broker) == brokerBalance + XRP(0.5) - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance); + // Broker charges no brokerFee. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex)); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } + // Note that minter's XRP balance goes up even though they + // requested XRP(0). + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(1)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance); - // o Seller is selling for zero XRP. - // o Broker charges no fee. - // o 50% transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } - uint256 const nftID = mintNFT(maxTransferFee); + // o Seller is selling for zero XRP. + // o Broker charges a fee. + // o No transfer fee. + // + // Since minter is selling for zero the currency must be XRP. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); + uint256 const nftID = mintNFT(); - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); + // buyer creates their offer. Note: a buy offer can never + // offer zero. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, XRP(1)), + token::owner(minter)); + env.close(); - // Broker charges no brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex)); - env.close(); + // Broker attempts to charge a 1.1 XRP brokerFee and fails. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(XRP(1.1)), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); - // Note that minter's XRP balance goes up even though they - // requested XRP(0). - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.5)); + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } + // Broker charges a 0.5 XRP brokerFee. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(XRP(0.5))); + env.close(); - // o Seller is selling for zero XRP. - // o Broker charges 0.5 XRP. - // o 50% transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + // Note that minter's XRP balance goes up even though they + // requested XRP(0). + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT( + env.balance(broker) == + brokerBalance + XRP(0.5) - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } - uint256 const nftID = mintNFT(maxTransferFee); + // o Seller is selling for zero XRP. + // o Broker charges no fee. + // o 50% transfer fee. + // + // Since minter is selling for zero the currency must be XRP. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); + uint256 const nftID = mintNFT(maxTransferFee); - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); + // buyer creates their offer. Note: a buy offer can never + // offer zero. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, XRP(1)), + token::owner(minter)); + env.close(); - // Broker charges a 0.75 XRP brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(XRP(0.75))); - env.close(); + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); - // Note that, with a 50% transfer fee, issuer gets 1/2 of what's - // left _after_ broker takes their fee. minter gets the remainder - // after both broker and minter take their cuts - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.125)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT( - env.balance(broker) == brokerBalance + XRP(0.75) - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.125)); + // Broker charges no brokerFee. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex)); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } + // Note that minter's XRP balance goes up even though they + // requested XRP(0). + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.5)); - // Lambda to set the balance of all passed in accounts to gwXAU(1000). - auto setXAUBalance_1000 = - [this, &gw, &gwXAU, &env]( - std::initializer_list> - accounts, - int line) { - for (Account const& acct : accounts) - { - static const auto xau1000 = gwXAU(1000); - auto const balance = env.balance(acct, gwXAU); - if (balance < xau1000) - { - env(pay(gw, acct, xau1000 - balance)); - env.close(); - } - else if (balance > xau1000) - { - env(pay(acct, gw, balance - xau1000)); - env.close(); - } - if (env.balance(acct, gwXAU) != xau1000) - { - std::stringstream ss; - ss << "Unable to set " << acct.human() - << " account balance to gwXAU(1000)"; - this->fail(ss.str(), __FILE__, line); - } - } - }; + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } - // The buyer and seller have identical amounts and there is no - // transfer fee. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__); + // o Seller is selling for zero XRP. + // o Broker charges 0.5 XRP. + // o 50% transfer fee. + // + // Since minter is selling for zero the currency must be XRP. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - uint256 const nftID = mintNFT(); + uint256 const nftID = mintNFT(maxTransferFee); - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, gwXAU(1000)), - txflags(tfSellNFToken)); - env.close(); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); - { - // buyer creates an offer for more XAU than they currently own. + // buyer creates their offer. Note: a buy offer can never + // offer zero. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1001)), + env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); + + // Broker charges a 0.75 XRP brokerFee. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), - ter(tecINSUFFICIENT_FUNDS)); + token::brokerFee(XRP(0.75))); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + // Note that, with a 50% transfer fee, issuer gets 1/2 of what's + // left _after_ broker takes their fee. minter gets the + // remainder after both broker and minter take their cuts + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.125)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT( + env.balance(broker) == + brokerBalance + XRP(0.75) - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.125)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } + + // Lambda to set the balance of all passed in accounts to + // gwXAU(amount). + auto setXAUBalance = + [this, &gw, &gwXAU, &env]( + std::initializer_list> + accounts, + int amount, + int line) { + for (Account const& acct : accounts) + { + auto const xauAmt = gwXAU(amount); + auto const balance = env.balance(acct, gwXAU); + if (balance < xauAmt) + { + env(pay(gw, acct, xauAmt - balance)); + env.close(); + } + else if (balance > xauAmt) + { + env(pay(acct, gw, balance - xauAmt)); + env.close(); + } + if (env.balance(acct, gwXAU) != xauAmt) + { + std::stringstream ss; + ss << "Unable to set " << acct.human() + << " account balance to gwXAU(" << amount << ")"; + this->fail(ss.str(), __FILE__, line); + } + } + }; + + // The buyer and seller have identical amounts and there is no + // transfer fee. { - // buyer creates an offer for less that what minter is asking. + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer, broker}, 1000, __LINE__); + + uint256 const nftID = mintNFT(); + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(1000)), + txflags(tfSellNFToken)); + env.close(); + + { + // buyer creates an offer for more XAU than they currently + // own. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1001)), + token::owner(minter)); + env.close(); + + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + { + // buyer creates an offer for less that what minter is + // asking. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(999)), + token::owner(minter)); + env.close(); + + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); + + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + + // buyer creates a large enough offer. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(999)), + env(token::createOffer(buyer, nftID, gwXAU(1000)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + // Broker attempts to charge a brokerFee but cannot. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(0.1)), ter(tecINSUFFICIENT_PAYMENT)); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + // broker charges no brokerFee and succeeds. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex)); + env.close(); + + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(2000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1000)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } - // buyer creates a large enough offer. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1000)), - token::owner(minter)); - env.close(); + // seller offers more than buyer is asking. + // There are both transfer and broker fees. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer, broker}, 1000, __LINE__); - // Broker attempts to charge a brokerFee but cannot. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(0.1)), - ter(tecINSUFFICIENT_PAYMENT)); - env.close(); + uint256 const nftID = mintNFT(maxTransferFee); - // broker charges no brokerFee and succeeds. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex)); - env.close(); - - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); - BEAST_EXPECT(ownerCount(env, broker) == 1); - BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000)); - BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(2000)); - BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); - BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1000)); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(900)), + txflags(tfSellNFToken)); + env.close(); + { + // buyer creates an offer for more XAU than they currently + // own. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1001)), + token::owner(minter)); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); - // seller offers more than buyer is asking. - // There are both transfer and broker fees. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__); + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + { + // buyer creates an offer for less that what minter is + // asking. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(899)), + token::owner(minter)); + env.close(); - uint256 const nftID = mintNFT(maxTransferFee); + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, gwXAU(900)), - txflags(tfSellNFToken)); - env.close(); - { - // buyer creates an offer for more XAU than they currently own. + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + // buyer creates a large enough offer. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1001)), + env(token::createOffer(buyer, nftID, gwXAU(1000)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + // Broker attempts to charge a brokerFee larger than the + // difference between the two offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(101)), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); + + // broker charges the full difference between the two offers and + // succeeds. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), - ter(tecINSUFFICIENT_FUNDS)); + token::brokerFee(gwXAU(100))); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1450)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1450)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1100)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } + // seller offers more than buyer is asking. + // There are both transfer and broker fees, but broker takes less + // than the maximum. { - // buyer creates an offer for less that what minter is asking. + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer, broker}, 1000, __LINE__); + + uint256 const nftID = mintNFT(maxTransferFee / 2); // 25% + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(900)), + txflags(tfSellNFToken)); + env.close(); + + // buyer creates a large enough offer. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(899)), + env(token::createOffer(buyer, nftID, gwXAU(1000)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + // broker charges half difference between the two offers and + // succeeds. 25% of the remaining difference goes to issuer. + // The rest goes to minter. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), - ter(tecINSUFFICIENT_PAYMENT)); + token::brokerFee(gwXAU(50))); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1050)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } - // buyer creates a large enough offer. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1000)), - token::owner(minter)); - env.close(); - - // Broker attempts to charge a brokerFee larger than the - // difference between the two offers but cannot. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(101)), - ter(tecINSUFFICIENT_PAYMENT)); - env.close(); - - // broker charges the full difference between the two offers and - // succeeds. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(100))); - env.close(); - - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); - BEAST_EXPECT(ownerCount(env, broker) == 1); - BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1450)); - BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1450)); - BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); - BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1100)); - - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } - // seller offers more than buyer is asking. - // There are both transfer and broker fees, but broker takes less than - // the maximum. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__); - - uint256 const nftID = mintNFT(maxTransferFee / 2); // 25% - - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, gwXAU(900)), - txflags(tfSellNFToken)); - env.close(); - - // buyer creates a large enough offer. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1000)), - token::owner(minter)); - env.close(); - - // broker charges half difference between the two offers and - // succeeds. 25% of the remaining difference goes to issuer. - // The rest goes to minter. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(50))); - env.close(); + // Broker has a balance less than the seller offer + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer}, 1000, __LINE__); + setXAUBalance({broker}, 500, __LINE__); + uint256 const nftID = mintNFT(maxTransferFee / 2); // 25% + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(900)), + txflags(tfSellNFToken)); + env.close(); - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); - BEAST_EXPECT(ownerCount(env, broker) == 1); - BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5)); - BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5)); - BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); - BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1050)); + // buyer creates a large enough offer. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1000)), + token::owner(minter)); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); + if (tweakedFeatures[fixUnburnableNFToken]) + { + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(50))); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(550)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } + else + { + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(50)), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 3); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(500)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(minter, nftID)); + env.close(); + } + } } } @@ -4817,9 +4906,14 @@ class NFToken_test : public beast::unit_test::suite Account const gw{"gw"}; IOU const gwXAU(gw["XAU"]); - // Test both with and without fixNFTokenNegOffer + // Test both with and without fixNFTokenNegOffer and + // fixUnburnableNFToken. Need to turn off fixUnburnableNFToken as well + // because that amendment came later and addressed the acceptance + // side of this issue. for (auto const& tweakedFeatures : - {features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1, + {features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1 - + fixUnburnableNFToken, + features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1, features | fixNFTokenNegOffer}) { // There was a bug in the initial NFT implementation that @@ -4908,8 +5002,10 @@ class NFToken_test : public beast::unit_test::suite env.close(); } { - // 1. If fixNFTokenNegOffer is NOT enabled get tecSUCCESS. - // 2. If fixNFTokenNegOffer IS enabled get tecOBJECT_NOT_FOUND. + // 1. If fixNFTokenNegOffer is enabled get tecOBJECT_NOT_FOUND + // 2. If it is not enabled, but fixUnburnableNFToken is + // enabled, get tecOBJECT_NOT_FOUND. + // 3. If neither are enabled, get tesSUCCESS. TER const offerAcceptTER = tweakedFeatures[fixNFTokenNegOffer] ? static_cast(tecOBJECT_NOT_FOUND) : static_cast(tesSUCCESS); @@ -5041,6 +5137,757 @@ class NFToken_test : public beast::unit_test::suite } } + void + testIOUWithTransferFee(FeatureBitset features) + { + using namespace test::jtx; + + testcase("Payments with IOU transfer fees"); + + for (auto const& tweakedFeatures : + {features - fixUnburnableNFToken, features | fixUnburnableNFToken}) + { + Env env{*this, tweakedFeatures}; + + Account const minter{"minter"}; + Account const secondarySeller{"seller"}; + Account const buyer{"buyer"}; + Account const gw{"gateway"}; + Account const broker{"broker"}; + IOU const gwXAU(gw["XAU"]); + IOU const gwXPB(gw["XPB"]); + + env.fund(XRP(1000), gw, minter, secondarySeller, buyer, broker); + env.close(); + + env(trust(minter, gwXAU(2000))); + env(trust(secondarySeller, gwXAU(2000))); + env(trust(broker, gwXAU(10000))); + env(trust(buyer, gwXAU(2000))); + env(trust(buyer, gwXPB(2000))); + env.close(); + + // The IOU issuer has a 2% transfer rate + env(rate(gw, 1.02)); + env.close(); + + auto expectInitialState = [this, + &env, + &buyer, + &minter, + &secondarySeller, + &broker, + &gw, + &gwXAU, + &gwXPB]() { + // Buyer should have XAU 1000, XPB 0 + // Minter should have XAU 0, XPB 0 + // Secondary seller should have XAU 0, XPB 0 + // Broker should have XAU 5000, XPB 0 + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(minter, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(secondarySeller, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(5000)); + BEAST_EXPECT(env.balance(broker, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XPB"]) == gwXPB(0)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, minter["XPB"]) == gwXPB(0)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(0)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XPB"]) == gwXPB(0)); + BEAST_EXPECT(env.balance(gw, broker["XAU"]) == gwXAU(-5000)); + BEAST_EXPECT(env.balance(gw, broker["XPB"]) == gwXPB(0)); + }; + + auto reinitializeTrustLineBalances = [&expectInitialState, + &env, + &buyer, + &minter, + &secondarySeller, + &broker, + &gw, + &gwXAU, + &gwXPB]() { + if (auto const difference = + gwXAU(1000) - env.balance(buyer, gwXAU); + difference > gwXAU(0)) + env(pay(gw, buyer, difference)); + if (env.balance(buyer, gwXPB) > gwXPB(0)) + env(pay(buyer, gw, env.balance(buyer, gwXPB))); + if (env.balance(minter, gwXAU) > gwXAU(0)) + env(pay(minter, gw, env.balance(minter, gwXAU))); + if (env.balance(minter, gwXPB) > gwXPB(0)) + env(pay(minter, gw, env.balance(minter, gwXPB))); + if (env.balance(secondarySeller, gwXAU) > gwXAU(0)) + env( + pay(secondarySeller, + gw, + env.balance(secondarySeller, gwXAU))); + if (env.balance(secondarySeller, gwXPB) > gwXPB(0)) + env( + pay(secondarySeller, + gw, + env.balance(secondarySeller, gwXPB))); + auto brokerDiff = gwXAU(5000) - env.balance(broker, gwXAU); + if (brokerDiff > gwXAU(0)) + env(pay(gw, broker, brokerDiff)); + else if (brokerDiff < gwXAU(0)) + { + brokerDiff.negate(); + env(pay(broker, gw, brokerDiff)); + } + if (env.balance(broker, gwXPB) > gwXPB(0)) + env(pay(broker, gw, env.balance(broker, gwXPB))); + env.close(); + expectInitialState(); + }; + + auto mintNFT = [&env](Account const& minter, int transferFee = 0) { + uint256 const nftID = token::getNextID( + env, minter, 0, tfTransferable, transferFee); + env(token::mint(minter), + token::xferFee(transferFee), + txflags(tfTransferable)); + env.close(); + return nftID; + }; + + auto createBuyOffer = + [&env]( + Account const& offerer, + Account const& owner, + uint256 const& nftID, + STAmount const& amount, + std::optional const terCode = {}) { + uint256 const offerID = + keylet::nftoffer(offerer, env.seq(offerer)).key; + env(token::createOffer(offerer, nftID, amount), + token::owner(owner), + terCode ? ter(*terCode) + : ter(static_cast(tesSUCCESS))); + env.close(); + return offerID; + }; + + auto createSellOffer = + [&env]( + Account const& offerer, + uint256 const& nftID, + STAmount const& amount, + std::optional const terCode = {}) { + uint256 const offerID = + keylet::nftoffer(offerer, env.seq(offerer)).key; + env(token::createOffer(offerer, nftID, amount), + txflags(tfSellNFToken), + terCode ? ter(*terCode) + : ter(static_cast(tesSUCCESS))); + env.close(); + return offerID; + }; + + { + // Buyer attempts to send 100% of their balance of an IOU + // (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // Buyer attempts to send 100% of their balance of an IOU + // (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU, but such that the addition of the transfer + // fee would be greater than the buyer's balance (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXAU(995)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(995)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-14.9)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-995)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(14.9)); + } + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU, but such that the addition of the transfer + // fee would be greater than the buyer's balance (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(995)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(995)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-14.9)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-995)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(14.9)); + } + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is still less than their balance + // (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXAU(900)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(900)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-900)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is still less than their balance + // (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(900)); + env(token::acceptBuyOffer(minter, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(900)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-900)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is equal than their balance + // (sellside) + reinitializeTrustLineBalances(); + + // pay them an additional XAU 20 to cover transfer rate + env(pay(gw, buyer, gwXAU(20))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(1000)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is equal than their balance + // (buyside) + reinitializeTrustLineBalances(); + + // pay them an additional XAU 20 to cover transfer rate + env(pay(gw, buyer, gwXAU(20))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(1000)); + env(token::acceptBuyOffer(minter, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Gateway attempts to buy NFT with their own IOU - no + // transfer fee is calculated here (sellside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecINSUFFICIENT_FUNDS); + env(token::acceptSellOffer(gw, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + } + else + expectInitialState(); + } + { + // Gateway attempts to buy NFT with their own IOU - no + // transfer fee is calculated here (buyside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const offerTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecUNFUNDED_OFFER); + auto const offerID = + createBuyOffer(gw, minter, nftID, gwXAU(1000), {offerTER}); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecOBJECT_NOT_FOUND); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + } + else + expectInitialState(); + } + { + // Gateway attempts to buy NFT with their own IOU for more + // than minter trusts (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(5000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecINSUFFICIENT_FUNDS); + env(token::acceptSellOffer(gw, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(5000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-5000)); + } + else + expectInitialState(); + } + { + // Gateway attempts to buy NFT with their own IOU for more + // than minter trusts (buyside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const offerTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecUNFUNDED_OFFER); + auto const offerID = + createBuyOffer(gw, minter, nftID, gwXAU(5000), {offerTER}); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecOBJECT_NOT_FOUND); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(5000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-5000)); + } + else + expectInitialState(); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that would be greater than a balance if there were a + // transfer fee calculated in this transaction. (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(gw); + auto const offerID = createSellOffer(gw, nftID, gwXAU(1000)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that would be greater than a balance if there were a + // transfer fee calculated in this transaction. (buyside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(gw); + auto const offerID = + createBuyOffer(buyer, gw, nftID, gwXAU(1000)); + env(token::acceptBuyOffer(gw, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that is greater than a balance before transfer fees. + // (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(gw); + auto const offerID = createSellOffer(gw, nftID, gwXAU(2000)); + env(token::acceptSellOffer(buyer, offerID), + ter(static_cast(tecINSUFFICIENT_FUNDS))); + env.close(); + expectInitialState(); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that is greater than a balance before transfer fees. + // (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(gw); + auto const offerID = + createBuyOffer(buyer, gw, nftID, gwXAU(2000)); + env(token::acceptBuyOffer(gw, offerID), + ter(static_cast(tecINSUFFICIENT_FUNDS))); + env.close(); + expectInitialState(); + } + { + // Minter attempts to sell the token for XPB 10, which they + // have no trust line for and buyer has none of (sellside). + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXPB(10)); + env(token::acceptSellOffer(buyer, offerID), + ter(static_cast(tecINSUFFICIENT_FUNDS))); + env.close(); + expectInitialState(); + } + { + // Minter attempts to sell the token for XPB 10, which they + // have no trust line for and buyer has none of (buyside). + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createBuyOffer( + buyer, + minter, + nftID, + gwXPB(10), + {static_cast(tecUNFUNDED_OFFER)}); + env(token::acceptBuyOffer(minter, offerID), + ter(static_cast(tecOBJECT_NOT_FOUND))); + env.close(); + expectInitialState(); + } + { + // Minter attempts to sell the token for XPB 10 and the buyer + // has it but the minter has no trust line. Trust line is + // created as a result of the tx (sellside). + reinitializeTrustLineBalances(); + env(pay(gw, buyer, gwXPB(100))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXPB(10)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXPB) == gwXPB(10)); + BEAST_EXPECT(env.balance(buyer, gwXPB) == gwXPB(89.8)); + BEAST_EXPECT(env.balance(gw, minter["XPB"]) == gwXPB(-10)); + BEAST_EXPECT(env.balance(gw, buyer["XPB"]) == gwXPB(-89.8)); + } + { + // Minter attempts to sell the token for XPB 10 and the buyer + // has it but the minter has no trust line. Trust line is + // created as a result of the tx (buyside). + reinitializeTrustLineBalances(); + env(pay(gw, buyer, gwXPB(100))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXPB(10)); + env(token::acceptBuyOffer(minter, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXPB) == gwXPB(10)); + BEAST_EXPECT(env.balance(buyer, gwXPB) == gwXPB(89.8)); + BEAST_EXPECT(env.balance(gw, minter["XPB"]) == gwXPB(-10)); + BEAST_EXPECT(env.balance(gw, buyer["XPB"]) == gwXPB(-89.8)); + } + { + // There is a transfer fee on the NFT and buyer has exact + // amount (sellside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createSellOffer(secondarySeller, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(30)); + BEAST_EXPECT( + env.balance(secondarySeller, gwXAU) == gwXAU(970)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-30)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-970)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // There is a transfer fee on the NFT and buyer has exact + // amount (buyside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createBuyOffer(buyer, secondarySeller, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptBuyOffer(secondarySeller, offerID), + ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(30)); + BEAST_EXPECT( + env.balance(secondarySeller, gwXAU) == gwXAU(970)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-30)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-970)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // There is a transfer fee on the NFT and buyer has enough + // (sellside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createSellOffer(secondarySeller, nftID, gwXAU(900)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(27)); + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(873)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-27)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-873)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // There is a transfer fee on the NFT and buyer has enough + // (buyside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createBuyOffer(buyer, secondarySeller, nftID, gwXAU(900)); + env(token::acceptBuyOffer(secondarySeller, offerID)); + env.close(); + + // receives 3% of 900 - 27 + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(27)); + // receives 97% of 900 - 873 + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(873)); + // pays 900 plus 2% transfer fee on XAU - 918 + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-27)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-873)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // There is a broker fee on the NFT. XAU transfer fee is only + // calculated from the buyer's output, not deducted from + // broker fee. + // + // For a payment of 500 with a 2% IOU transfee fee and 100 + // broker fee: + // + // A) Total sale amount + IOU transfer fee is paid by buyer + // (Buyer pays (1.02 * 500) = 510) + // B) GW receives the additional IOU transfer fee + // (GW receives 10 from buyer calculated above) + // C) Broker receives broker fee (no IOU transfer fee) + // (Broker receives 100 from buyer) + // D) Seller receives balance (no IOU transfer fee) + // (Seller receives (510 - 10 - 100) = 400) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const sellOffer = + createSellOffer(minter, nftID, gwXAU(300)); + auto const buyOffer = + createBuyOffer(buyer, minter, nftID, gwXAU(500)); + env(token::brokerOffers(broker, buyOffer, sellOffer), + token::brokerFee(gwXAU(100))); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(400)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(490)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(5100)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-400)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-490)); + BEAST_EXPECT(env.balance(gw, broker["XAU"]) == gwXAU(-5100)); + } + { + // There is broker and transfer fee on the NFT + // + // For a payment of 500 with a 2% IOU transfer fee, 3% NFT + // transfer fee, and 100 broker fee: + // + // A) Total sale amount + IOU transfer fee is paid by buyer + // (Buyer pays (1.02 * 500) = 510) + // B) GW receives the additional IOU transfer fee + // (GW receives 10 from buyer calculated above) + // C) Broker receives broker fee (no IOU transfer fee) + // (Broker receives 100 from buyer) + // D) Minter receives transfer fee (no IOU transfer fee) + // (Minter receives 0.03 * (510 - 10 - 100) = 12) + // E) Seller receives balance (no IOU transfer fee) + // (Seller receives (510 - 10 - 100 - 12) = 388) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const sellOffer = + createSellOffer(secondarySeller, nftID, gwXAU(300)); + auto const buyOffer = + createBuyOffer(buyer, secondarySeller, nftID, gwXAU(500)); + env(token::brokerOffers(broker, buyOffer, sellOffer), + token::brokerFee(gwXAU(100))); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(12)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(490)); + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(388)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(5100)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-12)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-490)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-388)); + BEAST_EXPECT(env.balance(gw, broker["XAU"]) == gwXAU(-5100)); + } + } + } + void testWithFeats(FeatureBitset features) { @@ -5070,6 +5917,7 @@ class NFToken_test : public beast::unit_test::suite testNFTokenDeleteAccount(features); testNftXxxOffers(features); testFixNFTokenNegOffer(features); + testIOUWithTransferFee(features); } public: @@ -5080,6 +5928,8 @@ class NFToken_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; + // TODO too many tests are being run - ths fixNFTDir check should be + // pushed into the tests that use it testWithFeats(all - fixNFTDir); testWithFeats(all - disallowIncoming); testWithFeats(all); From 40c55e90fd6872ef025f962caef2d996c66ba42e Mon Sep 17 00:00:00 2001 From: Scott Schurr Date: Thu, 9 Feb 2023 21:15:22 -0800 Subject: [PATCH 11/38] [Amendment] Prevent brokered sale of NFToken to owner: (#4403) Fixes #4374 It was possible for a broker to combine a sell and a buy offer from an account that already owns an NFT. Such brokering extracts money from the NFT owner and provides no benefit in return. With this amendment, the code detects when a broker is returning an NFToken to its initial owner and prohibits the transaction. This forbids a broker from selling an NFToken to the account that already owns the token. This fixes a bug in the original implementation of XLS-20. Thanks to @nixer89 for suggesting this fix. --- src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp | 6 + src/test/app/NFToken_test.cpp | 119 +++++++++++++++++- 2 files changed, 120 insertions(+), 5 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp index c335f8d28fd..257bda5c051 100644 --- a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp @@ -107,6 +107,12 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) if ((*bo)[sfAmount].issue() != (*so)[sfAmount].issue()) return tecNFTOKEN_BUY_SELL_MISMATCH; + // The two offers may not form a loop. A broker may not sell the + // token to the current owner of the token. + if (ctx.view.rules().enabled(fixUnburnableNFToken) && + ((*bo)[sfOwner] == (*so)[sfOwner])) + return tecCANT_ACCEPT_OWN_NFTOKEN_OFFER; + // Ensure that the buyer is willing to pay at least as much as the // seller is requesting: if ((*so)[sfAmount] > (*bo)[sfAmount]) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index e44ce136f72..a82c50e842a 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -4607,7 +4607,7 @@ class NFToken_test : public beast::unit_test::suite txflags(tfTransferable)); env.close(); - // At the momement issuer and minter cannot delete themselves. + // At the moment issuer and minter cannot delete themselves. // o issuer has an issued NFT in the ledger. // o minter owns an NFT. env(acctdelete(issuer, daria), fee(XRP(50)), ter(tecHAS_OBLIGATIONS)); @@ -5888,6 +5888,115 @@ class NFToken_test : public beast::unit_test::suite } } + void + testBrokeredSaleToSelf(FeatureBitset features) + { + // There was a bug that if an account had... + // + // 1. An NFToken, and + // 2. An offer on the ledger to buy that same token, and + // 3. Also an offer of the ledger to sell that same token, + // + // Then someone could broker the two offers. This would result in + // the NFToken being bought and returned to the original owner and + // the broker pocketing the profit. + // + // This unit test verifies that the fixUnburnableNFToken amendment + // fixes that bug. + testcase("Brokered sale to self"); + + using namespace test::jtx; + + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const broker{"broker"}; + + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, broker); + env.close(); + + // For this scenario to occur we need the following steps: + // + // 1. alice mints NFT. + // 2. bob creates a buy offer for it for 5 XRP. + // 3. alice decides to gift the NFT to bob for 0. + // creating a sell offer (hopefully using a destination too) + // 4. Bob accepts the sell offer, because it is better than + // paying 5 XRP. + // 5. At this point, bob has the NFT and still has their buy + // offer from when they did not have the NFT! This is because + // the order book is not cleared when an NFT changes hands. + // 6. Now that Bob owns the NFT, he cannot create new buy offers. + // However he still has one left over from when he did not own + // it. He can create new sell offers and does. + // 7. Now that bob has both a buy and a sell offer for the same NFT, + // a broker can sell the NFT that bob owns to bob and pocket the + // difference. + uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + + // Bob creates a buy offer for 5 XRP. Alice creates a sell offer + // for 0 XRP. + uint256 const bobBuyOfferIndex = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(bob, nftId, XRP(5)), token::owner(alice)); + + uint256 const aliceSellOfferIndex = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId, XRP(0)), + token::destination(bob), + txflags(tfSellNFToken)); + env.close(); + + // bob accepts alice's offer but forgets to remove the old buy offer. + env(token::acceptSellOffer(bob, aliceSellOfferIndex)); + env.close(); + + // Note that bob still has a buy offer on the books. + BEAST_EXPECT(env.le(keylet::nftoffer(bobBuyOfferIndex))); + + // Bob creates a sell offer for the gift NFT from alice. + uint256 const bobSellOfferIndex = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(bob, nftId, XRP(4)), txflags(tfSellNFToken)); + env.close(); + + // bob now has a buy offer and a sell offer on the books. A broker + // spots this and swoops in to make a profit. + BEAST_EXPECT(nftCount(env, bob) == 1); + auto const bobsPriorBalance = env.balance(bob); + auto const brokersPriorBalance = env.balance(broker); + TER expectTer = features[fixUnburnableNFToken] + ? TER(tecCANT_ACCEPT_OWN_NFTOKEN_OFFER) + : TER(tesSUCCESS); + env(token::brokerOffers(broker, bobBuyOfferIndex, bobSellOfferIndex), + token::brokerFee(XRP(1)), + ter(expectTer)); + env.close(); + + if (expectTer == tesSUCCESS) + { + // bob should still have the NFT from alice, but be XRP(1) poorer. + // broker should be almost XRP(1) richer because they also paid a + // transaction fee. + BEAST_EXPECT(nftCount(env, bob) == 1); + BEAST_EXPECT(env.balance(bob) == bobsPriorBalance - XRP(1)); + BEAST_EXPECT( + env.balance(broker) == + brokersPriorBalance + XRP(1) - drops(10)); + } + else + { + // A tec result was returned, so no state should change other + // than the broker burning their transaction fee. + BEAST_EXPECT(nftCount(env, bob) == 1); + BEAST_EXPECT(env.balance(bob) == bobsPriorBalance); + BEAST_EXPECT( + env.balance(broker) == brokersPriorBalance - drops(10)); + } + } + void testWithFeats(FeatureBitset features) { @@ -5918,6 +6027,7 @@ class NFToken_test : public beast::unit_test::suite testNftXxxOffers(features); testFixNFTokenNegOffer(features); testIOUWithTransferFee(features); + testBrokeredSaleToSelf(features); } public: @@ -5928,10 +6038,9 @@ class NFToken_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - // TODO too many tests are being run - ths fixNFTDir check should be - // pushed into the tests that use it - testWithFeats(all - fixNFTDir); - testWithFeats(all - disallowIncoming); + testWithFeats(all - fixNFTDir - fixUnburnableNFToken); + testWithFeats(all - disallowIncoming - fixUnburnableNFToken); + testWithFeats(all - fixUnburnableNFToken); testWithFeats(all); } }; From c95f9e8b949053a814f3ad121036adb17b2fcae7 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Mon, 13 Feb 2023 10:48:14 -0500 Subject: [PATCH 12/38] [FOLD] clang --- src/test/app/NFToken_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 6a04cee7f90..e40bc1fb338 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6637,7 +6637,7 @@ class NFToken_test : public beast::unit_test::suite nftIDs.end()); } } - + void testWithFeats(FeatureBitset features) { From 1a276a04df80641f1a3d3db5bc665d8d5f657c6f Mon Sep 17 00:00:00 2001 From: ledhed2222 Date: Wed, 18 Jan 2023 16:37:39 -0500 Subject: [PATCH 13/38] Add fixUnburnableNFToken feature (#4391) --- src/ripple/protocol/Feature.h | 3 ++- src/ripple/protocol/impl/Feature.cpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 78b5b152c87..0bdfd224dda 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 56; +static constexpr std::size_t numFeatures = 57; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -343,6 +343,7 @@ extern uint256 const featureImmediateOfferKilled; extern uint256 const featureDisallowIncoming; extern uint256 const featureXRPFees; extern uint256 const fixUniversalNumber; +extern uint256 const fixUnburnableNFToken; } // namespace ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 2f01c39888a..f021ea4674d 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -453,6 +453,7 @@ REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no) REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no); REGISTER_FEATURE(XRPFees, Supported::yes, DefaultVote::no); REGISTER_FIX (fixUniversalNumber, Supported::yes, DefaultVote::no); +REGISTER_FIX (fixUnburnableNFToken, Supported::yes, DefaultVote::no); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. From 31959dbb4d542eb32d4a5abc5c83e5ff5be7f8e3 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Thu, 2 Feb 2023 18:48:03 -0500 Subject: [PATCH 14/38] Allow NFT to be burned when number of offers is greater than 500 (#4346) * Allow offers to be removable * Delete sell offers first Signed-off-by: Shawn Xie --- src/ripple/app/tx/impl/NFTokenBurn.cpp | 46 ++- .../app/tx/impl/details/NFTokenUtils.cpp | 67 ++-- src/ripple/app/tx/impl/details/NFTokenUtils.h | 10 +- src/test/app/NFTokenBurn_test.cpp | 323 ++++++++++++++---- 4 files changed, 347 insertions(+), 99 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenBurn.cpp b/src/ripple/app/tx/impl/NFTokenBurn.cpp index da23d78bdbd..e8693c7c6fb 100644 --- a/src/ripple/app/tx/impl/NFTokenBurn.cpp +++ b/src/ripple/app/tx/impl/NFTokenBurn.cpp @@ -77,9 +77,14 @@ NFTokenBurn::preclaim(PreclaimContext const& ctx) } } - // If there are too many offers, then burning the token would produce too - // much metadata. Disallow burning a token with too many offers. - return nft::notTooManyOffers(ctx.view, ctx.tx[sfNFTokenID]); + if (!ctx.view.rules().enabled(fixUnburnableNFToken)) + { + // If there are too many offers, then burning the token would produce + // too much metadata. Disallow burning a token with too many offers. + return nft::notTooManyOffers(ctx.view, ctx.tx[sfNFTokenID]); + } + + return tesSUCCESS; } TER @@ -104,9 +109,38 @@ NFTokenBurn::doApply() view().update(issuer); } - // Optimized deletion of all offers. - nft::removeAllTokenOffers(view(), keylet::nft_sells(ctx_.tx[sfNFTokenID])); - nft::removeAllTokenOffers(view(), keylet::nft_buys(ctx_.tx[sfNFTokenID])); + if (ctx_.view().rules().enabled(fixUnburnableNFToken)) + { + // Delete up to 500 offers in total. + // Because the number of sell offers is likely to be less than + // the number of buy offers, we prioritize the deletion of sell + // offers in order to clean up sell offer directory + std::size_t const deletedSellOffers = nft::removeTokenOffersWithLimit( + view(), + keylet::nft_sells(ctx_.tx[sfNFTokenID]), + maxDeletableTokenOfferEntries); + + if (maxDeletableTokenOfferEntries > deletedSellOffers) + { + nft::removeTokenOffersWithLimit( + view(), + keylet::nft_buys(ctx_.tx[sfNFTokenID]), + maxDeletableTokenOfferEntries - deletedSellOffers); + } + } + else + { + // Deletion of all offers. + nft::removeTokenOffersWithLimit( + view(), + keylet::nft_sells(ctx_.tx[sfNFTokenID]), + std::numeric_limits::max()); + + nft::removeTokenOffersWithLimit( + view(), + keylet::nft_buys(ctx_.tx[sfNFTokenID]), + std::numeric_limits::max()); + } return tesSUCCESS; } diff --git a/src/ripple/app/tx/impl/details/NFTokenUtils.cpp b/src/ripple/app/tx/impl/details/NFTokenUtils.cpp index d1214a98ee8..db2c3ae62f7 100644 --- a/src/ripple/app/tx/impl/details/NFTokenUtils.cpp +++ b/src/ripple/app/tx/impl/details/NFTokenUtils.cpp @@ -520,34 +520,55 @@ findTokenAndPage( } return std::nullopt; } -void -removeAllTokenOffers(ApplyView& view, Keylet const& directory) -{ - view.dirDelete(directory, [&view](uint256 const& id) { - auto offer = view.peek(Keylet{ltNFTOKEN_OFFER, id}); - if (!offer) - Throw( - "Offer " + to_string(id) + " not found in ledger!"); +std::size_t +removeTokenOffersWithLimit( + ApplyView& view, + Keylet const& directory, + std::size_t maxDeletableOffers) +{ + if (maxDeletableOffers == 0) + return 0; - auto const owner = (*offer)[sfOwner]; + std::optional pageIndex{0}; + std::size_t deletedOffersCount = 0; - if (!view.dirRemove( - keylet::ownerDir(owner), - (*offer)[sfOwnerNode], - offer->key(), - false)) - Throw( - "Offer " + to_string(id) + " not found in owner directory!"); + do + { + auto const page = view.peek(keylet::page(directory, *pageIndex)); + if (!page) + break; + + // We get the index of the next page in case the current + // page is deleted after all of its entries have been removed + pageIndex = (*page)[~sfIndexNext]; + + auto offerIndexes = page->getFieldV256(sfIndexes); + + // We reverse-iterate the offer directory page to delete all entries. + // Deleting an entry in a NFTokenOffer directory page won't cause + // entries from other pages to move to the current, so, it is safe to + // delete entries one by one in the page. It is required to iterate + // backwards to handle iterator invalidation for vector, as we are + // deleting during iteration. + for (int i = offerIndexes.size() - 1; i >= 0; --i) + { + if (auto const offer = view.peek(keylet::nftoffer(offerIndexes[i]))) + { + if (deleteTokenOffer(view, offer)) + ++deletedOffersCount; + else + Throw( + "Offer " + to_string(offerIndexes[i]) + + " cannot be deleted!"); + } - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - -1, - beast::Journal{beast::Journal::getNullSink()}); + if (maxDeletableOffers == deletedOffersCount) + break; + } + } while (pageIndex.value_or(0) && maxDeletableOffers != deletedOffersCount); - view.erase(offer); - }); + return deletedOffersCount; } TER diff --git a/src/ripple/app/tx/impl/details/NFTokenUtils.h b/src/ripple/app/tx/impl/details/NFTokenUtils.h index fa8c43b5877..db7cf00be10 100644 --- a/src/ripple/app/tx/impl/details/NFTokenUtils.h +++ b/src/ripple/app/tx/impl/details/NFTokenUtils.h @@ -53,9 +53,13 @@ constexpr std::uint16_t const flagOnlyXRP = 0x0002; constexpr std::uint16_t const flagCreateTrustLines = 0x0004; constexpr std::uint16_t const flagTransferable = 0x0008; -/** Deletes all offers from the specified token offer directory. */ -void -removeAllTokenOffers(ApplyView& view, Keylet const& directory); +/** Delete up to a specified number of offers from the specified token offer + * directory. */ +std::size_t +removeTokenOffersWithLimit( + ApplyView& view, + Keylet const& directory, + std::size_t maxDeletableOffers); /** Returns tesSUCCESS if NFToken has few enough offers that it can be burned */ TER diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 00124731cb9..4896932acd2 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -49,6 +49,37 @@ class NFTokenBurn_test : public beast::unit_test::suite return nfts[jss::result][jss::account_nfts].size(); }; + // Helper function that returns new nft id for an account and create + // specified number of sell offers + uint256 + createNftAndOffers( + test::jtx::Env& env, + test::jtx::Account const& owner, + std::vector& offerIndexes, + size_t const tokenCancelCount) + { + using namespace test::jtx; + uint256 const nftokenID = + token::getNextID(env, owner, 0, tfTransferable); + env(token::mint(owner, 0), + token::uri(std::string(maxTokenURILength, 'u')), + txflags(tfTransferable)); + env.close(); + + offerIndexes.reserve(tokenCancelCount); + + for (uint32_t i = 0; i < tokenCancelCount; ++i) + { + // Create sell offer + offerIndexes.push_back(keylet::nftoffer(owner, env.seq(owner)).key); + env(token::createOffer(owner, nftokenID, drops(1)), + txflags(tfSellNFToken)); + env.close(); + } + + return nftokenID; + }; + void testBurnRandom(FeatureBitset features) { @@ -492,94 +523,251 @@ class NFTokenBurn_test : public beast::unit_test::suite using namespace test::jtx; - Env env{*this, features}; + // Test what happens if a NFT is unburnable when there are + // more than 500 offers, before fixUnburnableNFToken goes live + if (!features[fixUnburnableNFToken]) + { + Env env{*this, features}; - Account const alice("alice"); - Account const becky("becky"); - env.fund(XRP(1000), alice, becky); - env.close(); + Account const alice("alice"); + Account const becky("becky"); + env.fund(XRP(1000), alice, becky); + env.close(); - // We structure the test to try and maximize the metadata produced. - // This verifies that we don't create too much metadata during a - // maximal burn operation. - // - // 1. alice mints an nft with a full-sized URI. - // 2. We create 1000 new accounts, each of which creates an offer for - // alice's nft. - // 3. becky creates one more offer for alice's NFT - // 4. Attempt to burn the nft which fails because there are too - // many offers. - // 5. Cancel becky's offer and the nft should become burnable. - uint256 const nftokenID = - token::getNextID(env, alice, 0, tfTransferable); - env(token::mint(alice, 0), - token::uri(std::string(maxTokenURILength, 'u')), - txflags(tfTransferable)); - env.close(); + // We structure the test to try and maximize the metadata produced. + // This verifies that we don't create too much metadata during a + // maximal burn operation. + // + // 1. alice mints an nft with a full-sized URI. + // 2. We create 500 new accounts, each of which creates an offer + // for alice's nft. + // 3. becky creates one more offer for alice's NFT + // 4. Attempt to burn the nft which fails because there are too + // many offers. + // 5. Cancel becky's offer and the nft should become burnable. + uint256 const nftokenID = + token::getNextID(env, alice, 0, tfTransferable); + env(token::mint(alice, 0), + token::uri(std::string(maxTokenURILength, 'u')), + txflags(tfTransferable)); + env.close(); - std::vector offerIndexes; - offerIndexes.reserve(maxTokenOfferCancelCount); - for (uint32_t i = 0; i < maxTokenOfferCancelCount; ++i) + std::vector offerIndexes; + offerIndexes.reserve(maxTokenOfferCancelCount); + for (std::uint32_t i = 0; i < maxTokenOfferCancelCount; ++i) + { + Account const acct(std::string("acct") + std::to_string(i)); + env.fund(XRP(1000), acct); + env.close(); + + offerIndexes.push_back( + keylet::nftoffer(acct, env.seq(acct)).key); + env(token::createOffer(acct, nftokenID, drops(1)), + token::owner(alice)); + env.close(); + } + + // Verify all offers are present in the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); + } + + // Create one too many offers. + uint256 const beckyOfferIndex = + keylet::nftoffer(becky, env.seq(becky)).key; + env(token::createOffer(becky, nftokenID, drops(1)), + token::owner(alice)); + + // Attempt to burn the nft which should fail. + env(token::burn(alice, nftokenID), ter(tefTOO_BIG)); + + // Close enough ledgers that the burn transaction is no longer + // retried. + for (int i = 0; i < 10; ++i) + env.close(); + + // Cancel becky's offer, but alice adds a sell offer. The token + // should still not be burnable. + env(token::cancelOffer(becky, {beckyOfferIndex})); + env.close(); + + uint256 const aliceOfferIndex = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftokenID, drops(1)), + txflags(tfSellNFToken)); + env.close(); + + env(token::burn(alice, nftokenID), ter(tefTOO_BIG)); + env.close(); + + // Cancel alice's sell offer. Now the token should be burnable. + env(token::cancelOffer(alice, {aliceOfferIndex})); + env.close(); + + env(token::burn(alice, nftokenID)); + env.close(); + + // Burning the token should remove all the offers from the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex))); + } + + // Both alice and becky should have ownerCounts of zero. + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, becky) == 0); + } + + // Test that up to 499 buy/sell offers will be removed when NFT is + // burned after fixUnburnableNFToken is enabled. This is to test that we + // can successfully remove all offers if the number of offers is less + // than 500. + if (features[fixUnburnableNFToken]) { - Account const acct(std::string("acct") + std::to_string(i)); - env.fund(XRP(1000), acct); + Env env{*this, features}; + + Account const alice("alice"); + Account const becky("becky"); + env.fund(XRP(100000), alice, becky); env.close(); - offerIndexes.push_back(keylet::nftoffer(acct, env.seq(acct)).key); - env(token::createOffer(acct, nftokenID, drops(1)), + // alice creates 498 sell offers and becky creates 1 buy offers. + // When the token is burned, 498 sell offers and 1 buy offer are + // removed. In total, 499 offers are removed + std::vector offerIndexes; + auto const nftokenID = createNftAndOffers( + env, alice, offerIndexes, maxDeletableTokenOfferEntries - 2); + + // Verify all sell offers are present in the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); + } + + // Becky creates a buy offer + uint256 const beckyOfferIndex = + keylet::nftoffer(becky, env.seq(becky)).key; + env(token::createOffer(becky, nftokenID, drops(1)), token::owner(alice)); env.close(); + + // Burn the token + env(token::burn(alice, nftokenID)); + env.close(); + + // Burning the token should remove all 498 sell offers + // that alice created + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex))); + } + + // Burning the token should also remove the one buy offer + // that becky created + BEAST_EXPECT(!env.le(keylet::nftoffer(beckyOfferIndex))); + + // alice and becky should have ownerCounts of zero + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, becky) == 0); } - // Verify all offers are present in the ledger. - for (uint256 const& offerIndex : offerIndexes) + // Test that up to 500 buy offers are removed when NFT is burned + // after fixUnburnableNFToken is enabled + if (features[fixUnburnableNFToken]) { - BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); - } + Env env{*this, features}; - // Create one too many offers. - uint256 const beckyOfferIndex = - keylet::nftoffer(becky, env.seq(becky)).key; - env(token::createOffer(becky, nftokenID, drops(1)), - token::owner(alice)); + Account const alice("alice"); + Account const becky("becky"); + env.fund(XRP(100000), alice, becky); + env.close(); - // Attempt to burn the nft which should fail. - env(token::burn(alice, nftokenID), ter(tefTOO_BIG)); + // alice creates 501 sell offers for the token + // After we burn the token, 500 of the sell offers should be + // removed, and one is left over + std::vector offerIndexes; + auto const nftokenID = createNftAndOffers( + env, alice, offerIndexes, maxDeletableTokenOfferEntries + 1); - // Close enough ledgers that the burn transaction is no longer retried. - for (int i = 0; i < 10; ++i) - env.close(); + // Verify all sell offers are present in the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); + } - // Cancel becky's offer, but alice adds a sell offer. The token - // should still not be burnable. - env(token::cancelOffer(becky, {beckyOfferIndex})); - env.close(); + // Burn the token + env(token::burn(alice, nftokenID)); + env.close(); - uint256 const aliceOfferIndex = - keylet::nftoffer(alice, env.seq(alice)).key; - env(token::createOffer(alice, nftokenID, drops(1)), - txflags(tfSellNFToken)); - env.close(); + uint32_t offerDeletedCount = 0; + // Count the number of sell offers that have been deleted + for (uint256 const& offerIndex : offerIndexes) + { + if (!env.le(keylet::nftoffer(offerIndex))) + offerDeletedCount++; + } - env(token::burn(alice, nftokenID), ter(tefTOO_BIG)); - env.close(); + BEAST_EXPECT(offerIndexes.size() == maxTokenOfferCancelCount + 1); - // Cancel alice's sell offer. Now the token should be burnable. - env(token::cancelOffer(alice, {aliceOfferIndex})); - env.close(); + // 500 sell offers should be removed + BEAST_EXPECT(offerDeletedCount == maxTokenOfferCancelCount); - env(token::burn(alice, nftokenID)); - env.close(); + // alice should have ownerCounts of one for the orphaned sell offer + BEAST_EXPECT(ownerCount(env, alice) == 1); + } - // Burning the token should remove all the offers from the ledger. - for (uint256 const& offerIndex : offerIndexes) + // Test that up to 500 buy/sell offers are removed when NFT is burned + // after fixUnburnableNFToken is enabled + if (features[fixUnburnableNFToken]) { - BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex))); - } + Env env{*this, features}; - // Both alice and becky should have ownerCounts of zero. - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(ownerCount(env, becky) == 0); + Account const alice("alice"); + Account const becky("becky"); + env.fund(XRP(100000), alice, becky); + env.close(); + + // alice creates 499 sell offers and becky creates 2 buy offers. + // When the token is burned, 499 sell offers and 1 buy offer + // are removed. + // In total, 500 offers are removed + std::vector offerIndexes; + auto const nftokenID = createNftAndOffers( + env, alice, offerIndexes, maxDeletableTokenOfferEntries - 1); + + // Verify all sell offers are present in the ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex))); + } + + // becky creates 2 buy offers + env(token::createOffer(becky, nftokenID, drops(1)), + token::owner(alice)); + env.close(); + env(token::createOffer(becky, nftokenID, drops(1)), + token::owner(alice)); + env.close(); + + // Burn the token + env(token::burn(alice, nftokenID)); + env.close(); + + // Burning the token should remove all 499 sell offers from the + // ledger. + for (uint256 const& offerIndex : offerIndexes) + { + BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex))); + } + + // alice should have ownerCount of zero because all her + // sell offers have been deleted + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // becky has ownerCount of one due to an orphaned buy offer + BEAST_EXPECT(ownerCount(env, becky) == 1); + } } void @@ -598,7 +786,8 @@ class NFTokenBurn_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixNFTDir); + testWithFeats(all - fixUnburnableNFToken - fixNFTDir); + testWithFeats(all - fixUnburnableNFToken); testWithFeats(all); } }; From bdaeee2138f8bb7bd8e4e24c92bd7798e663bcbb Mon Sep 17 00:00:00 2001 From: ledhed2222 Date: Thu, 9 Feb 2023 16:57:51 -0500 Subject: [PATCH 15/38] Fix 3 issues around NFToken offer acceptance (#4380) Fixes 3 issues: In the following scenario, an account cannot perform NFTokenAcceptOffer even though it should be allowed to: - BROKER has < S - ALICE offers to sell token for S - BOB offers to buy token for > S - BROKER tries to bridge the two offers This currently results in `tecINSUFFICIENT_FUNDS`, but should not because BROKER is not spending any funds in this transaction, beyond the transaction fee. When trading an NFT using IOUs, and when the issuer of the IOU has any non-zero value set for TransferFee on their account via AccountSet (not a TransferFee on the NFT), and when the sale amount is equal to the total balance of that IOU that the buyer has, the resulting balance for the issuer of the IOU will become positive. This means that the buyer of the NFT was supposed to have caused a certain amount of IOU to be burned. That amount was unable to be burned because the buyer couldn't cover it. This results in the buyer owing this amount back to the issuer. In a real world scenario, this is appropriate and can be settled off-chain. Currency issuers could not make offers for NFTs using their own currency, receiving `tecINSUFFICIENT_FUNDS` if they tried to do so. With this fix, they are now able to buy/sell NFTs using their own currency. --- src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp | 74 +- src/ripple/app/tx/impl/NFTokenCreateOffer.cpp | 31 +- src/ripple/ledger/View.h | 5 + src/test/app/NFToken_test.cpp | 1650 +++++++++++++---- 4 files changed, 1339 insertions(+), 421 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp index 07fe9957a76..c335f8d28fd 100644 --- a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp @@ -168,10 +168,21 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) dest.has_value() && *dest != ctx.tx[sfAccount]) return tecNO_PERMISSION; } + // The account offering to buy must have funds: + // + // After this amendment, we allow an IOU issuer to buy an NFT with their + // own currency auto const needed = bo->at(sfAmount); - - if (accountHolds( + if (ctx.view.rules().enabled(fixUnburnableNFToken)) + { + if (accountFunds( + ctx.view, (*bo)[sfOwner], needed, fhZERO_IF_FROZEN, ctx.j) < + needed) + return tecINSUFFICIENT_FUNDS; + } + else if ( + accountHolds( ctx.view, (*bo)[sfOwner], needed.getCurrency(), @@ -206,15 +217,39 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // The account offering to buy must have funds: auto const needed = so->at(sfAmount); - - if (accountHolds( - ctx.view, - ctx.tx[sfAccount], - needed.getCurrency(), - needed.getIssuer(), - fhZERO_IF_FROZEN, - ctx.j) < needed) - return tecINSUFFICIENT_FUNDS; + if (!ctx.view.rules().enabled(fixUnburnableNFToken)) + { + if (accountHolds( + ctx.view, + ctx.tx[sfAccount], + needed.getCurrency(), + needed.getIssuer(), + fhZERO_IF_FROZEN, + ctx.j) < needed) + return tecINSUFFICIENT_FUNDS; + } + else if (!bo) + { + // After this amendment, we allow buyers to buy with their own + // issued currency. + // + // In the case of brokered mode, this check is essentially + // redundant, since we have already confirmed that buy offer is > + // than the sell offer, and that the buyer can cover the buy + // offer. + // + // We also _must not_ check the tx submitter in brokered + // mode, because then we are confirming that the broker can + // cover what the buyer will pay, which doesn't make sense, causes + // an unncessary tec, and is also resolved with this amendment. + if (accountFunds( + ctx.view, + ctx.tx[sfAccount], + needed, + fhZERO_IF_FROZEN, + ctx.j) < needed) + return tecINSUFFICIENT_FUNDS; + } } return tesSUCCESS; @@ -230,7 +265,22 @@ NFTokenAcceptOffer::pay( if (amount < beast::zero) return tecINTERNAL; - return accountSend(view(), from, to, amount, j_); + auto const result = accountSend(view(), from, to, amount, j_); + + // After this amendment, if any payment would cause a non-IOU-issuer to + // have a negative balance, or an IOU-issuer to have a positive balance in + // their own currency, we know that something went wrong. This was + // originally found in the context of IOU transfer fees. Since there are + // several payouts in this tx, just confirm that the end state is OK. + if (!view().rules().enabled(fixUnburnableNFToken)) + return result; + if (result != tesSUCCESS) + return result; + if (accountFunds(view(), from, amount, fhZERO_IF_FROZEN, j_).signum() < 0) + return tecINSUFFICIENT_FUNDS; + if (accountFunds(view(), to, amount, fhZERO_IF_FROZEN, j_).signum() < 0) + return tecINSUFFICIENT_FUNDS; + return tesSUCCESS; } TER diff --git a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp index 695efdd0aa4..ff8668e4488 100644 --- a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp @@ -153,15 +153,28 @@ NFTokenCreateOffer::preclaim(PreclaimContext const& ctx) // offer may later become unfunded. if (!isSellOffer) { - auto const funds = accountHolds( - ctx.view, - ctx.tx[sfAccount], - amount.getCurrency(), - amount.getIssuer(), - FreezeHandling::fhZERO_IF_FROZEN, - ctx.j); - - if (funds.signum() <= 0) + // After this amendment, we allow an IOU issuer to make a buy offer + // using their own currency. + if (ctx.view.rules().enabled(fixUnburnableNFToken)) + { + if (accountFunds( + ctx.view, + ctx.tx[sfAccount], + amount, + FreezeHandling::fhZERO_IF_FROZEN, + ctx.j) + .signum() <= 0) + return tecUNFUNDED_OFFER; + } + else if ( + accountHolds( + ctx.view, + ctx.tx[sfAccount], + amount.getCurrency(), + amount.getIssuer(), + FreezeHandling::fhZERO_IF_FROZEN, + ctx.j) + .signum() <= 0) return tecUNFUNDED_OFFER; } diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index ee917115515..24a647c768d 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -97,6 +97,11 @@ accountHolds( FreezeHandling zeroIfFrozen, beast::Journal j); +// Returns the amount an account can spend of the currency type saDefault, or +// returns saDefault if this account is the issuer of the the currency in +// question. Should be used in favor of accountHolds when questioning how much +// an account can spend while also allowing currency issuers to spend +// unlimited amounts of their own currency (since they can always issue more). [[nodiscard]] STAmount accountFunds( ReadView const& view, diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 842f3f76cc8..33d725e5a17 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -3827,497 +3827,586 @@ class NFToken_test : public beast::unit_test::suite using namespace test::jtx; - Env env{*this, features}; - - // The most important thing to explore here is the way funds are - // assigned from the buyer to... - // o the Seller, - // o the Broker, and - // o the Issuer (in the case of a transfer fee). - - Account const issuer{"issuer"}; - Account const minter{"minter"}; - Account const buyer{"buyer"}; - Account const broker{"broker"}; - Account const gw{"gw"}; - IOU const gwXAU(gw["XAU"]); - - env.fund(XRP(1000), issuer, minter, buyer, broker, gw); - env.close(); - - env(trust(issuer, gwXAU(2000))); - env(trust(minter, gwXAU(2000))); - env(trust(buyer, gwXAU(2000))); - env(trust(broker, gwXAU(2000))); - env.close(); - - env(token::setMinter(issuer, minter)); - env.close(); - - // Lambda to check owner count of all accounts is one. - auto checkOwnerCountIsOne = - [this, &env]( - std::initializer_list> - accounts, - int line) { - for (Account const& acct : accounts) - { - if (std::uint32_t ownerCount = this->ownerCount(env, acct); - ownerCount != 1) - { - std::stringstream ss; - ss << "Account " << acct.human() - << " expected ownerCount == 1. Got " << ownerCount; - fail(ss.str(), __FILE__, line); - } - } - }; - - // Lambda that mints an NFT and returns the nftID. - auto mintNFT = [&env, &issuer, &minter](std::uint16_t xferFee = 0) { - uint256 const nftID = - token::getNextID(env, issuer, 0, tfTransferable, xferFee); - env(token::mint(minter, 0), - token::issuer(issuer), - token::xferFee(xferFee), - txflags(tfTransferable)); - env.close(); - return nftID; - }; - - // o Seller is selling for zero XRP. - // o Broker charges no fee. - // o No transfer fee. - // - // Since minter is selling for zero the currency must be XRP. + for (auto const& tweakedFeatures : + {features - fixUnburnableNFToken, features | fixUnburnableNFToken}) { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + Env env{*this, tweakedFeatures}; - uint256 const nftID = mintNFT(); + // The most important thing to explore here is the way funds are + // assigned from the buyer to... + // o the Seller, + // o the Broker, and + // o the Issuer (in the case of a transfer fee). - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); + Account const issuer{"issuer"}; + Account const minter{"minter"}; + Account const buyer{"buyer"}; + Account const broker{"broker"}; + Account const gw{"gw"}; + IOU const gwXAU(gw["XAU"]); - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); + env.fund(XRP(1000), issuer, minter, buyer, broker, gw); env.close(); - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); - - // Broker charges no brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex)); + env(trust(issuer, gwXAU(2000))); + env(trust(minter, gwXAU(2000))); + env(trust(buyer, gwXAU(2000))); + env(trust(broker, gwXAU(2000))); env.close(); - // Note that minter's XRP balance goes up even though they - // requested XRP(0). - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(1)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance); - - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); + env(token::setMinter(issuer, minter)); env.close(); - } - // o Seller is selling for zero XRP. - // o Broker charges a fee. - // o No transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + // Lambda to check owner count of all accounts is one. + auto checkOwnerCountIsOne = + [this, &env]( + std::initializer_list> + accounts, + int line) { + for (Account const& acct : accounts) + { + if (std::uint32_t ownerCount = + this->ownerCount(env, acct); + ownerCount != 1) + { + std::stringstream ss; + ss << "Account " << acct.human() + << " expected ownerCount == 1. Got " + << ownerCount; + fail(ss.str(), __FILE__, line); + } + } + }; - uint256 const nftID = mintNFT(); + // Lambda that mints an NFT and returns the nftID. + auto mintNFT = [&env, &issuer, &minter](std::uint16_t xferFee = 0) { + uint256 const nftID = + token::getNextID(env, issuer, 0, tfTransferable, xferFee); + env(token::mint(minter, 0), + token::issuer(issuer), + token::xferFee(xferFee), + txflags(tfTransferable)); + env.close(); + return nftID; + }; - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); + // o Seller is selling for zero XRP. + // o Broker charges no fee. + // o No transfer fee. + // + // Since minter is selling for zero the currency must be XRP. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); + uint256 const nftID = mintNFT(); - // Broker attempts to charge a 1.1 XRP brokerFee and fails. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(XRP(1.1)), - ter(tecINSUFFICIENT_PAYMENT)); - env.close(); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); + // buyer creates their offer. Note: a buy offer can never + // offer zero. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, XRP(1)), + token::owner(minter)); + env.close(); - // Broker charges a 0.5 XRP brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(XRP(0.5))); - env.close(); + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); - // Note that minter's XRP balance goes up even though they - // requested XRP(0). - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT( - env.balance(broker) == brokerBalance + XRP(0.5) - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance); + // Broker charges no brokerFee. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex)); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } + // Note that minter's XRP balance goes up even though they + // requested XRP(0). + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(1)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance); - // o Seller is selling for zero XRP. - // o Broker charges no fee. - // o 50% transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } - uint256 const nftID = mintNFT(maxTransferFee); + // o Seller is selling for zero XRP. + // o Broker charges a fee. + // o No transfer fee. + // + // Since minter is selling for zero the currency must be XRP. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); + uint256 const nftID = mintNFT(); - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); + // buyer creates their offer. Note: a buy offer can never + // offer zero. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, XRP(1)), + token::owner(minter)); + env.close(); - // Broker charges no brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex)); - env.close(); + // Broker attempts to charge a 1.1 XRP brokerFee and fails. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(XRP(1.1)), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); - // Note that minter's XRP balance goes up even though they - // requested XRP(0). - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.5)); + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } + // Broker charges a 0.5 XRP brokerFee. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(XRP(0.5))); + env.close(); - // o Seller is selling for zero XRP. - // o Broker charges 0.5 XRP. - // o 50% transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + // Note that minter's XRP balance goes up even though they + // requested XRP(0). + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT( + env.balance(broker) == + brokerBalance + XRP(0.5) - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } - uint256 const nftID = mintNFT(maxTransferFee); + // o Seller is selling for zero XRP. + // o Broker charges no fee. + // o 50% transfer fee. + // + // Since minter is selling for zero the currency must be XRP. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); + uint256 const nftID = mintNFT(maxTransferFee); - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); + // buyer creates their offer. Note: a buy offer can never + // offer zero. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, XRP(1)), + token::owner(minter)); + env.close(); - // Broker charges a 0.75 XRP brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(XRP(0.75))); - env.close(); + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); - // Note that, with a 50% transfer fee, issuer gets 1/2 of what's - // left _after_ broker takes their fee. minter gets the remainder - // after both broker and minter take their cuts - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.125)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT( - env.balance(broker) == brokerBalance + XRP(0.75) - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.125)); + // Broker charges no brokerFee. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex)); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } + // Note that minter's XRP balance goes up even though they + // requested XRP(0). + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.5)); - // Lambda to set the balance of all passed in accounts to gwXAU(1000). - auto setXAUBalance_1000 = - [this, &gw, &gwXAU, &env]( - std::initializer_list> - accounts, - int line) { - for (Account const& acct : accounts) - { - static const auto xau1000 = gwXAU(1000); - auto const balance = env.balance(acct, gwXAU); - if (balance < xau1000) - { - env(pay(gw, acct, xau1000 - balance)); - env.close(); - } - else if (balance > xau1000) - { - env(pay(acct, gw, balance - xau1000)); - env.close(); - } - if (env.balance(acct, gwXAU) != xau1000) - { - std::stringstream ss; - ss << "Unable to set " << acct.human() - << " account balance to gwXAU(1000)"; - this->fail(ss.str(), __FILE__, line); - } - } - }; + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } - // The buyer and seller have identical amounts and there is no - // transfer fee. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__); + // o Seller is selling for zero XRP. + // o Broker charges 0.5 XRP. + // o 50% transfer fee. + // + // Since minter is selling for zero the currency must be XRP. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - uint256 const nftID = mintNFT(); + uint256 const nftID = mintNFT(maxTransferFee); - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, gwXAU(1000)), - txflags(tfSellNFToken)); - env.close(); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); - { - // buyer creates an offer for more XAU than they currently own. + // buyer creates their offer. Note: a buy offer can never + // offer zero. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1001)), + env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); + + // Broker charges a 0.75 XRP brokerFee. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), - ter(tecINSUFFICIENT_FUNDS)); + token::brokerFee(XRP(0.75))); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + // Note that, with a 50% transfer fee, issuer gets 1/2 of what's + // left _after_ broker takes their fee. minter gets the + // remainder after both broker and minter take their cuts + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.125)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT( + env.balance(broker) == + brokerBalance + XRP(0.75) - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.125)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } + + // Lambda to set the balance of all passed in accounts to + // gwXAU(amount). + auto setXAUBalance = + [this, &gw, &gwXAU, &env]( + std::initializer_list> + accounts, + int amount, + int line) { + for (Account const& acct : accounts) + { + auto const xauAmt = gwXAU(amount); + auto const balance = env.balance(acct, gwXAU); + if (balance < xauAmt) + { + env(pay(gw, acct, xauAmt - balance)); + env.close(); + } + else if (balance > xauAmt) + { + env(pay(acct, gw, balance - xauAmt)); + env.close(); + } + if (env.balance(acct, gwXAU) != xauAmt) + { + std::stringstream ss; + ss << "Unable to set " << acct.human() + << " account balance to gwXAU(" << amount << ")"; + this->fail(ss.str(), __FILE__, line); + } + } + }; + + // The buyer and seller have identical amounts and there is no + // transfer fee. { - // buyer creates an offer for less that what minter is asking. + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer, broker}, 1000, __LINE__); + + uint256 const nftID = mintNFT(); + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(1000)), + txflags(tfSellNFToken)); + env.close(); + + { + // buyer creates an offer for more XAU than they currently + // own. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1001)), + token::owner(minter)); + env.close(); + + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + { + // buyer creates an offer for less that what minter is + // asking. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(999)), + token::owner(minter)); + env.close(); + + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); + + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + + // buyer creates a large enough offer. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(999)), + env(token::createOffer(buyer, nftID, gwXAU(1000)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + // Broker attempts to charge a brokerFee but cannot. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(0.1)), ter(tecINSUFFICIENT_PAYMENT)); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + // broker charges no brokerFee and succeeds. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex)); + env.close(); + + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(2000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1000)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } - // buyer creates a large enough offer. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1000)), - token::owner(minter)); - env.close(); + // seller offers more than buyer is asking. + // There are both transfer and broker fees. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer, broker}, 1000, __LINE__); - // Broker attempts to charge a brokerFee but cannot. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(0.1)), - ter(tecINSUFFICIENT_PAYMENT)); - env.close(); + uint256 const nftID = mintNFT(maxTransferFee); - // broker charges no brokerFee and succeeds. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex)); - env.close(); - - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); - BEAST_EXPECT(ownerCount(env, broker) == 1); - BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000)); - BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(2000)); - BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); - BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1000)); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(900)), + txflags(tfSellNFToken)); + env.close(); + { + // buyer creates an offer for more XAU than they currently + // own. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1001)), + token::owner(minter)); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); - // seller offers more than buyer is asking. - // There are both transfer and broker fees. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__); + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + { + // buyer creates an offer for less that what minter is + // asking. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(899)), + token::owner(minter)); + env.close(); - uint256 const nftID = mintNFT(maxTransferFee); + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, gwXAU(900)), - txflags(tfSellNFToken)); - env.close(); - { - // buyer creates an offer for more XAU than they currently own. + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + // buyer creates a large enough offer. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1001)), + env(token::createOffer(buyer, nftID, gwXAU(1000)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + // Broker attempts to charge a brokerFee larger than the + // difference between the two offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(101)), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); + + // broker charges the full difference between the two offers and + // succeeds. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), - ter(tecINSUFFICIENT_FUNDS)); + token::brokerFee(gwXAU(100))); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1450)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1450)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1100)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } + // seller offers more than buyer is asking. + // There are both transfer and broker fees, but broker takes less + // than the maximum. { - // buyer creates an offer for less that what minter is asking. + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer, broker}, 1000, __LINE__); + + uint256 const nftID = mintNFT(maxTransferFee / 2); // 25% + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(900)), + txflags(tfSellNFToken)); + env.close(); + + // buyer creates a large enough offer. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(899)), + env(token::createOffer(buyer, nftID, gwXAU(1000)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + // broker charges half difference between the two offers and + // succeeds. 25% of the remaining difference goes to issuer. + // The rest goes to minter. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), - ter(tecINSUFFICIENT_PAYMENT)); + token::brokerFee(gwXAU(50))); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1050)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } - // buyer creates a large enough offer. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1000)), - token::owner(minter)); - env.close(); - - // Broker attempts to charge a brokerFee larger than the - // difference between the two offers but cannot. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(101)), - ter(tecINSUFFICIENT_PAYMENT)); - env.close(); - - // broker charges the full difference between the two offers and - // succeeds. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(100))); - env.close(); - - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); - BEAST_EXPECT(ownerCount(env, broker) == 1); - BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1450)); - BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1450)); - BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); - BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1100)); - - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } - // seller offers more than buyer is asking. - // There are both transfer and broker fees, but broker takes less than - // the maximum. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__); - - uint256 const nftID = mintNFT(maxTransferFee / 2); // 25% - - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, gwXAU(900)), - txflags(tfSellNFToken)); - env.close(); - - // buyer creates a large enough offer. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1000)), - token::owner(minter)); - env.close(); - - // broker charges half difference between the two offers and - // succeeds. 25% of the remaining difference goes to issuer. - // The rest goes to minter. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(50))); - env.close(); + // Broker has a balance less than the seller offer + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer}, 1000, __LINE__); + setXAUBalance({broker}, 500, __LINE__); + uint256 const nftID = mintNFT(maxTransferFee / 2); // 25% + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(900)), + txflags(tfSellNFToken)); + env.close(); - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); - BEAST_EXPECT(ownerCount(env, broker) == 1); - BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5)); - BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5)); - BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); - BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1050)); + // buyer creates a large enough offer. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1000)), + token::owner(minter)); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); + if (tweakedFeatures[fixUnburnableNFToken]) + { + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(50))); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(550)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } + else + { + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(50)), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 3); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(500)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(minter, nftID)); + env.close(); + } + } } } @@ -4823,9 +4912,14 @@ class NFToken_test : public beast::unit_test::suite Account const gw{"gw"}; IOU const gwXAU(gw["XAU"]); - // Test both with and without fixNFTokenNegOffer + // Test both with and without fixNFTokenNegOffer and + // fixUnburnableNFToken. Need to turn off fixUnburnableNFToken as well + // because that amendment came later and addressed the acceptance + // side of this issue. for (auto const& tweakedFeatures : - {features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1, + {features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1 - + fixUnburnableNFToken, + features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1, features | fixNFTokenNegOffer}) { // There was a bug in the initial NFT implementation that @@ -4914,8 +5008,10 @@ class NFToken_test : public beast::unit_test::suite env.close(); } { - // 1. If fixNFTokenNegOffer is NOT enabled get tecSUCCESS. - // 2. If fixNFTokenNegOffer IS enabled get tecOBJECT_NOT_FOUND. + // 1. If fixNFTokenNegOffer is enabled get tecOBJECT_NOT_FOUND + // 2. If it is not enabled, but fixUnburnableNFToken is + // enabled, get tecOBJECT_NOT_FOUND. + // 3. If neither are enabled, get tesSUCCESS. TER const offerAcceptTER = tweakedFeatures[fixNFTokenNegOffer] ? static_cast(tecOBJECT_NOT_FOUND) : static_cast(tesSUCCESS); @@ -5047,6 +5143,757 @@ class NFToken_test : public beast::unit_test::suite } } + void + testIOUWithTransferFee(FeatureBitset features) + { + using namespace test::jtx; + + testcase("Payments with IOU transfer fees"); + + for (auto const& tweakedFeatures : + {features - fixUnburnableNFToken, features | fixUnburnableNFToken}) + { + Env env{*this, tweakedFeatures}; + + Account const minter{"minter"}; + Account const secondarySeller{"seller"}; + Account const buyer{"buyer"}; + Account const gw{"gateway"}; + Account const broker{"broker"}; + IOU const gwXAU(gw["XAU"]); + IOU const gwXPB(gw["XPB"]); + + env.fund(XRP(1000), gw, minter, secondarySeller, buyer, broker); + env.close(); + + env(trust(minter, gwXAU(2000))); + env(trust(secondarySeller, gwXAU(2000))); + env(trust(broker, gwXAU(10000))); + env(trust(buyer, gwXAU(2000))); + env(trust(buyer, gwXPB(2000))); + env.close(); + + // The IOU issuer has a 2% transfer rate + env(rate(gw, 1.02)); + env.close(); + + auto expectInitialState = [this, + &env, + &buyer, + &minter, + &secondarySeller, + &broker, + &gw, + &gwXAU, + &gwXPB]() { + // Buyer should have XAU 1000, XPB 0 + // Minter should have XAU 0, XPB 0 + // Secondary seller should have XAU 0, XPB 0 + // Broker should have XAU 5000, XPB 0 + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(minter, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(secondarySeller, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(5000)); + BEAST_EXPECT(env.balance(broker, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XPB"]) == gwXPB(0)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, minter["XPB"]) == gwXPB(0)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(0)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XPB"]) == gwXPB(0)); + BEAST_EXPECT(env.balance(gw, broker["XAU"]) == gwXAU(-5000)); + BEAST_EXPECT(env.balance(gw, broker["XPB"]) == gwXPB(0)); + }; + + auto reinitializeTrustLineBalances = [&expectInitialState, + &env, + &buyer, + &minter, + &secondarySeller, + &broker, + &gw, + &gwXAU, + &gwXPB]() { + if (auto const difference = + gwXAU(1000) - env.balance(buyer, gwXAU); + difference > gwXAU(0)) + env(pay(gw, buyer, difference)); + if (env.balance(buyer, gwXPB) > gwXPB(0)) + env(pay(buyer, gw, env.balance(buyer, gwXPB))); + if (env.balance(minter, gwXAU) > gwXAU(0)) + env(pay(minter, gw, env.balance(minter, gwXAU))); + if (env.balance(minter, gwXPB) > gwXPB(0)) + env(pay(minter, gw, env.balance(minter, gwXPB))); + if (env.balance(secondarySeller, gwXAU) > gwXAU(0)) + env( + pay(secondarySeller, + gw, + env.balance(secondarySeller, gwXAU))); + if (env.balance(secondarySeller, gwXPB) > gwXPB(0)) + env( + pay(secondarySeller, + gw, + env.balance(secondarySeller, gwXPB))); + auto brokerDiff = gwXAU(5000) - env.balance(broker, gwXAU); + if (brokerDiff > gwXAU(0)) + env(pay(gw, broker, brokerDiff)); + else if (brokerDiff < gwXAU(0)) + { + brokerDiff.negate(); + env(pay(broker, gw, brokerDiff)); + } + if (env.balance(broker, gwXPB) > gwXPB(0)) + env(pay(broker, gw, env.balance(broker, gwXPB))); + env.close(); + expectInitialState(); + }; + + auto mintNFT = [&env](Account const& minter, int transferFee = 0) { + uint256 const nftID = token::getNextID( + env, minter, 0, tfTransferable, transferFee); + env(token::mint(minter), + token::xferFee(transferFee), + txflags(tfTransferable)); + env.close(); + return nftID; + }; + + auto createBuyOffer = + [&env]( + Account const& offerer, + Account const& owner, + uint256 const& nftID, + STAmount const& amount, + std::optional const terCode = {}) { + uint256 const offerID = + keylet::nftoffer(offerer, env.seq(offerer)).key; + env(token::createOffer(offerer, nftID, amount), + token::owner(owner), + terCode ? ter(*terCode) + : ter(static_cast(tesSUCCESS))); + env.close(); + return offerID; + }; + + auto createSellOffer = + [&env]( + Account const& offerer, + uint256 const& nftID, + STAmount const& amount, + std::optional const terCode = {}) { + uint256 const offerID = + keylet::nftoffer(offerer, env.seq(offerer)).key; + env(token::createOffer(offerer, nftID, amount), + txflags(tfSellNFToken), + terCode ? ter(*terCode) + : ter(static_cast(tesSUCCESS))); + env.close(); + return offerID; + }; + + { + // Buyer attempts to send 100% of their balance of an IOU + // (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // Buyer attempts to send 100% of their balance of an IOU + // (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU, but such that the addition of the transfer + // fee would be greater than the buyer's balance (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXAU(995)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(995)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-14.9)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-995)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(14.9)); + } + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU, but such that the addition of the transfer + // fee would be greater than the buyer's balance (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(995)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(995)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-14.9)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-995)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(14.9)); + } + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is still less than their balance + // (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXAU(900)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(900)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-900)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is still less than their balance + // (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(900)); + env(token::acceptBuyOffer(minter, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(900)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-900)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is equal than their balance + // (sellside) + reinitializeTrustLineBalances(); + + // pay them an additional XAU 20 to cover transfer rate + env(pay(gw, buyer, gwXAU(20))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(1000)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is equal than their balance + // (buyside) + reinitializeTrustLineBalances(); + + // pay them an additional XAU 20 to cover transfer rate + env(pay(gw, buyer, gwXAU(20))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(1000)); + env(token::acceptBuyOffer(minter, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Gateway attempts to buy NFT with their own IOU - no + // transfer fee is calculated here (sellside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecINSUFFICIENT_FUNDS); + env(token::acceptSellOffer(gw, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + } + else + expectInitialState(); + } + { + // Gateway attempts to buy NFT with their own IOU - no + // transfer fee is calculated here (buyside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const offerTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecUNFUNDED_OFFER); + auto const offerID = + createBuyOffer(gw, minter, nftID, gwXAU(1000), {offerTER}); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecOBJECT_NOT_FOUND); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + } + else + expectInitialState(); + } + { + // Gateway attempts to buy NFT with their own IOU for more + // than minter trusts (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(5000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecINSUFFICIENT_FUNDS); + env(token::acceptSellOffer(gw, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(5000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-5000)); + } + else + expectInitialState(); + } + { + // Gateway attempts to buy NFT with their own IOU for more + // than minter trusts (buyside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const offerTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecUNFUNDED_OFFER); + auto const offerID = + createBuyOffer(gw, minter, nftID, gwXAU(5000), {offerTER}); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecOBJECT_NOT_FOUND); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(5000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-5000)); + } + else + expectInitialState(); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that would be greater than a balance if there were a + // transfer fee calculated in this transaction. (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(gw); + auto const offerID = createSellOffer(gw, nftID, gwXAU(1000)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that would be greater than a balance if there were a + // transfer fee calculated in this transaction. (buyside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(gw); + auto const offerID = + createBuyOffer(buyer, gw, nftID, gwXAU(1000)); + env(token::acceptBuyOffer(gw, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that is greater than a balance before transfer fees. + // (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(gw); + auto const offerID = createSellOffer(gw, nftID, gwXAU(2000)); + env(token::acceptSellOffer(buyer, offerID), + ter(static_cast(tecINSUFFICIENT_FUNDS))); + env.close(); + expectInitialState(); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that is greater than a balance before transfer fees. + // (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(gw); + auto const offerID = + createBuyOffer(buyer, gw, nftID, gwXAU(2000)); + env(token::acceptBuyOffer(gw, offerID), + ter(static_cast(tecINSUFFICIENT_FUNDS))); + env.close(); + expectInitialState(); + } + { + // Minter attempts to sell the token for XPB 10, which they + // have no trust line for and buyer has none of (sellside). + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXPB(10)); + env(token::acceptSellOffer(buyer, offerID), + ter(static_cast(tecINSUFFICIENT_FUNDS))); + env.close(); + expectInitialState(); + } + { + // Minter attempts to sell the token for XPB 10, which they + // have no trust line for and buyer has none of (buyside). + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createBuyOffer( + buyer, + minter, + nftID, + gwXPB(10), + {static_cast(tecUNFUNDED_OFFER)}); + env(token::acceptBuyOffer(minter, offerID), + ter(static_cast(tecOBJECT_NOT_FOUND))); + env.close(); + expectInitialState(); + } + { + // Minter attempts to sell the token for XPB 10 and the buyer + // has it but the minter has no trust line. Trust line is + // created as a result of the tx (sellside). + reinitializeTrustLineBalances(); + env(pay(gw, buyer, gwXPB(100))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXPB(10)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXPB) == gwXPB(10)); + BEAST_EXPECT(env.balance(buyer, gwXPB) == gwXPB(89.8)); + BEAST_EXPECT(env.balance(gw, minter["XPB"]) == gwXPB(-10)); + BEAST_EXPECT(env.balance(gw, buyer["XPB"]) == gwXPB(-89.8)); + } + { + // Minter attempts to sell the token for XPB 10 and the buyer + // has it but the minter has no trust line. Trust line is + // created as a result of the tx (buyside). + reinitializeTrustLineBalances(); + env(pay(gw, buyer, gwXPB(100))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXPB(10)); + env(token::acceptBuyOffer(minter, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXPB) == gwXPB(10)); + BEAST_EXPECT(env.balance(buyer, gwXPB) == gwXPB(89.8)); + BEAST_EXPECT(env.balance(gw, minter["XPB"]) == gwXPB(-10)); + BEAST_EXPECT(env.balance(gw, buyer["XPB"]) == gwXPB(-89.8)); + } + { + // There is a transfer fee on the NFT and buyer has exact + // amount (sellside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createSellOffer(secondarySeller, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(30)); + BEAST_EXPECT( + env.balance(secondarySeller, gwXAU) == gwXAU(970)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-30)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-970)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // There is a transfer fee on the NFT and buyer has exact + // amount (buyside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createBuyOffer(buyer, secondarySeller, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptBuyOffer(secondarySeller, offerID), + ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(30)); + BEAST_EXPECT( + env.balance(secondarySeller, gwXAU) == gwXAU(970)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-30)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-970)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // There is a transfer fee on the NFT and buyer has enough + // (sellside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createSellOffer(secondarySeller, nftID, gwXAU(900)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(27)); + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(873)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-27)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-873)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // There is a transfer fee on the NFT and buyer has enough + // (buyside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createBuyOffer(buyer, secondarySeller, nftID, gwXAU(900)); + env(token::acceptBuyOffer(secondarySeller, offerID)); + env.close(); + + // receives 3% of 900 - 27 + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(27)); + // receives 97% of 900 - 873 + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(873)); + // pays 900 plus 2% transfer fee on XAU - 918 + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-27)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-873)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // There is a broker fee on the NFT. XAU transfer fee is only + // calculated from the buyer's output, not deducted from + // broker fee. + // + // For a payment of 500 with a 2% IOU transfee fee and 100 + // broker fee: + // + // A) Total sale amount + IOU transfer fee is paid by buyer + // (Buyer pays (1.02 * 500) = 510) + // B) GW receives the additional IOU transfer fee + // (GW receives 10 from buyer calculated above) + // C) Broker receives broker fee (no IOU transfer fee) + // (Broker receives 100 from buyer) + // D) Seller receives balance (no IOU transfer fee) + // (Seller receives (510 - 10 - 100) = 400) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const sellOffer = + createSellOffer(minter, nftID, gwXAU(300)); + auto const buyOffer = + createBuyOffer(buyer, minter, nftID, gwXAU(500)); + env(token::brokerOffers(broker, buyOffer, sellOffer), + token::brokerFee(gwXAU(100))); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(400)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(490)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(5100)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-400)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-490)); + BEAST_EXPECT(env.balance(gw, broker["XAU"]) == gwXAU(-5100)); + } + { + // There is broker and transfer fee on the NFT + // + // For a payment of 500 with a 2% IOU transfer fee, 3% NFT + // transfer fee, and 100 broker fee: + // + // A) Total sale amount + IOU transfer fee is paid by buyer + // (Buyer pays (1.02 * 500) = 510) + // B) GW receives the additional IOU transfer fee + // (GW receives 10 from buyer calculated above) + // C) Broker receives broker fee (no IOU transfer fee) + // (Broker receives 100 from buyer) + // D) Minter receives transfer fee (no IOU transfer fee) + // (Minter receives 0.03 * (510 - 10 - 100) = 12) + // E) Seller receives balance (no IOU transfer fee) + // (Seller receives (510 - 10 - 100 - 12) = 388) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const sellOffer = + createSellOffer(secondarySeller, nftID, gwXAU(300)); + auto const buyOffer = + createBuyOffer(buyer, secondarySeller, nftID, gwXAU(500)); + env(token::brokerOffers(broker, buyOffer, sellOffer), + token::brokerFee(gwXAU(100))); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(12)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(490)); + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(388)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(5100)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-12)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-490)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-388)); + BEAST_EXPECT(env.balance(gw, broker["XAU"]) == gwXAU(-5100)); + } + } + } + void testWithFeats(FeatureBitset features) { @@ -5076,6 +5923,7 @@ class NFToken_test : public beast::unit_test::suite testNFTokenDeleteAccount(features); testNftXxxOffers(features); testFixNFTokenNegOffer(features); + testIOUWithTransferFee(features); } public: @@ -5086,6 +5934,8 @@ class NFToken_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; + // TODO too many tests are being run - ths fixNFTDir check should be + // pushed into the tests that use it testWithFeats(all - fixNFTDir); testWithFeats(all - disallowIncoming); testWithFeats(all); From df7eb5d69f128fee23f80e9a9b6a08889b76d758 Mon Sep 17 00:00:00 2001 From: Scott Schurr Date: Thu, 9 Feb 2023 21:15:22 -0800 Subject: [PATCH 16/38] Prevent brokered sale of NFToken to owner: (#4403) Fixes #4374 It was possible for a broker to combine a sell and a buy offer from an account that already owns an NFT. Such brokering extracts money from the NFT owner and provides no benefit in return. With this amendment, the code detects when a broker is returning an NFToken to its initial owner and prohibits the transaction. This forbids a broker from selling an NFToken to the account that already owns the token. This fixes a bug in the original implementation of XLS-20. Thanks to @nixer89 for suggesting this fix. --- src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp | 6 + src/test/app/NFToken_test.cpp | 119 +++++++++++++++++- 2 files changed, 120 insertions(+), 5 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp index c335f8d28fd..257bda5c051 100644 --- a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp @@ -107,6 +107,12 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) if ((*bo)[sfAmount].issue() != (*so)[sfAmount].issue()) return tecNFTOKEN_BUY_SELL_MISMATCH; + // The two offers may not form a loop. A broker may not sell the + // token to the current owner of the token. + if (ctx.view.rules().enabled(fixUnburnableNFToken) && + ((*bo)[sfOwner] == (*so)[sfOwner])) + return tecCANT_ACCEPT_OWN_NFTOKEN_OFFER; + // Ensure that the buyer is willing to pay at least as much as the // seller is requesting: if ((*so)[sfAmount] > (*bo)[sfAmount]) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 33d725e5a17..0c428e6fac9 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -4613,7 +4613,7 @@ class NFToken_test : public beast::unit_test::suite txflags(tfTransferable)); env.close(); - // At the momement issuer and minter cannot delete themselves. + // At the moment issuer and minter cannot delete themselves. // o issuer has an issued NFT in the ledger. // o minter owns an NFT. env(acctdelete(issuer, daria), fee(XRP(50)), ter(tecHAS_OBLIGATIONS)); @@ -5894,6 +5894,115 @@ class NFToken_test : public beast::unit_test::suite } } + void + testBrokeredSaleToSelf(FeatureBitset features) + { + // There was a bug that if an account had... + // + // 1. An NFToken, and + // 2. An offer on the ledger to buy that same token, and + // 3. Also an offer of the ledger to sell that same token, + // + // Then someone could broker the two offers. This would result in + // the NFToken being bought and returned to the original owner and + // the broker pocketing the profit. + // + // This unit test verifies that the fixUnburnableNFToken amendment + // fixes that bug. + testcase("Brokered sale to self"); + + using namespace test::jtx; + + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const broker{"broker"}; + + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, broker); + env.close(); + + // For this scenario to occur we need the following steps: + // + // 1. alice mints NFT. + // 2. bob creates a buy offer for it for 5 XRP. + // 3. alice decides to gift the NFT to bob for 0. + // creating a sell offer (hopefully using a destination too) + // 4. Bob accepts the sell offer, because it is better than + // paying 5 XRP. + // 5. At this point, bob has the NFT and still has their buy + // offer from when they did not have the NFT! This is because + // the order book is not cleared when an NFT changes hands. + // 6. Now that Bob owns the NFT, he cannot create new buy offers. + // However he still has one left over from when he did not own + // it. He can create new sell offers and does. + // 7. Now that bob has both a buy and a sell offer for the same NFT, + // a broker can sell the NFT that bob owns to bob and pocket the + // difference. + uint256 const nftId{token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + + // Bob creates a buy offer for 5 XRP. Alice creates a sell offer + // for 0 XRP. + uint256 const bobBuyOfferIndex = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(bob, nftId, XRP(5)), token::owner(alice)); + + uint256 const aliceSellOfferIndex = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId, XRP(0)), + token::destination(bob), + txflags(tfSellNFToken)); + env.close(); + + // bob accepts alice's offer but forgets to remove the old buy offer. + env(token::acceptSellOffer(bob, aliceSellOfferIndex)); + env.close(); + + // Note that bob still has a buy offer on the books. + BEAST_EXPECT(env.le(keylet::nftoffer(bobBuyOfferIndex))); + + // Bob creates a sell offer for the gift NFT from alice. + uint256 const bobSellOfferIndex = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(bob, nftId, XRP(4)), txflags(tfSellNFToken)); + env.close(); + + // bob now has a buy offer and a sell offer on the books. A broker + // spots this and swoops in to make a profit. + BEAST_EXPECT(nftCount(env, bob) == 1); + auto const bobsPriorBalance = env.balance(bob); + auto const brokersPriorBalance = env.balance(broker); + TER expectTer = features[fixUnburnableNFToken] + ? TER(tecCANT_ACCEPT_OWN_NFTOKEN_OFFER) + : TER(tesSUCCESS); + env(token::brokerOffers(broker, bobBuyOfferIndex, bobSellOfferIndex), + token::brokerFee(XRP(1)), + ter(expectTer)); + env.close(); + + if (expectTer == tesSUCCESS) + { + // bob should still have the NFT from alice, but be XRP(1) poorer. + // broker should be almost XRP(1) richer because they also paid a + // transaction fee. + BEAST_EXPECT(nftCount(env, bob) == 1); + BEAST_EXPECT(env.balance(bob) == bobsPriorBalance - XRP(1)); + BEAST_EXPECT( + env.balance(broker) == + brokersPriorBalance + XRP(1) - drops(10)); + } + else + { + // A tec result was returned, so no state should change other + // than the broker burning their transaction fee. + BEAST_EXPECT(nftCount(env, bob) == 1); + BEAST_EXPECT(env.balance(bob) == bobsPriorBalance); + BEAST_EXPECT( + env.balance(broker) == brokersPriorBalance - drops(10)); + } + } + void testWithFeats(FeatureBitset features) { @@ -5924,6 +6033,7 @@ class NFToken_test : public beast::unit_test::suite testNftXxxOffers(features); testFixNFTokenNegOffer(features); testIOUWithTransferFee(features); + testBrokeredSaleToSelf(features); } public: @@ -5934,10 +6044,9 @@ class NFToken_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - // TODO too many tests are being run - ths fixNFTDir check should be - // pushed into the tests that use it - testWithFeats(all - fixNFTDir); - testWithFeats(all - disallowIncoming); + testWithFeats(all - fixNFTDir - fixUnburnableNFToken); + testWithFeats(all - disallowIncoming - fixUnburnableNFToken); + testWithFeats(all - fixUnburnableNFToken); testWithFeats(all); } }; From 0c7781313c93774ccd49fb02b245aaddaf9a85a2 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Mon, 13 Feb 2023 13:02:24 -0500 Subject: [PATCH 17/38] Only account specified as destination can settle through brokerage: (#4399) Without this amendment, for NFTs using broker mode, if the sell offer contains a destination and that destination is the buyer account, anyone can broker the transaction. Also, if a buy offer contains a destination and that destination is the seller account, anyone can broker the transaction. This is not ideal and is misleading. Instead, with this amendment: If you set a destination, that destination needs to be the account settling the transaction. So, the broker must be the destination if they want to settle. If the buyer is the destination, then the buyer must accept the sell offer, as you cannot broker your own offers. If users want their offers open to the public, then they should not set a destination. On the other hand, if users want to limit who can settle the offers, then they would set a destination. Unit tests: 1. The broker cannot broker a destination offer to the buyer and the buyer must accept the sell offer. (0 transfer) 2. If the broker is the destination, the broker will take the difference. (broker mode) --- src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp | 36 ++++-- src/test/app/NFToken_test.cpp | 113 +++++++++++------- 2 files changed, 101 insertions(+), 48 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp index 257bda5c051..c420bfc6197 100644 --- a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp @@ -118,20 +118,40 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) if ((*so)[sfAmount] > (*bo)[sfAmount]) return tecINSUFFICIENT_PAYMENT; - // If the buyer specified a destination, that destination must be - // the seller or the broker. + // If the buyer specified a destination if (auto const dest = bo->at(~sfDestination)) { - if (*dest != so->at(sfOwner) && *dest != ctx.tx[sfAccount]) - return tecNFTOKEN_BUY_SELL_MISMATCH; + // fixUnburnableNFToken + if (ctx.view.rules().enabled(fixUnburnableNFToken)) + { + // the destination may only be the account brokering the offer + if (*dest != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + } + else + { + // the destination must be the seller or the broker. + if (*dest != so->at(sfOwner) && *dest != ctx.tx[sfAccount]) + return tecNFTOKEN_BUY_SELL_MISMATCH; + } } - // If the seller specified a destination, that destination must be - // the buyer or the broker. + // If the seller specified a destination if (auto const dest = so->at(~sfDestination)) { - if (*dest != bo->at(sfOwner) && *dest != ctx.tx[sfAccount]) - return tecNFTOKEN_BUY_SELL_MISMATCH; + // fixUnburnableNFToken + if (ctx.view.rules().enabled(fixUnburnableNFToken)) + { + // the destination may only be the account brokering the offer + if (*dest != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + } + else + { + // the destination must be the buyer or the broker. + if (*dest != bo->at(sfOwner) && *dest != ctx.tx[sfAccount]) + return tecNFTOKEN_BUY_SELL_MISMATCH; + } } // The broker can specify an amount that represents their cut; if they diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 0c428e6fac9..d581a6d0d90 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -2878,15 +2878,20 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(ownerCount(env, minter) == 2); BEAST_EXPECT(ownerCount(env, buyer) == 1); - // issuer cannot broker the offers, because they are not the - // Destination. - env(token::brokerOffers( - issuer, offerBuyerToMinter, offerMinterToBroker), - ter(tecNFTOKEN_BUY_SELL_MISMATCH)); - env.close(); - BEAST_EXPECT(ownerCount(env, issuer) == 0); - BEAST_EXPECT(ownerCount(env, minter) == 2); - BEAST_EXPECT(ownerCount(env, buyer) == 1); + { + // issuer cannot broker the offers, because they are not the + // Destination. + TER const expectTer = features[fixUnburnableNFToken] + ? tecNO_PERMISSION + : tecNFTOKEN_BUY_SELL_MISMATCH; + env(token::brokerOffers( + issuer, offerBuyerToMinter, offerMinterToBroker), + ter(expectTer)); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, minter) == 2); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + } // Since broker is the sell offer's destination, they can broker // the two offers. @@ -2923,29 +2928,52 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(ownerCount(env, minter) == 1); BEAST_EXPECT(ownerCount(env, buyer) == 2); - // Cannot broker offers when the sell destination is not the buyer. - env(token::brokerOffers( - broker, offerIssuerToBuyer, offerBuyerToMinter), - ter(tecNFTOKEN_BUY_SELL_MISMATCH)); - env.close(); - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); + { + // Cannot broker offers when the sell destination is not the + // buyer. + TER const expectTer = features[fixUnburnableNFToken] + ? tecNO_PERMISSION + : tecNFTOKEN_BUY_SELL_MISMATCH; + env(token::brokerOffers( + broker, offerIssuerToBuyer, offerBuyerToMinter), + ter(expectTer)); + env.close(); - // Broker is successful when destination is buyer. - env(token::brokerOffers( - broker, offerMinterToBuyer, offerBuyerToMinter)); - env.close(); - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 0); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); - // Clean out the unconsumed offer. - env(token::cancelOffer(issuer, {offerIssuerToBuyer})); - env.close(); - BEAST_EXPECT(ownerCount(env, issuer) == 0); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 0); + // amendment switch: When enabled the broker fails, when + // disabled the broker succeeds if the destination is the buyer. + TER const eexpectTer = features[fixUnburnableNFToken] + ? tecNO_PERMISSION + : TER(tesSUCCESS); + env(token::brokerOffers( + broker, offerMinterToBuyer, offerBuyerToMinter), + ter(eexpectTer)); + env.close(); + + if (features[fixUnburnableNFToken]) + // Buyer is successful with acceptOffer. + env(token::acceptBuyOffer(buyer, offerMinterToBuyer)); + env.close(); + + // Clean out the unconsumed offer. + env(token::cancelOffer(buyer, {offerBuyerToMinter})); + env.close(); + + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 0); + + // Clean out the unconsumed offer. + env(token::cancelOffer(issuer, {offerIssuerToBuyer})); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 0); + return; + } } // Show that if a buy and a sell offer both have the same destination, @@ -2963,15 +2991,20 @@ class NFToken_test : public beast::unit_test::suite token::owner(minter), token::destination(broker)); - // Cannot broker offers when the sell destination is not the buyer - // or the broker. - env(token::brokerOffers( - issuer, offerBuyerToBroker, offerMinterToBroker), - ter(tecNFTOKEN_BUY_SELL_MISMATCH)); - env.close(); - BEAST_EXPECT(ownerCount(env, issuer) == 0); - BEAST_EXPECT(ownerCount(env, minter) == 2); - BEAST_EXPECT(ownerCount(env, buyer) == 1); + { + // Cannot broker offers when the sell destination is not the + // buyer or the broker. + TER const expectTer = features[fixUnburnableNFToken] + ? tecNO_PERMISSION + : tecNFTOKEN_BUY_SELL_MISMATCH; + env(token::brokerOffers( + issuer, offerBuyerToBroker, offerMinterToBroker), + ter(expectTer)); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 0); + BEAST_EXPECT(ownerCount(env, minter) == 2); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + } // Broker is successful if they are the destination of both offers. env(token::brokerOffers( @@ -6053,4 +6086,4 @@ class NFToken_test : public beast::unit_test::suite BEAST_DEFINE_TESTSUITE_PRIO(NFToken, tx, ripple, 2); -} // namespace ripple +} // namespace ripple \ No newline at end of file From 05602af58faaf385dbb676d978c211620882fa4a Mon Sep 17 00:00:00 2001 From: ledhed2222 Date: Mon, 13 Feb 2023 15:30:01 -0500 Subject: [PATCH 18/38] Rename to fixNonFungibleTokensV1_2 and some cosmetic changes (#4419) --- src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp | 36 ++++---- src/ripple/app/tx/impl/NFTokenBurn.cpp | 4 +- src/ripple/app/tx/impl/NFTokenCreateOffer.cpp | 2 +- src/ripple/protocol/Feature.h | 2 +- src/ripple/protocol/impl/Feature.cpp | 2 +- src/test/app/NFTokenBurn_test.cpp | 24 +++--- src/test/app/NFToken_test.cpp | 84 ++++++++++--------- 7 files changed, 74 insertions(+), 80 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp index c420bfc6197..61aa7e0629a 100644 --- a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp @@ -109,7 +109,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // The two offers may not form a loop. A broker may not sell the // token to the current owner of the token. - if (ctx.view.rules().enabled(fixUnburnableNFToken) && + if (ctx.view.rules().enabled(fixNonFungibleTokensV1_2) && ((*bo)[sfOwner] == (*so)[sfOwner])) return tecCANT_ACCEPT_OWN_NFTOKEN_OFFER; @@ -121,37 +121,29 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // If the buyer specified a destination if (auto const dest = bo->at(~sfDestination)) { - // fixUnburnableNFToken - if (ctx.view.rules().enabled(fixUnburnableNFToken)) + // Before this fix the destination could be either the seller or + // a broker. After, it must be whoever is submitting the tx. + if (ctx.view.rules().enabled(fixNonFungibleTokensV1_2)) { - // the destination may only be the account brokering the offer if (*dest != ctx.tx[sfAccount]) return tecNO_PERMISSION; } - else - { - // the destination must be the seller or the broker. - if (*dest != so->at(sfOwner) && *dest != ctx.tx[sfAccount]) - return tecNFTOKEN_BUY_SELL_MISMATCH; - } + else if (*dest != so->at(sfOwner) && *dest != ctx.tx[sfAccount]) + return tecNFTOKEN_BUY_SELL_MISMATCH; } // If the seller specified a destination if (auto const dest = so->at(~sfDestination)) { - // fixUnburnableNFToken - if (ctx.view.rules().enabled(fixUnburnableNFToken)) + // Before this fix the destination could be either the seller or + // a broker. After, it must be whoever is submitting the tx. + if (ctx.view.rules().enabled(fixNonFungibleTokensV1_2)) { - // the destination may only be the account brokering the offer if (*dest != ctx.tx[sfAccount]) return tecNO_PERMISSION; } - else - { - // the destination must be the buyer or the broker. - if (*dest != bo->at(sfOwner) && *dest != ctx.tx[sfAccount]) - return tecNFTOKEN_BUY_SELL_MISMATCH; - } + else if (*dest != bo->at(sfOwner) && *dest != ctx.tx[sfAccount]) + return tecNFTOKEN_BUY_SELL_MISMATCH; } // The broker can specify an amount that represents their cut; if they @@ -200,7 +192,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // After this amendment, we allow an IOU issuer to buy an NFT with their // own currency auto const needed = bo->at(sfAmount); - if (ctx.view.rules().enabled(fixUnburnableNFToken)) + if (ctx.view.rules().enabled(fixNonFungibleTokensV1_2)) { if (accountFunds( ctx.view, (*bo)[sfOwner], needed, fhZERO_IF_FROZEN, ctx.j) < @@ -243,7 +235,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // The account offering to buy must have funds: auto const needed = so->at(sfAmount); - if (!ctx.view.rules().enabled(fixUnburnableNFToken)) + if (!ctx.view.rules().enabled(fixNonFungibleTokensV1_2)) { if (accountHolds( ctx.view, @@ -298,7 +290,7 @@ NFTokenAcceptOffer::pay( // their own currency, we know that something went wrong. This was // originally found in the context of IOU transfer fees. Since there are // several payouts in this tx, just confirm that the end state is OK. - if (!view().rules().enabled(fixUnburnableNFToken)) + if (!view().rules().enabled(fixNonFungibleTokensV1_2)) return result; if (result != tesSUCCESS) return result; diff --git a/src/ripple/app/tx/impl/NFTokenBurn.cpp b/src/ripple/app/tx/impl/NFTokenBurn.cpp index e8693c7c6fb..99acfd61dca 100644 --- a/src/ripple/app/tx/impl/NFTokenBurn.cpp +++ b/src/ripple/app/tx/impl/NFTokenBurn.cpp @@ -77,7 +77,7 @@ NFTokenBurn::preclaim(PreclaimContext const& ctx) } } - if (!ctx.view.rules().enabled(fixUnburnableNFToken)) + if (!ctx.view.rules().enabled(fixNonFungibleTokensV1_2)) { // If there are too many offers, then burning the token would produce // too much metadata. Disallow burning a token with too many offers. @@ -109,7 +109,7 @@ NFTokenBurn::doApply() view().update(issuer); } - if (ctx_.view().rules().enabled(fixUnburnableNFToken)) + if (ctx_.view().rules().enabled(fixNonFungibleTokensV1_2)) { // Delete up to 500 offers in total. // Because the number of sell offers is likely to be less than diff --git a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp index ff8668e4488..6db31c69892 100644 --- a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp @@ -155,7 +155,7 @@ NFTokenCreateOffer::preclaim(PreclaimContext const& ctx) { // After this amendment, we allow an IOU issuer to make a buy offer // using their own currency. - if (ctx.view.rules().enabled(fixUnburnableNFToken)) + if (ctx.view.rules().enabled(fixNonFungibleTokensV1_2)) { if (accountFunds( ctx.view, diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 0bdfd224dda..d53d992d242 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -343,7 +343,7 @@ extern uint256 const featureImmediateOfferKilled; extern uint256 const featureDisallowIncoming; extern uint256 const featureXRPFees; extern uint256 const fixUniversalNumber; -extern uint256 const fixUnburnableNFToken; +extern uint256 const fixNonFungibleTokensV1_2; } // namespace ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index f021ea4674d..4fb79e4cc48 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -453,7 +453,7 @@ REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no) REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no); REGISTER_FEATURE(XRPFees, Supported::yes, DefaultVote::no); REGISTER_FIX (fixUniversalNumber, Supported::yes, DefaultVote::no); -REGISTER_FIX (fixUnburnableNFToken, Supported::yes, DefaultVote::no); +REGISTER_FIX (fixNonFungibleTokensV1_2, Supported::yes, DefaultVote::no); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 4896932acd2..096fd5ce1e8 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -524,8 +524,8 @@ class NFTokenBurn_test : public beast::unit_test::suite using namespace test::jtx; // Test what happens if a NFT is unburnable when there are - // more than 500 offers, before fixUnburnableNFToken goes live - if (!features[fixUnburnableNFToken]) + // more than 500 offers, before fixNonFungibleTokensV1_2 goes live + if (!features[fixNonFungibleTokensV1_2]) { Env env{*this, features}; @@ -620,10 +620,10 @@ class NFTokenBurn_test : public beast::unit_test::suite } // Test that up to 499 buy/sell offers will be removed when NFT is - // burned after fixUnburnableNFToken is enabled. This is to test that we - // can successfully remove all offers if the number of offers is less - // than 500. - if (features[fixUnburnableNFToken]) + // burned after fixNonFungibleTokensV1_2 is enabled. This is to test + // that we can successfully remove all offers if the number of offers is + // less than 500. + if (features[fixNonFungibleTokensV1_2]) { Env env{*this, features}; @@ -673,8 +673,8 @@ class NFTokenBurn_test : public beast::unit_test::suite } // Test that up to 500 buy offers are removed when NFT is burned - // after fixUnburnableNFToken is enabled - if (features[fixUnburnableNFToken]) + // after fixNonFungibleTokensV1_2 is enabled + if (features[fixNonFungibleTokensV1_2]) { Env env{*this, features}; @@ -718,8 +718,8 @@ class NFTokenBurn_test : public beast::unit_test::suite } // Test that up to 500 buy/sell offers are removed when NFT is burned - // after fixUnburnableNFToken is enabled - if (features[fixUnburnableNFToken]) + // after fixNonFungibleTokensV1_2 is enabled + if (features[fixNonFungibleTokensV1_2]) { Env env{*this, features}; @@ -786,8 +786,8 @@ class NFTokenBurn_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixUnburnableNFToken - fixNFTDir); - testWithFeats(all - fixUnburnableNFToken); + testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTDir); + testWithFeats(all - fixNonFungibleTokensV1_2); testWithFeats(all); } }; diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index d581a6d0d90..40202e07dce 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -2881,7 +2881,7 @@ class NFToken_test : public beast::unit_test::suite { // issuer cannot broker the offers, because they are not the // Destination. - TER const expectTer = features[fixUnburnableNFToken] + TER const expectTer = features[fixNonFungibleTokensV1_2] ? tecNO_PERMISSION : tecNFTOKEN_BUY_SELL_MISMATCH; env(token::brokerOffers( @@ -2931,7 +2931,7 @@ class NFToken_test : public beast::unit_test::suite { // Cannot broker offers when the sell destination is not the // buyer. - TER const expectTer = features[fixUnburnableNFToken] + TER const expectTer = features[fixNonFungibleTokensV1_2] ? tecNO_PERMISSION : tecNFTOKEN_BUY_SELL_MISMATCH; env(token::brokerOffers( @@ -2945,7 +2945,7 @@ class NFToken_test : public beast::unit_test::suite // amendment switch: When enabled the broker fails, when // disabled the broker succeeds if the destination is the buyer. - TER const eexpectTer = features[fixUnburnableNFToken] + TER const eexpectTer = features[fixNonFungibleTokensV1_2] ? tecNO_PERMISSION : TER(tesSUCCESS); env(token::brokerOffers( @@ -2953,7 +2953,7 @@ class NFToken_test : public beast::unit_test::suite ter(eexpectTer)); env.close(); - if (features[fixUnburnableNFToken]) + if (features[fixNonFungibleTokensV1_2]) // Buyer is successful with acceptOffer. env(token::acceptBuyOffer(buyer, offerMinterToBuyer)); env.close(); @@ -2994,7 +2994,7 @@ class NFToken_test : public beast::unit_test::suite { // Cannot broker offers when the sell destination is not the // buyer or the broker. - TER const expectTer = features[fixUnburnableNFToken] + TER const expectTer = features[fixNonFungibleTokensV1_2] ? tecNO_PERMISSION : tecNFTOKEN_BUY_SELL_MISMATCH; env(token::brokerOffers( @@ -3861,7 +3861,8 @@ class NFToken_test : public beast::unit_test::suite using namespace test::jtx; for (auto const& tweakedFeatures : - {features - fixUnburnableNFToken, features | fixUnburnableNFToken}) + {features - fixNonFungibleTokensV1_2, + features | fixNonFungibleTokensV1_2}) { Env env{*this, tweakedFeatures}; @@ -4400,7 +4401,7 @@ class NFToken_test : public beast::unit_test::suite token::owner(minter)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) { env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), @@ -4946,12 +4947,12 @@ class NFToken_test : public beast::unit_test::suite IOU const gwXAU(gw["XAU"]); // Test both with and without fixNFTokenNegOffer and - // fixUnburnableNFToken. Need to turn off fixUnburnableNFToken as well - // because that amendment came later and addressed the acceptance - // side of this issue. + // fixNonFungibleTokensV1_2. Need to turn off fixNonFungibleTokensV1_2 + // as well because that amendment came later and addressed the + // acceptance side of this issue. for (auto const& tweakedFeatures : {features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1 - - fixUnburnableNFToken, + fixNonFungibleTokensV1_2, features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1, features | fixNFTokenNegOffer}) { @@ -5042,7 +5043,7 @@ class NFToken_test : public beast::unit_test::suite } { // 1. If fixNFTokenNegOffer is enabled get tecOBJECT_NOT_FOUND - // 2. If it is not enabled, but fixUnburnableNFToken is + // 2. If it is not enabled, but fixNonFungibleTokensV1_2 is // enabled, get tecOBJECT_NOT_FOUND. // 3. If neither are enabled, get tesSUCCESS. TER const offerAcceptTER = tweakedFeatures[fixNFTokenNegOffer] @@ -5184,7 +5185,8 @@ class NFToken_test : public beast::unit_test::suite testcase("Payments with IOU transfer fees"); for (auto const& tweakedFeatures : - {features - fixUnburnableNFToken, features | fixUnburnableNFToken}) + {features - fixNonFungibleTokensV1_2, + features | fixNonFungibleTokensV1_2}) { Env env{*this, tweakedFeatures}; @@ -5336,13 +5338,13 @@ class NFToken_test : public beast::unit_test::suite auto const nftID = mintNFT(minter); auto const offerID = createSellOffer(minter, nftID, gwXAU(1000)); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tecINSUFFICIENT_FUNDS) : static_cast(tesSUCCESS); env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) expectInitialState(); else { @@ -5360,13 +5362,13 @@ class NFToken_test : public beast::unit_test::suite auto const nftID = mintNFT(minter); auto const offerID = createBuyOffer(buyer, minter, nftID, gwXAU(1000)); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tecINSUFFICIENT_FUNDS) : static_cast(tesSUCCESS); env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) expectInitialState(); else { @@ -5384,13 +5386,13 @@ class NFToken_test : public beast::unit_test::suite reinitializeTrustLineBalances(); auto const nftID = mintNFT(minter); auto const offerID = createSellOffer(minter, nftID, gwXAU(995)); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tecINSUFFICIENT_FUNDS) : static_cast(tesSUCCESS); env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) expectInitialState(); else { @@ -5408,13 +5410,13 @@ class NFToken_test : public beast::unit_test::suite auto const nftID = mintNFT(minter); auto const offerID = createBuyOffer(buyer, minter, nftID, gwXAU(995)); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tecINSUFFICIENT_FUNDS) : static_cast(tesSUCCESS); env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) expectInitialState(); else { @@ -5509,13 +5511,13 @@ class NFToken_test : public beast::unit_test::suite auto const nftID = mintNFT(minter); auto const offerID = createSellOffer(minter, nftID, gwXAU(1000)); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tesSUCCESS) : static_cast(tecINSUFFICIENT_FUNDS); env(token::acceptSellOffer(gw, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) { BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); BEAST_EXPECT( @@ -5530,18 +5532,18 @@ class NFToken_test : public beast::unit_test::suite reinitializeTrustLineBalances(); auto const nftID = mintNFT(minter); - auto const offerTER = tweakedFeatures[fixUnburnableNFToken] + auto const offerTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tesSUCCESS) : static_cast(tecUNFUNDED_OFFER); auto const offerID = createBuyOffer(gw, minter, nftID, gwXAU(1000), {offerTER}); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tesSUCCESS) : static_cast(tecOBJECT_NOT_FOUND); env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) { BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); BEAST_EXPECT( @@ -5557,13 +5559,13 @@ class NFToken_test : public beast::unit_test::suite auto const nftID = mintNFT(minter); auto const offerID = createSellOffer(minter, nftID, gwXAU(5000)); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tesSUCCESS) : static_cast(tecINSUFFICIENT_FUNDS); env(token::acceptSellOffer(gw, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) { BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(5000)); BEAST_EXPECT( @@ -5578,18 +5580,18 @@ class NFToken_test : public beast::unit_test::suite reinitializeTrustLineBalances(); auto const nftID = mintNFT(minter); - auto const offerTER = tweakedFeatures[fixUnburnableNFToken] + auto const offerTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tesSUCCESS) : static_cast(tecUNFUNDED_OFFER); auto const offerID = createBuyOffer(gw, minter, nftID, gwXAU(5000), {offerTER}); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tesSUCCESS) : static_cast(tecOBJECT_NOT_FOUND); env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) { BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(5000)); BEAST_EXPECT( @@ -5731,13 +5733,13 @@ class NFToken_test : public beast::unit_test::suite // now we can do a secondary sale auto const offerID = createSellOffer(secondarySeller, nftID, gwXAU(1000)); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tecINSUFFICIENT_FUNDS) : static_cast(tesSUCCESS); env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) expectInitialState(); else { @@ -5767,14 +5769,14 @@ class NFToken_test : public beast::unit_test::suite // now we can do a secondary sale auto const offerID = createBuyOffer(buyer, secondarySeller, nftID, gwXAU(1000)); - auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + auto const sellTER = tweakedFeatures[fixNonFungibleTokensV1_2] ? static_cast(tecINSUFFICIENT_FUNDS) : static_cast(tesSUCCESS); env(token::acceptBuyOffer(secondarySeller, offerID), ter(sellTER)); env.close(); - if (tweakedFeatures[fixUnburnableNFToken]) + if (tweakedFeatures[fixNonFungibleTokensV1_2]) expectInitialState(); else { @@ -5940,7 +5942,7 @@ class NFToken_test : public beast::unit_test::suite // the NFToken being bought and returned to the original owner and // the broker pocketing the profit. // - // This unit test verifies that the fixUnburnableNFToken amendment + // This unit test verifies that the fixNonFungibleTokensV1_2 amendment // fixes that bug. testcase("Brokered sale to self"); @@ -6006,7 +6008,7 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(nftCount(env, bob) == 1); auto const bobsPriorBalance = env.balance(bob); auto const brokersPriorBalance = env.balance(broker); - TER expectTer = features[fixUnburnableNFToken] + TER expectTer = features[fixNonFungibleTokensV1_2] ? TER(tecCANT_ACCEPT_OWN_NFTOKEN_OFFER) : TER(tesSUCCESS); env(token::brokerOffers(broker, bobBuyOfferIndex, bobSellOfferIndex), @@ -6077,13 +6079,13 @@ class NFToken_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixNFTDir - fixUnburnableNFToken); - testWithFeats(all - disallowIncoming - fixUnburnableNFToken); - testWithFeats(all - fixUnburnableNFToken); + testWithFeats(all - fixNFTDir - fixNonFungibleTokensV1_2); + testWithFeats(all - disallowIncoming - fixNonFungibleTokensV1_2); + testWithFeats(all - fixNonFungibleTokensV1_2); testWithFeats(all); } }; BEAST_DEFINE_TESTSUITE_PRIO(NFToken, tx, ripple, 2); -} // namespace ripple \ No newline at end of file +} // namespace ripple From d46454315c572c9c5cd2f842f879d86aa3a24f37 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Mon, 13 Feb 2023 17:01:42 -0500 Subject: [PATCH 19/38] Add remint tests and flag change --- src/ripple/app/tx/impl/DeleteAccount.cpp | 4 +- src/ripple/app/tx/impl/NFTokenMint.cpp | 2 +- src/ripple/protocol/Feature.h | 7 +- src/ripple/protocol/impl/Feature.cpp | 1 + src/test/app/NFTokenBurn_test.cpp | 4 +- src/test/app/NFTokenDir_test.cpp | 28 +- src/test/app/NFToken_test.cpp | 614 ++++++++++++++++++++++- src/test/jtx/impl/token.cpp | 4 +- 8 files changed, 630 insertions(+), 34 deletions(-) diff --git a/src/ripple/app/tx/impl/DeleteAccount.cpp b/src/ripple/app/tx/impl/DeleteAccount.cpp index 85be8ebe04a..62cc9e1fbbf 100644 --- a/src/ripple/app/tx/impl/DeleteAccount.cpp +++ b/src/ripple/app/tx/impl/DeleteAccount.cpp @@ -214,7 +214,7 @@ DeleteAccount::preclaim(PreclaimContext const& ctx) if ((*sleAccount)[sfSequence] + seqDelta > ctx.view.seq()) return tecTOO_SOON; - // When fixUnburnableNFToken is enabled, we don't allow an account to be + // When fixNFTokenRemint is enabled, we don't allow an account to be // deleted if is within 256 of the // current ledger. This is to prevent having duplicate NFTokenIDs after // account re-creation. @@ -225,7 +225,7 @@ DeleteAccount::preclaim(PreclaimContext const& ctx) // their account and mints a NFToken, it is possible that the // NFTokenSequence of this NFToken is the same as the one that the // authorized minter minted in a previous ledger. - if (ctx.view.rules().enabled(fixUnburnableNFToken) && + if (ctx.view.rules().enabled(fixNFTokenRemint) && ((*sleAccount)[~sfFirstNFTokenSequence].value_or(0) + (*sleAccount)[~sfMintedNFTokens].value_or(0) + seqDelta > ctx.view.seq())) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index 0c6ff74897f..be478576980 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -161,7 +161,7 @@ NFTokenMint::doApply() return Unexpected(tecNO_ISSUER); std::uint32_t tokenSeq; - if (ctx_.view().rules().enabled(fixUnburnableNFToken)) + if (ctx_.view().rules().enabled(fixNFTokenRemint)) { auto const accSeq = (*root)[sfSequence]; diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index d54b99597d7..62dc327d98d 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,11 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -<<<<<<< HEAD -static constexpr std::size_t numFeatures = 55; -======= -static constexpr std::size_t numFeatures = 57; ->>>>>>> upstream/feature/nft-fixes +static constexpr std::size_t numFeatures = 58; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -348,6 +344,7 @@ extern uint256 const featureDisallowIncoming; extern uint256 const featureXRPFees; extern uint256 const fixUniversalNumber; extern uint256 const fixNonFungibleTokensV1_2; +extern uint256 const fixNFTokenRemint; } // namespace ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 4fb79e4cc48..d15c3fc60ff 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -454,6 +454,7 @@ REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no) REGISTER_FEATURE(XRPFees, Supported::yes, DefaultVote::no); REGISTER_FIX (fixUniversalNumber, Supported::yes, DefaultVote::no); REGISTER_FIX (fixNonFungibleTokensV1_2, Supported::yes, DefaultVote::no); +REGISTER_FIX (fixNFTokenRemint, Supported::yes, DefaultVote::no); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 6c49cd20167..76b2934982e 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -382,9 +382,9 @@ class NFTokenBurn_test : public beast::unit_test::suite std::uint32_t taxon) -> std::uint32_t { std::uint32_t tokenSeq; - // If fixUnburnableNFToken amendment is on, we must + // If fixNFTokenRemint amendment is on, we must // generate the NFT sequence using new construct - if (env.current()->rules().enabled(fixUnburnableNFToken)) + if (env.current()->rules().enabled(fixNFTokenRemint)) tokenSeq = { env.le(acct) ->at(~sfFirstNFTokenSequence) diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index d2b2a840116..e008a29d1c7 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -191,12 +191,12 @@ class NFTokenDir_test : public beast::unit_test::suite Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - // Should not advance ledger if fixUnburnableNFToken is + // Should not advance ledger if fixNFTokenRemint is // enabled. Because otherwise, accounts are initialized at // different ledgers, and will have different account // sequences, which then causes the sequence number of // NFTokenIDs to be different - if (!features[fixUnburnableNFToken]) + if (!features[fixNFTokenRemint]) env.close(); } env.close(); @@ -417,12 +417,12 @@ class NFTokenDir_test : public beast::unit_test::suite Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - // Should not advance ledger if fixUnburnableNFToken is + // Should not advance ledger if fixNFTokenRemint is // enabled. Because otherwise, accounts are initialized at // different ledgers, and will have different account // sequences, which then causes the sequence number of // NFTokenIDs to be different - if (!features[fixUnburnableNFToken]) + if (!features[fixNFTokenRemint]) env.close(); } env.close(); @@ -664,9 +664,9 @@ class NFTokenDir_test : public beast::unit_test::suite std::vector accounts; accounts.reserve(seeds.size()); - // If fixUnburnableNFToken is not enabled, accounts can be created in + // If fixNFTokenRemint is not enabled, accounts can be created in // different ledgers. - // If fixUnburnableNFToken is enabled, all accounts must be created in + // If fixNFTokenRemint is enabled, all accounts must be created in // the same ledger in order to initialize all accounts with the same // account sequence. for (std::string_view const& seed : seeds) @@ -675,8 +675,8 @@ class NFTokenDir_test : public beast::unit_test::suite accounts.emplace_back(Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - // Only advance the ledger if fixUnburnableNFToken is disabled - if (!features[fixUnburnableNFToken]) + // Only advance the ledger if fixNFTokenRemint is disabled + if (!features[fixNFTokenRemint]) env.close(); } env.close(); @@ -849,9 +849,9 @@ class NFTokenDir_test : public beast::unit_test::suite std::vector accounts; accounts.reserve(seeds.size()); - // If fixUnburnableNFToken is not enabled, accounts can be created in + // If fixNFTokenRemint is not enabled, accounts can be created in // different ledgers. - // If fixUnburnableNFToken is enabled, accounts must be created in the + // If fixNFTokenRemint is enabled, accounts must be created in the // same ledger in order to initialize all accounts with the same // account sequence. for (std::string_view const& seed : seeds) @@ -860,8 +860,8 @@ class NFTokenDir_test : public beast::unit_test::suite accounts.emplace_back(Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - // Only advance the ledger if fixUnburnableNFToken is disabled - if (!features[fixUnburnableNFToken]) + // Only advance the ledger if fixNFTokenRemint is disabled + if (!features[fixNFTokenRemint]) env.close(); } env.close(); @@ -1114,8 +1114,8 @@ class NFTokenDir_test : public beast::unit_test::suite FeatureBitset const fixNFTDir{ fixNFTokenDirV1, featureNonFungibleTokensV1_1}; - testWithFeats(all - fixNFTDir - fixUnburnableNFToken); - testWithFeats(all - fixUnburnableNFToken); + testWithFeats(all - fixNFTDir - fixNFTokenRemint); + testWithFeats(all - fixNFTokenRemint); testWithFeats(all); } }; diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index d585dbc576e..3a61982d7d1 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -314,8 +314,8 @@ class NFToken_test : public beast::unit_test::suite while (seq < 33) { - if (features[fixUnburnableNFToken]) - // If fixUnburnableNFToken is enabled, we must add + if (features[fixNFTokenRemint]) + // If fixNFTokenRemint is enabled, we must add // FirstNFTokenSequence to offset the starting NFT sequence // number env(token::burn( @@ -442,8 +442,8 @@ class NFToken_test : public beast::unit_test::suite // minter burns the NFTs she created. while (nftSeq < 65) { - if (features[fixUnburnableNFToken]) - // If fixUnburnableNFToken is enabled, we must add + if (features[fixNFTokenRemint]) + // If fixNFTokenRemint is enabled, we must add // FirstNFTokenSequence to offset the starting NFT sequence // number env(token::burn( @@ -462,8 +462,8 @@ class NFToken_test : public beast::unit_test::suite // minter has one more NFT to burn. Should take her owner count to // 0. - if (features[fixUnburnableNFToken]) - // If fixUnburnableNFToken is enabled, we must add + if (features[fixNFTokenRemint]) + // If fixNFTokenRemint is enabled, we must add // FirstNFTokenSequence to offset the starting NFT sequence number env(token::burn( minter, @@ -527,9 +527,9 @@ class NFToken_test : public beast::unit_test::suite if (replacement->getFieldU32(sfMintedNFTokens) != 1) return false; // Unexpected test conditions. - if (env.current()->rules().enabled(fixUnburnableNFToken)) + if (env.current()->rules().enabled(fixNFTokenRemint)) { - // If fixUnburnableNFToken is enabled, sequence number is + // If fixNFTokenRemint is enabled, sequence number is // generated by sfFirstNFTokenSequence + sfMintedNFTokens. // We can replace the two fields with any numbers as long as // they add up to the largest valid number. In our case, @@ -5448,7 +5448,18 @@ class NFToken_test : public beast::unit_test::suite env.close(); if (tweakedFeatures[fixNonFungibleTokensV1_2]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(995)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-14.9)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-995)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(14.9)); + } + } + { // Buyer attempts to send an amount less than 100% of their + // balance of an IOU, but such that the addition of the transfer // fee would be greater than the buyer's balance (buyside) reinitializeTrustLineBalances(); auto const nftID = mintNFT(minter); @@ -6082,6 +6093,592 @@ class NFToken_test : public beast::unit_test::suite } } + void + testFixNFTokenRemint(FeatureBitset features) + { + using namespace test::jtx; + + testcase("fixNFTokenRemint"); + + // Returns the current ledger sequence + auto openLedgerSeq = [](Env& env) { return env.current()->seq(); }; + + // Close the ledger until the ledger sequence is large enough to close + // the account (no longer within ) + // This is enforced by the featureDeletableAccounts amendment + auto incLgrSeqForAccDel = [&](Env& env, Account const& acc) { + int const delta = [&]() -> int { + if (env.seq(acc) + 255 > openLedgerSeq(env)) + return env.seq(acc) - openLedgerSeq(env) + 255; + return 0; + }(); + BEAST_EXPECT(delta >= 0); + for (int i = 0; i < delta; ++i) + env.close(); + BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255); + }; + + // Close the ledger until the ledger sequence is no longer + // within . + // This is enforced by the fixNFTokenRemint amendment. + auto incLgrSeqForNFTokenAccDel = [&](Env& env, Account const& acc) { + int delta = 0; + auto const deletableLgrSeq = + (*env.le(acc))[~sfFirstNFTokenSequence].value_or(0) + + (*env.le(acc))[sfMintedNFTokens] + 255; + + if (deletableLgrSeq > openLedgerSeq(env)) + delta = deletableLgrSeq - openLedgerSeq(env); + + BEAST_EXPECT(delta >= 0); + for (int i = 0; i < delta; ++i) + env.close(); + BEAST_EXPECT(openLedgerSeq(env) == deletableLgrSeq); + }; + + // We check if NFTokenIDs can be duplicated by + // re-creation of an account + { + Env env{*this, features}; + Account const alice("alice"); + Account const becky("becky"); + + env.fund(XRP(10000), alice, becky); + env.close(); + + // alice mint and burn a NFT + uint256 const prevNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + env(token::burn(alice, prevNftokenID)); + env.close(); + + // alice has minted 1 NFToken + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 1); + + // Close enough ledgers to delete alice's account + incLgrSeqForAccDel(env, alice); + + // alice's account is deleted + Keylet const aliceAcctKey{keylet::account(alice.id())}; + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + if (features[fixNFTokenRemint]) + // Check that two NFTs don't have the same ID + BEAST_EXPECT(remintNftokenID != prevNftokenID); + else + // Check that two NFTs have the same ID + BEAST_EXPECT(remintNftokenID == prevNftokenID); + } + + // If fixNFTokenRemint is not enabled, we test if the issuer account + // can be deleted after an authorized minter mints and burns a batch of + // NFTokens. + // After the issuer's account is re-created and mints a NFT, it should + // have the same NFTokenID as the one minted before. + if (!features[fixNFTokenRemint]) + { + Env env{*this, features}; + Account const alice("alice"); + Account const becky("becky"); + Account const minter{"minter"}; + + env.fund(XRP(10000), alice, becky, minter); + env.close(); + + // alice sets minter as her authorized minter + env(token::setMinter(alice, minter)); + env.close(); + + // minter mints 500 NFTs for alice + std::vector nftIDs; + nftIDs.reserve(500); + for (int i = 0; i < 500; i++) + { + uint256 const nftokenID = token::getNextID(env, alice, 0u); + nftIDs.push_back(nftokenID); + env(token::mint(minter), token::issuer(alice)); + } + env.close(); + + // minter burns 500 NFTs + for (auto const nftokenID : nftIDs) + { + env(token::burn(minter, nftokenID)); + } + env.close(); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + auto const acctDelFee1{drops(env.current()->fees().increment)}; + + // alice's account can be successfully deleted. + env(acctdelete(alice, becky), fee(acctDelFee1)); + env.close(); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + // The new NFT minted has the same ID as one of the NFTs + // authorized minter minted for alice + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) != + nftIDs.end()); + } + + // If fixNFTokenRemint is not enabled, + // when an account mints and burns a batch of NFTokens using tickets, + // the account should be able to be deleted. + // After the issuer's account is re-created and mints a NFT, it should + // have the same NFTokenID as the one minted before. + if (!features[fixNFTokenRemint]) + { + Env env{*this, features}; + + Account const alice{"alice"}; + Account const becky{"becky"}; + env.fund(XRP(10000), alice, becky); + env.close(); + + // alice grab enough tickets for all of the following + // transactions. Note that once the tickets are acquired alice's + // account sequence number should not advance. + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 100)); + env.close(); + + BEAST_EXPECT(ticketCount(env, alice) == 100); + BEAST_EXPECT(ownerCount(env, alice) == 100); + + // alice mints 50 NFTs using tickets + std::vector nftIDs; + nftIDs.reserve(50); + for (int i = 0; i < 50; i++) + { + nftIDs.push_back(token::getNextID(env, alice, 0u)); + env(token::mint(alice, 0u), ticket::use(aliceTicketSeq++)); + env.close(); + } + + // alice burns 50 NFTs using tickets + for (auto const nftokenID : nftIDs) + { + env(token::burn(alice, nftokenID), + ticket::use(aliceTicketSeq++)); + } + env.close(); + + BEAST_EXPECT(ticketCount(env, alice) == 0); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // alice tries to delete her account, and is successful. + auto const acctDelFee1{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee1)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + // The new NFT minted will have the same ID + // as one of NFTs minted using tickets + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) != + nftIDs.end()); + } + + // If fixNFTokenRemint is enabled, + // when an authorized minter mints and burns a batch of NFTokens, + // issuer's account needs to wait a longer time before it can deleted. + // After the issuer's account is re-created and mints a NFT, it should + // not have the same NFTokenID as the ones authorized minter minted. + if (features[fixNFTokenRemint]) + { + Env env{*this, features}; + Account const alice("alice"); + Account const becky("becky"); + Account const minter{"minter"}; + + env.fund(XRP(10000), alice, becky, minter); + env.close(); + + // alice sets minter as her authorized minter + env(token::setMinter(alice, minter)); + env.close(); + + // minter mints 500 NFTs for alice + std::vector nftIDs; + nftIDs.reserve(500); + for (int i = 0; i < 500; i++) + { + uint256 const nftokenID = token::getNextID(env, alice, 0u); + nftIDs.push_back(nftokenID); + env(token::mint(minter), token::issuer(alice)); + } + env.close(); + + // minter burns 500 NFTs + for (auto const nftokenID : nftIDs) + { + env(token::burn(minter, nftokenID)); + } + env.close(); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // alice tries to delete her account, but is unsuccessful. + // Due to authorized minting, alice's account sequence does not + // advance while minter mints NFTokens for her. + // The new account deletion retriction enabled by this amendment will enforce + // alice to wait for more ledgers to close before she can delete her + // account, to prevent duplicate NFTokenIDs + auto const acctDelFee1{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); + env.close(); + + // alice's account is still present + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // Close more ledgers until it is no longer within + // + // to be able to delete alice's account + incLgrSeqForNFTokenAccDel(env, alice); + + // alice's account is deleted + auto const acctDelFee2{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee2)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + // The new NFT minted will not have the same ID + // as one of NFTs authorized minter minted + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) == + nftIDs.end()); + } + + // If fixNFTokenRemint is enabled, + // when an account mints and burns a batch of NFTokens using tickets, + // the account needs to wait a longer time before it can deleted. + // After the issuer's account is re-created and mints a NFT, it should + // not have the same NFTokenID as the ones minted using tickets. + if (features[fixNFTokenRemint]) + { + Env env{*this, features}; + + Account const alice{"alice"}; + Account const becky{"becky"}; + env.fund(XRP(10000), alice, becky); + env.close(); + + // alice grab enough tickets for all of the following + // transactions. Note that once the tickets are acquired alice's + // account sequence number should not advance. + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 100)); + env.close(); + + BEAST_EXPECT(ticketCount(env, alice) == 100); + BEAST_EXPECT(ownerCount(env, alice) == 100); + + // alice mints 50 NFTs using tickets + std::vector nftIDs; + nftIDs.reserve(50); + for (int i = 0; i < 50; i++) + { + nftIDs.push_back(token::getNextID(env, alice, 0u)); + env(token::mint(alice, 0u), ticket::use(aliceTicketSeq++)); + env.close(); + } + + // alice burns 50 NFTs using tickets + for (auto const nftokenID : nftIDs) + { + env(token::burn(alice, nftokenID), + ticket::use(aliceTicketSeq++)); + } + env.close(); + + BEAST_EXPECT(ticketCount(env, alice) == 0); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // alice tries to delete her account, but is unsuccessful. + // Because alice used tickets to mint and burn NFTs, her account + // sequence did not change while while submitting these + // transactions. Hence, her is still greater than the current ledger sequence + auto const acctDelFee1{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); + env.close(); + + // Close more ledgers until it is no longer within + // + // to be able to delete alice's account + incLgrSeqForNFTokenAccDel(env, alice); + + // alice's account is deleted + auto const acctDelFee2{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee2)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + // The new NFT minted will not have the same ID + // as one of NFTs alice minted using tickets + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) == + nftIDs.end()); + } + + // If fixNFTokenRemint is enabled, + // when an authorized minter mints and burns a batch of NFTokens using + // tickets, issuer's account needs to wait a longer time before it can + // deleted. + // After the issuer's account is re-created and mints a NFT, it should + // not have the same NFTokenID as the ones authorized minter minted. + if (features[fixNFTokenRemint]) + { + Env env{*this, features}; + Account const alice("alice"); + Account const becky("becky"); + Account const minter{"minter"}; + + env.fund(XRP(10000), alice, becky, minter); + env.close(); + + // alice sets minter as her authorized minter + env(token::setMinter(alice, minter)); + env.close(); + + // minter creates 100 tickets + std::uint32_t minterTicketSeq{env.seq(minter) + 1}; + env(ticket::create(minter, 100)); + env.close(); + + BEAST_EXPECT(ticketCount(env, minter) == 100); + BEAST_EXPECT(ownerCount(env, minter) == 100); + + // minter mints 50 NFTs for alice using tickets + std::vector nftIDs; + nftIDs.reserve(50); + for (int i = 0; i < 50; i++) + { + uint256 const nftokenID = token::getNextID(env, alice, 0u); + nftIDs.push_back(nftokenID); + env(token::mint(minter), + token::issuer(alice), + ticket::use(minterTicketSeq++)); + } + env.close(); + + // minter burns 50 NFTs using tickets + for (auto const nftokenID : nftIDs) + { + env(token::burn(minter, nftokenID), + ticket::use(minterTicketSeq++)); + } + env.close(); + + BEAST_EXPECT(ticketCount(env, minter) == 0); + + // Increment ledger sequence to the number that is + // enforced by the featureDeletableAccounts amendment + incLgrSeqForAccDel(env, alice); + + // Verify that alice's account root is present. + Keylet const aliceAcctKey{keylet::account(alice.id())}; + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // alice tries to delete her account, but is unsuccessful. + // Due to authorized minting, alice's account sequence does not + // advance while minter mints NFTokens for her using tickets. + // The new account deletion retriction enabled by this amendment will enforce + // alice to wait for more ledgers to close before she can delete her + // account, to prevent duplicate NFTokenIDs + auto const acctDelFee1{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); + env.close(); + + // alice's account is still present + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // Close more ledgers until it is no longer within + // + // to be able to delete alice's account + incLgrSeqForNFTokenAccDel(env, alice); + + // alice's account is deleted + auto const acctDelFee2{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee2)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as prevNftokenID + uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + + // burn the NFT to make sure alice owns remintNftokenID + env(token::burn(alice, remintNftokenID)); + env.close(); + + // The new NFT minted will not have the same ID + // as one of NFTs authorized minter minted using tickets + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) == + nftIDs.end()); + } + } + void testWithFeats(FeatureBitset features) { @@ -6113,6 +6710,7 @@ class NFToken_test : public beast::unit_test::suite testFixNFTokenNegOffer(features); testIOUWithTransferFee(features); testBrokeredSaleToSelf(features); + testFixNFTokenRemint(features); } public: diff --git a/src/test/jtx/impl/token.cpp b/src/test/jtx/impl/token.cpp index dcb0b19f9cc..6565f01fb48 100644 --- a/src/test/jtx/impl/token.cpp +++ b/src/test/jtx/impl/token.cpp @@ -67,9 +67,9 @@ getNextID( { std::uint32_t nftSeq; - // If fixUnburnableNFToken amendment is on, we must + // If fixNFTokenRemint amendment is on, we must // generate the NFT sequence using new construct - if (env.current()->rules().enabled(fixUnburnableNFToken)) + if (env.current()->rules().enabled(fixNFTokenRemint)) nftSeq = { env.le(issuer) ->at(~sfFirstNFTokenSequence) From facf8f0235188eb3968489b2c83aee6a811c882d Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 14 Feb 2023 15:28:12 -0500 Subject: [PATCH 20/38] [FOLD] refactor sequence generation --- src/ripple/app/tx/impl/NFTokenMint.cpp | 73 ++++++++++++++------------ 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index be478576980..ab7630b6041 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -160,41 +160,10 @@ NFTokenMint::doApply() // Should not happen. Checked in preclaim. return Unexpected(tecNO_ISSUER); - std::uint32_t tokenSeq; - if (ctx_.view().rules().enabled(fixNFTokenRemint)) - { - auto const accSeq = (*root)[sfSequence]; - - // If the issuer hasn't minted a NFT before, we must - // initialize sfFirstNFTokenSequence to equal to the current account - // sequence. In general, we must subtract account sequence by - // one, since it is incremented by the transactor beforehand. In - // scenarios of AuthorizedMinting or Tickets, we use the - // account sequence as it is because it has not been incremented - std::uint32_t const firstNFTokenSeq = - (*root)[~sfFirstNFTokenSequence].value_or( - (*root)[~sfNFTokenMinter] == ctx_.tx[sfAccount] || - ctx_.tx.getSeqProxy().isTicket() - ? accSeq - : accSeq - 1); - - // Get the unique sequence number of this token by - // sfFirstNFTokenSequence + sfMintedNFTokens - tokenSeq = firstNFTokenSeq + (*root)[~sfMintedNFTokens].value_or(0); - { - std::uint32_t const nextTokenSeq = tokenSeq + 1; - if (nextTokenSeq < tokenSeq) - return Unexpected(tecMAX_SEQUENCE_REACHED); - - (*root)[sfMintedNFTokens] = - (*root)[~sfMintedNFTokens].value_or(0) + 1; - (*root)[sfFirstNFTokenSequence] = firstNFTokenSeq; - } - } - else + if (!ctx_.view().rules().enabled(fixNFTokenRemint)) { // Get the unique sequence number for this token: - tokenSeq = (*root)[~sfMintedNFTokens].value_or(0); + std::uint32_t const tokenSeq = (*root)[~sfMintedNFTokens].value_or(0); { std::uint32_t const nextTokenSeq = tokenSeq + 1; if (nextTokenSeq < tokenSeq) @@ -202,7 +171,45 @@ NFTokenMint::doApply() (*root)[sfMintedNFTokens] = nextTokenSeq; } + ctx_.view().update(root); + return tokenSeq; } + + + // With fixNFTokenRemint amendment enabled: + // + // If the issuer hasn't minted a NFT before, we must + // initialize sfFirstNFTokenSequence to equal to the current account + // sequence. In general, we must subtract account sequence by + // one, since it is incremented by the transactor beforehand. In + // scenarios of AuthorizedMinting or Tickets, we use the + // account sequence as it is because it has not been incremented. + // + // Whether we subtract the acct sequence by 1 is unimportant in real + // world use case. But it is needed in order to deterministically + // generate the NFTokenSequence for test cases. + if (auto fts = (*root)[~sfFirstNFTokenSequence]; !fts) + { + auto const accSeq = (*root)[sfSequence]; + (*root)[sfFirstNFTokenSequence] = (*root)[~sfNFTokenMinter] == ctx_.tx[sfAccount] || ctx_.tx.getSeqProxy().isTicket()? accSeq : accSeq - 1; + } + + auto const mintedNftCnt = (*root)[~sfMintedNFTokens].value_or(0); + + (*root)[sfMintedNFTokens] = mintedNftCnt + 1; + + if ((*root)[sfMintedNFTokens] == 0) + return Unexpected(tecMAX_SEQUENCE_REACHED); + + // Get the unique sequence number of this token by + // sfFirstNFTokenSequence + sfMintedNFTokens + auto const offset = (*root)[sfFirstNFTokenSequence]; + auto const tokenSeq = offset + mintedNftCnt; + + // Check for more overflow cases + if (tokenSeq + 1 == 0 || tokenSeq < mintedNftCnt) + return Unexpected(tecMAX_SEQUENCE_REACHED); + ctx_.view().update(root); return tokenSeq; }(); From 4ef2890873b5e2efb7491fc7a8df889f16f2bac1 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 14 Feb 2023 15:29:20 -0500 Subject: [PATCH 21/38] [FOLD] clang --- src/ripple/app/tx/impl/NFTokenMint.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index ab7630b6041..30e14c9c365 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -160,10 +160,11 @@ NFTokenMint::doApply() // Should not happen. Checked in preclaim. return Unexpected(tecNO_ISSUER); - if (!ctx_.view().rules().enabled(fixNFTokenRemint)) + if (!ctx_.view().rules().enabled(fixNFTokenRemint)) { // Get the unique sequence number for this token: - std::uint32_t const tokenSeq = (*root)[~sfMintedNFTokens].value_or(0); + std::uint32_t const tokenSeq = + (*root)[~sfMintedNFTokens].value_or(0); { std::uint32_t const nextTokenSeq = tokenSeq + 1; if (nextTokenSeq < tokenSeq) @@ -175,7 +176,6 @@ NFTokenMint::doApply() return tokenSeq; } - // With fixNFTokenRemint amendment enabled: // // If the issuer hasn't minted a NFT before, we must @@ -191,12 +191,16 @@ NFTokenMint::doApply() if (auto fts = (*root)[~sfFirstNFTokenSequence]; !fts) { auto const accSeq = (*root)[sfSequence]; - (*root)[sfFirstNFTokenSequence] = (*root)[~sfNFTokenMinter] == ctx_.tx[sfAccount] || ctx_.tx.getSeqProxy().isTicket()? accSeq : accSeq - 1; + (*root)[sfFirstNFTokenSequence] = + (*root)[~sfNFTokenMinter] == ctx_.tx[sfAccount] || + ctx_.tx.getSeqProxy().isTicket() + ? accSeq + : accSeq - 1; } auto const mintedNftCnt = (*root)[~sfMintedNFTokens].value_or(0); - (*root)[sfMintedNFTokens] = mintedNftCnt + 1; + (*root)[sfMintedNFTokens] = mintedNftCnt + 1; if ((*root)[sfMintedNFTokens] == 0) return Unexpected(tecMAX_SEQUENCE_REACHED); From 386ffecc5ded0d4d3dbf036bea041e3d896117c1 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 14 Feb 2023 15:35:41 -0500 Subject: [PATCH 22/38] [FOLD] newline --- src/ripple/app/tx/impl/NFTokenMint.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index 30e14c9c365..379c01910c9 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -191,6 +191,7 @@ NFTokenMint::doApply() if (auto fts = (*root)[~sfFirstNFTokenSequence]; !fts) { auto const accSeq = (*root)[sfSequence]; + (*root)[sfFirstNFTokenSequence] = (*root)[~sfNFTokenMinter] == ctx_.tx[sfAccount] || ctx_.tx.getSeqProxy().isTicket() From 53f32381faa5f00a7597c60496f88d2a8c2e72fe Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 14 Feb 2023 16:23:51 -0500 Subject: [PATCH 23/38] [FOLD] remove new lines --- src/ripple/app/tx/impl/NFTokenMint.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index 379c01910c9..560d487be6f 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -200,9 +200,8 @@ NFTokenMint::doApply() } auto const mintedNftCnt = (*root)[~sfMintedNFTokens].value_or(0); - + (*root)[sfMintedNFTokens] = mintedNftCnt + 1; - if ((*root)[sfMintedNFTokens] == 0) return Unexpected(tecMAX_SEQUENCE_REACHED); From 8747f8e67af43f2e1c035f0c3c6fef3cb02c64cb Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 14 Feb 2023 16:58:05 -0500 Subject: [PATCH 24/38] [FOLD] clang --- src/ripple/app/tx/impl/NFTokenMint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index 560d487be6f..1851b6ab3d7 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -200,7 +200,7 @@ NFTokenMint::doApply() } auto const mintedNftCnt = (*root)[~sfMintedNFTokens].value_or(0); - + (*root)[sfMintedNFTokens] = mintedNftCnt + 1; if ((*root)[sfMintedNFTokens] == 0) return Unexpected(tecMAX_SEQUENCE_REACHED); From c4d448763131e21b237ff97e1197333721493aff Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 14 Feb 2023 17:12:36 -0500 Subject: [PATCH 25/38] [FOLD] reserve field values for amm and hooks --- src/ripple/protocol/impl/SField.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index d59b9bf23cf..14c2bd5c3de 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -150,7 +150,9 @@ CONSTRUCT_TYPED_SFIELD(sfMintedNFTokens, "MintedNFTokens", UINT32, CONSTRUCT_TYPED_SFIELD(sfBurnedNFTokens, "BurnedNFTokens", UINT32, 44); CONSTRUCT_TYPED_SFIELD(sfHookStateCount, "HookStateCount", UINT32, 45); CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32, 46); -CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 47); +// Three field values of 47, 48 and 49 are reserved for +// LockCount(Hooks), VoteWeight(AMM), DiscountedFee(AMM) +CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50); // 64-bit integers (common) CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1); From d04f37f5526c52fba9bc1e1461ef0707cec3a788 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 15 Feb 2023 09:53:23 -0500 Subject: [PATCH 26/38] [FOLD] function renaming --- src/test/app/NFToken_test.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 3a61982d7d1..8221403397b 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6121,7 +6121,7 @@ class NFToken_test : public beast::unit_test::suite // Close the ledger until the ledger sequence is no longer // within . // This is enforced by the fixNFTokenRemint amendment. - auto incLgrSeqForNFTokenAccDel = [&](Env& env, Account const& acc) { + auto incLgrSeqForFixNftRemint = [&](Env& env, Account const& acc) { int delta = 0; auto const deletableLgrSeq = (*env.le(acc))[~sfFirstNFTokenSequence].value_or(0) + @@ -6425,7 +6425,7 @@ class NFToken_test : public beast::unit_test::suite // Close more ledgers until it is no longer within // // to be able to delete alice's account - incLgrSeqForNFTokenAccDel(env, alice); + incLgrSeqForFixNftRemint(env, alice); // alice's account is deleted auto const acctDelFee2{drops(env.current()->fees().increment)}; @@ -6527,7 +6527,7 @@ class NFToken_test : public beast::unit_test::suite // Close more ledgers until it is no longer within // // to be able to delete alice's account - incLgrSeqForNFTokenAccDel(env, alice); + incLgrSeqForFixNftRemint(env, alice); // alice's account is deleted auto const acctDelFee2{drops(env.current()->fees().increment)}; @@ -6641,7 +6641,7 @@ class NFToken_test : public beast::unit_test::suite // Close more ledgers until it is no longer within // // to be able to delete alice's account - incLgrSeqForNFTokenAccDel(env, alice); + incLgrSeqForFixNftRemint(env, alice); // alice's account is deleted auto const acctDelFee2{drops(env.current()->fees().increment)}; From 9bfa3ed18bba097838c584b3aa71c0b862a82c6c Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 7 Mar 2023 12:05:49 -0500 Subject: [PATCH 27/38] [fold] remove auto and addressed some comments --- src/ripple/app/tx/impl/NFTokenMint.cpp | 41 +++++++++++--------- src/test/app/NFTokenBurn_test.cpp | 16 +++----- src/test/app/NFTokenDir_test.cpp | 52 ++++++++++---------------- src/test/jtx/impl/token.cpp | 15 +++----- 4 files changed, 54 insertions(+), 70 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index 1851b6ab3d7..a769575093b 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -178,40 +178,45 @@ NFTokenMint::doApply() // With fixNFTokenRemint amendment enabled: // - // If the issuer hasn't minted a NFT before, we must - // initialize sfFirstNFTokenSequence to equal to the current account - // sequence. In general, we must subtract account sequence by - // one, since it is incremented by the transactor beforehand. In - // scenarios of AuthorizedMinting or Tickets, we use the - // account sequence as it is because it has not been incremented. + // If the issuer hasn't minted an NFToken before we must add a + // FirstNFTokenSequence field to the issuer's AccountRoot. The + // value of the FirstNFTokenSequence must equal the issuer's + // current account sequence. // - // Whether we subtract the acct sequence by 1 is unimportant in real - // world use case. But it is needed in order to deterministically - // generate the NFTokenSequence for test cases. + // There are three situations: + // o If the first token is being minted by the issuer and + // * If the transaction consumes a Sequence number, then the + // Sequence has been pre-incremented by the time we get here in + // doApply. We must decrement the value in the Sequence field. + // * Otherwise the transaction uses a Ticket so the Sequence has + // not been pre-incremented. We use the Sequence value as is. + // o The first token is being minted by an authorized minter. In + // this case the issuer's Sequence field has been left untouched. + // We use the issuer's Sequence value as is. if (auto fts = (*root)[~sfFirstNFTokenSequence]; !fts) { - auto const accSeq = (*root)[sfSequence]; + std::uint32_t const acctSeq = (*root)[sfSequence]; (*root)[sfFirstNFTokenSequence] = (*root)[~sfNFTokenMinter] == ctx_.tx[sfAccount] || ctx_.tx.getSeqProxy().isTicket() - ? accSeq - : accSeq - 1; + ? acctSeq + : acctSeq - 1; } - auto const mintedNftCnt = (*root)[~sfMintedNFTokens].value_or(0); + std::uint32_t const mintedNftCnt = (*root)[~sfMintedNFTokens].value_or(0u); - (*root)[sfMintedNFTokens] = mintedNftCnt + 1; - if ((*root)[sfMintedNFTokens] == 0) + (*root)[sfMintedNFTokens] = mintedNftCnt + 1u; + if ((*root)[sfMintedNFTokens] == 0u) return Unexpected(tecMAX_SEQUENCE_REACHED); // Get the unique sequence number of this token by // sfFirstNFTokenSequence + sfMintedNFTokens - auto const offset = (*root)[sfFirstNFTokenSequence]; - auto const tokenSeq = offset + mintedNftCnt; + std::uint32_t const offset = (*root)[sfFirstNFTokenSequence]; + std::uint32_t const tokenSeq = offset + mintedNftCnt; // Check for more overflow cases - if (tokenSeq + 1 == 0 || tokenSeq < mintedNftCnt) + if (tokenSeq + 1u == 0u || tokenSeq < offset) return Unexpected(tecMAX_SEQUENCE_REACHED); ctx_.view().update(root); diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 76b2934982e..2ca457ff043 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -380,19 +380,15 @@ class NFTokenBurn_test : public beast::unit_test::suite auto internalTaxon = [&env]( Account const& acct, std::uint32_t taxon) -> std::uint32_t { - std::uint32_t tokenSeq; + std::uint32_t tokenSeq = + env.le(acct)->at(~sfMintedNFTokens).value_or(0); // If fixNFTokenRemint amendment is on, we must - // generate the NFT sequence using new construct + // add FirstNFTokenSequence. if (env.current()->rules().enabled(fixNFTokenRemint)) - tokenSeq = { - env.le(acct) - ->at(~sfFirstNFTokenSequence) - .value_or(env.seq(acct)) + - env.le(acct)->at(~sfMintedNFTokens).value_or(0)}; - else - tokenSeq = { - env.le(acct)->at(~sfMintedNFTokens).value_or(0)}; + tokenSeq += env.le(acct) + ->at(~sfFirstNFTokenSequence) + .value_or(env.seq(acct)); return toUInt32( nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon))); diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index e008a29d1c7..e9addfa83f7 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -191,13 +191,11 @@ class NFTokenDir_test : public beast::unit_test::suite Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - // Should not advance ledger if fixNFTokenRemint is - // enabled. Because otherwise, accounts are initialized at - // different ledgers, and will have different account - // sequences, which then causes the sequence number of - // NFTokenIDs to be different - if (!features[fixNFTokenRemint]) - env.close(); + // Do not close the ledger inside the loop. If + // fixNFTokenRemint is enabled and accounts are initialized + // at different ledgers, they will have different account + // sequences. That would cause the accounts to have + // different NFTokenID sequence numbers. } env.close(); @@ -417,13 +415,11 @@ class NFTokenDir_test : public beast::unit_test::suite Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - // Should not advance ledger if fixNFTokenRemint is - // enabled. Because otherwise, accounts are initialized at - // different ledgers, and will have different account - // sequences, which then causes the sequence number of - // NFTokenIDs to be different - if (!features[fixNFTokenRemint]) - env.close(); + // Do not close the ledger inside the loop. If + // fixNFTokenRemint is enabled and accounts are initialized + // at different ledgers, they will have different account + // sequences. That would cause the accounts to have + // different NFTokenID sequence numbers. } env.close(); @@ -663,21 +659,17 @@ class NFTokenDir_test : public beast::unit_test::suite // Create accounts for all of the seeds and fund those accounts. std::vector accounts; accounts.reserve(seeds.size()); - - // If fixNFTokenRemint is not enabled, accounts can be created in - // different ledgers. - // If fixNFTokenRemint is enabled, all accounts must be created in - // the same ledger in order to initialize all accounts with the same - // account sequence. for (std::string_view const& seed : seeds) { Account const& account = accounts.emplace_back(Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - // Only advance the ledger if fixNFTokenRemint is disabled - if (!features[fixNFTokenRemint]) - env.close(); + // Do not close the ledger inside the loop. If + // fixNFTokenRemint is enabled and accounts are initialized + // at different ledgers, they will have different account + // sequences. That would cause the accounts to have + // different NFTokenID sequence numbers. } env.close(); @@ -848,21 +840,17 @@ class NFTokenDir_test : public beast::unit_test::suite // Create accounts for all of the seeds and fund those accounts. std::vector accounts; accounts.reserve(seeds.size()); - - // If fixNFTokenRemint is not enabled, accounts can be created in - // different ledgers. - // If fixNFTokenRemint is enabled, accounts must be created in the - // same ledger in order to initialize all accounts with the same - // account sequence. for (std::string_view const& seed : seeds) { Account const& account = accounts.emplace_back(Account::base58Seed, std::string(seed)); env.fund(XRP(10000), account); - // Only advance the ledger if fixNFTokenRemint is disabled - if (!features[fixNFTokenRemint]) - env.close(); + // Do not close the ledger inside the loop. If + // fixNFTokenRemint is enabled and accounts are initialized + // at different ledgers, they will have different account + // sequences. That would cause the accounts to have + // different NFTokenID sequence numbers. } env.close(); diff --git a/src/test/jtx/impl/token.cpp b/src/test/jtx/impl/token.cpp index 6565f01fb48..3491fb0083f 100644 --- a/src/test/jtx/impl/token.cpp +++ b/src/test/jtx/impl/token.cpp @@ -65,18 +65,13 @@ getNextID( std::uint16_t flags, std::uint16_t xferFee) { - std::uint32_t nftSeq; + std::uint32_t nftSeq = env.le(issuer)->at(~sfMintedNFTokens).value_or(0); - // If fixNFTokenRemint amendment is on, we must - // generate the NFT sequence using new construct + // If fixNFTokenRemint amendment is on, we must add FirstNFTokenSequence. if (env.current()->rules().enabled(fixNFTokenRemint)) - nftSeq = { - env.le(issuer) - ->at(~sfFirstNFTokenSequence) - .value_or(env.seq(issuer)) + - env.le(issuer)->at(~sfMintedNFTokens).value_or(0)}; - else - nftSeq = {env.le(issuer)->at(~sfMintedNFTokens).value_or(0)}; + nftSeq += env.le(issuer) + ->at(~sfFirstNFTokenSequence) + .value_or(env.seq(issuer)); return token::getID(issuer, nfTokenTaxon, nftSeq, flags, xferFee); } From 5a174b36de0b03cb2d08a6cb68b8aa2bc617ba97 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 7 Mar 2023 14:40:55 -0500 Subject: [PATCH 28/38] [fold]param renaming --- src/test/app/NFToken_test.cpp | 82 ++++++++++------------------------- src/test/jtx/impl/token.cpp | 21 +++++---- src/test/jtx/token.h | 1 + 3 files changed, 37 insertions(+), 67 deletions(-) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 8221403397b..57d805a2f86 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -314,25 +314,13 @@ class NFToken_test : public beast::unit_test::suite while (seq < 33) { - if (features[fixNFTokenRemint]) - // If fixNFTokenRemint is enabled, we must add - // FirstNFTokenSequence to offset the starting NFT sequence - // number - env(token::burn( - alice, - token::getID( - alice, - 0, - (*env.le(alice))[sfFirstNFTokenSequence] + seq++))); - else - env(token::burn(alice, token::getID(alice, 0, seq++))); - + env(token::burn(alice, token::getID(env, alice, 0, seq++))); env.close(); checkAliceOwnerMintedBurned((33 - seq) ? 1 : 0, 33, seq, __LINE__); } // alice burns a non-existent NFT. - env(token::burn(alice, token::getID(alice, 197, 5)), ter(tecNO_ENTRY)); + env(token::burn(alice, token::getID(env, alice, 197, 5)), ter(tecNO_ENTRY)); env.close(); checkAliceOwnerMintedBurned(0, 33, 33, __LINE__); @@ -442,19 +430,7 @@ class NFToken_test : public beast::unit_test::suite // minter burns the NFTs she created. while (nftSeq < 65) { - if (features[fixNFTokenRemint]) - // If fixNFTokenRemint is enabled, we must add - // FirstNFTokenSequence to offset the starting NFT sequence - // number - env(token::burn( - minter, - token::getID( - alice, - 0, - (*env.le(alice))[sfFirstNFTokenSequence] + nftSeq++))); - else - env(token::burn(minter, token::getID(alice, 0, nftSeq++))); - + env(token::burn(minter, token::getID(env, alice, 0, nftSeq++))); env.close(); checkMintersOwnerMintedBurned( 0, 66, nftSeq, (65 - seq) ? 1 : 0, 0, 0, __LINE__); @@ -462,22 +438,12 @@ class NFToken_test : public beast::unit_test::suite // minter has one more NFT to burn. Should take her owner count to // 0. - if (features[fixNFTokenRemint]) - // If fixNFTokenRemint is enabled, we must add - // FirstNFTokenSequence to offset the starting NFT sequence number - env(token::burn( - minter, - token::getID( - alice, - 0, - (*env.le(alice))[sfFirstNFTokenSequence] + nftSeq++))); - else - env(token::burn(minter, token::getID(alice, 0, nftSeq++))); + env(token::burn(minter, token::getID(env, alice, 0, nftSeq++))); env.close(); checkMintersOwnerMintedBurned(0, 66, nftSeq, 0, 0, 0, __LINE__); // minter burns a non-existent NFT. - env(token::burn(minter, token::getID(alice, 2009, 3)), + env(token::burn(minter, token::getID(env, alice, 2009, 3)), ter(tecNO_ENTRY)); env.close(); checkMintersOwnerMintedBurned(0, 66, nftSeq, 0, 0, 0, __LINE__); @@ -678,7 +644,7 @@ class NFToken_test : public beast::unit_test::suite // preclaim // Try to burn a token that doesn't exist. - env(token::burn(alice, token::getID(alice, 0, 1)), ter(tecNO_ENTRY)); + env(token::burn(alice, token::getID(env, alice, 0, 1)), ter(tecNO_ENTRY)); env.close(); BEAST_EXPECT(ownerCount(env, buyer) == 0); @@ -824,14 +790,14 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(ownerCount(env, buyer) == 0); // The nftID must be present in the ledger. - env(token::createOffer(buyer, token::getID(alice, 0, 1), XRP(1000)), + env(token::createOffer(buyer, token::getID(env, alice, 0, 1), XRP(1000)), token::owner(alice), ter(tecNO_ENTRY)); env.close(); BEAST_EXPECT(ownerCount(env, buyer) == 0); // The nftID must be present in the ledger of a sell offer too. - env(token::createOffer(alice, token::getID(alice, 0, 1), XRP(1000)), + env(token::createOffer(alice, token::getID(env, alice, 0, 1), XRP(1000)), txflags(tfSellNFToken), ter(tecNO_ENTRY)); env.close(); @@ -2608,7 +2574,7 @@ class NFToken_test : public beast::unit_test::suite } }; - uint256 const nftAliceID = token::getID( + uint256 const nftAliceID = token::getID(env, alice, taxon, rand_int(), @@ -2616,7 +2582,7 @@ class NFToken_test : public beast::unit_test::suite rand_int()); check(taxon, nftAliceID); - uint256 const nftBeckyID = token::getID( + uint256 const nftBeckyID = token::getID(env, becky, taxon, rand_int(), @@ -6103,29 +6069,29 @@ class NFToken_test : public beast::unit_test::suite // Returns the current ledger sequence auto openLedgerSeq = [](Env& env) { return env.current()->seq(); }; - // Close the ledger until the ledger sequence is large enough to close + // Close the ledger until the ledger sequence is large enough to delete // the account (no longer within ) // This is enforced by the featureDeletableAccounts amendment - auto incLgrSeqForAccDel = [&](Env& env, Account const& acc) { + auto incLgrSeqForAcctDel = [&](Env& env, Account const& acct) { int const delta = [&]() -> int { - if (env.seq(acc) + 255 > openLedgerSeq(env)) - return env.seq(acc) - openLedgerSeq(env) + 255; + if (env.seq(acct) + 255 > openLedgerSeq(env)) + return env.seq(acct) - openLedgerSeq(env) + 255; return 0; }(); BEAST_EXPECT(delta >= 0); for (int i = 0; i < delta; ++i) env.close(); - BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255); + BEAST_EXPECT(openLedgerSeq(env) == env.seq(acct) + 255); }; // Close the ledger until the ledger sequence is no longer // within . // This is enforced by the fixNFTokenRemint amendment. - auto incLgrSeqForFixNftRemint = [&](Env& env, Account const& acc) { + auto incLgrSeqForFixNftRemint = [&](Env& env, Account const& acct) { int delta = 0; auto const deletableLgrSeq = - (*env.le(acc))[~sfFirstNFTokenSequence].value_or(0) + - (*env.le(acc))[sfMintedNFTokens] + 255; + (*env.le(acct))[~sfFirstNFTokenSequence].value_or(0) + + (*env.le(acct))[sfMintedNFTokens] + 255; if (deletableLgrSeq > openLedgerSeq(env)) delta = deletableLgrSeq - openLedgerSeq(env); @@ -6157,7 +6123,7 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 1); // Close enough ledgers to delete alice's account - incLgrSeqForAccDel(env, alice); + incLgrSeqForAcctDel(env, alice); // alice's account is deleted Keylet const aliceAcctKey{keylet::account(alice.id())}; @@ -6235,7 +6201,7 @@ class NFToken_test : public beast::unit_test::suite // Increment ledger sequence to the number that is // enforced by the featureDeletableAccounts amendment - incLgrSeqForAccDel(env, alice); + incLgrSeqForAcctDel(env, alice); // Verify that alice's account root is present. Keylet const aliceAcctKey{keylet::account(alice.id())}; @@ -6320,7 +6286,7 @@ class NFToken_test : public beast::unit_test::suite // Increment ledger sequence to the number that is // enforced by the featureDeletableAccounts amendment - incLgrSeqForAccDel(env, alice); + incLgrSeqForAcctDel(env, alice); // Verify that alice's account root is present. Keylet const aliceAcctKey{keylet::account(alice.id())}; @@ -6401,7 +6367,7 @@ class NFToken_test : public beast::unit_test::suite // Increment ledger sequence to the number that is // enforced by the featureDeletableAccounts amendment - incLgrSeqForAccDel(env, alice); + incLgrSeqForAcctDel(env, alice); // Verify that alice's account root is present. Keylet const aliceAcctKey{keylet::account(alice.id())}; @@ -6508,7 +6474,7 @@ class NFToken_test : public beast::unit_test::suite // Increment ledger sequence to the number that is // enforced by the featureDeletableAccounts amendment - incLgrSeqForAccDel(env, alice); + incLgrSeqForAcctDel(env, alice); // Verify that alice's account root is present. Keylet const aliceAcctKey{keylet::account(alice.id())}; @@ -6617,7 +6583,7 @@ class NFToken_test : public beast::unit_test::suite // Increment ledger sequence to the number that is // enforced by the featureDeletableAccounts amendment - incLgrSeqForAccDel(env, alice); + incLgrSeqForAcctDel(env, alice); // Verify that alice's account root is present. Keylet const aliceAcctKey{keylet::account(alice.id())}; diff --git a/src/test/jtx/impl/token.cpp b/src/test/jtx/impl/token.cpp index 3491fb0083f..76c5388880e 100644 --- a/src/test/jtx/impl/token.cpp +++ b/src/test/jtx/impl/token.cpp @@ -65,25 +65,28 @@ getNextID( std::uint16_t flags, std::uint16_t xferFee) { - std::uint32_t nftSeq = env.le(issuer)->at(~sfMintedNFTokens).value_or(0); - - // If fixNFTokenRemint amendment is on, we must add FirstNFTokenSequence. - if (env.current()->rules().enabled(fixNFTokenRemint)) - nftSeq += env.le(issuer) - ->at(~sfFirstNFTokenSequence) - .value_or(env.seq(issuer)); - - return token::getID(issuer, nfTokenTaxon, nftSeq, flags, xferFee); + std::uint32_t const nftSeq = { + env.le(issuer)->at(~sfMintedNFTokens).value_or(0)}; + return token::getID(env, issuer, nfTokenTaxon, nftSeq, flags, xferFee); } uint256 getID( + jtx::Env const& env, jtx::Account const& issuer, std::uint32_t nfTokenTaxon, std::uint32_t nftSeq, std::uint16_t flags, std::uint16_t xferFee) { + if (env.current()->rules().enabled(fixNFTokenRemint)) + { + // If fixNFTokenRemint is enabled, we must add issuer's + // FirstNFTokenSequence to offset the starting NFT sequence number. + nftSeq += env.le(issuer) + ->at(~sfFirstNFTokenSequence) + .value_or(env.seq(issuer)); + } return ripple::NFTokenMint::createNFTokenID( flags, xferFee, issuer, nft::toTaxon(nfTokenTaxon), nftSeq); } diff --git a/src/test/jtx/token.h b/src/test/jtx/token.h index 44f89087b85..150ddfab0ea 100644 --- a/src/test/jtx/token.h +++ b/src/test/jtx/token.h @@ -95,6 +95,7 @@ getNextID( /** Get the NFTokenID for a particular nftSequence. */ uint256 getID( + jtx::Env const& env, jtx::Account const& account, std::uint32_t tokenTaxon, std::uint32_t nftSeq, From 8041edd517c61b2e3164fcd9b2e6e1bfac347d99 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 7 Mar 2023 16:13:06 -0500 Subject: [PATCH 29/38] [fold] refactor tests --- src/test/app/NFToken_test.cpp | 451 ++++++++++++++-------------------- 1 file changed, 182 insertions(+), 269 deletions(-) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 57d805a2f86..86673405466 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6113,10 +6113,10 @@ class NFToken_test : public beast::unit_test::suite env.close(); // alice mint and burn a NFT - uint256 const prevNftokenID = token::getNextID(env, alice, 0u); + uint256 const prevNFTokenID = token::getNextID(env, alice, 0u); env(token::mint(alice)); env.close(); - env(token::burn(alice, prevNftokenID)); + env(token::burn(alice, prevNFTokenID)); env.close(); // alice has minted 1 NFToken @@ -6145,29 +6145,25 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(env.current()->exists(aliceAcctKey)); BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); - // alice mints a NFT with same params as prevNftokenID - uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + // alice mints a NFT with same params as prevNFTokenID + uint256 const remintNFTokenID = token::getNextID(env, alice, 0u); env(token::mint(alice)); env.close(); - // burn the NFT to make sure alice owns remintNftokenID - env(token::burn(alice, remintNftokenID)); + // burn the NFT to make sure alice owns remintNFTokenID + env(token::burn(alice, remintNFTokenID)); env.close(); if (features[fixNFTokenRemint]) // Check that two NFTs don't have the same ID - BEAST_EXPECT(remintNftokenID != prevNftokenID); + BEAST_EXPECT(remintNFTokenID != prevNFTokenID); else // Check that two NFTs have the same ID - BEAST_EXPECT(remintNftokenID == prevNftokenID); + BEAST_EXPECT(remintNFTokenID == prevNFTokenID); } - // If fixNFTokenRemint is not enabled, we test if the issuer account - // can be deleted after an authorized minter mints and burns a batch of - // NFTokens. - // After the issuer's account is re-created and mints a NFT, it should - // have the same NFTokenID as the one minted before. - if (!features[fixNFTokenRemint]) + // Test if the issuer account can be deleted after an authorized + // minter mints and burns a batch of NFTokens. { Env env{*this, features}; Account const alice("alice"); @@ -6208,232 +6204,102 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - auto const acctDelFee1{drops(env.current()->fees().increment)}; - - // alice's account can be successfully deleted. - env(acctdelete(alice, becky), fee(acctDelFee1)); - env.close(); - BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); - - // Fund alice to re-create her account - env.fund(XRP(10000), alice); - env.close(); - - // alice's account now exists and has minted 0 NFTokens - BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); - BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); - - // alice mints a NFT with same params as prevNftokenID - uint256 const remintNftokenID = token::getNextID(env, alice, 0u); - env(token::mint(alice)); - env.close(); - - // burn the NFT to make sure alice owns remintNftokenID - env(token::burn(alice, remintNftokenID)); - env.close(); - - // The new NFT minted has the same ID as one of the NFTs - // authorized minter minted for alice - BEAST_EXPECT( - std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) != - nftIDs.end()); - } - - // If fixNFTokenRemint is not enabled, - // when an account mints and burns a batch of NFTokens using tickets, - // the account should be able to be deleted. - // After the issuer's account is re-created and mints a NFT, it should - // have the same NFTokenID as the one minted before. - if (!features[fixNFTokenRemint]) - { - Env env{*this, features}; - - Account const alice{"alice"}; - Account const becky{"becky"}; - env.fund(XRP(10000), alice, becky); - env.close(); - - // alice grab enough tickets for all of the following - // transactions. Note that once the tickets are acquired alice's - // account sequence number should not advance. - std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; - env(ticket::create(alice, 100)); - env.close(); - - BEAST_EXPECT(ticketCount(env, alice) == 100); - BEAST_EXPECT(ownerCount(env, alice) == 100); + auto const acctDelFee{drops(env.current()->fees().increment)}; - // alice mints 50 NFTs using tickets - std::vector nftIDs; - nftIDs.reserve(50); - for (int i = 0; i < 50; i++) + if (!features[fixNFTokenRemint]) { - nftIDs.push_back(token::getNextID(env, alice, 0u)); - env(token::mint(alice, 0u), ticket::use(aliceTicketSeq++)); + // alice's account can be successfully deleted. + env(acctdelete(alice, becky), fee(acctDelFee)); env.close(); - } - - // alice burns 50 NFTs using tickets - for (auto const nftokenID : nftIDs) - { - env(token::burn(alice, nftokenID), - ticket::use(aliceTicketSeq++)); - } - env.close(); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); - BEAST_EXPECT(ticketCount(env, alice) == 0); - - // Increment ledger sequence to the number that is - // enforced by the featureDeletableAccounts amendment - incLgrSeqForAcctDel(env, alice); - - // Verify that alice's account root is present. - Keylet const aliceAcctKey{keylet::account(alice.id())}; - BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); - BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - - // alice tries to delete her account, and is successful. - auto const acctDelFee1{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee1)); - env.close(); - - // alice's account account root is gone from the most recently - // closed ledger and the current ledger. - BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); - BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); - - // Fund alice to re-create her account - env.fund(XRP(10000), alice); - env.close(); - - // alice's account now exists and has minted 0 NFTokens - BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); - BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); - - // alice mints a NFT with same params as prevNftokenID - uint256 const remintNftokenID = token::getNextID(env, alice, 0u); - env(token::mint(alice)); - env.close(); - - // burn the NFT to make sure alice owns remintNftokenID - env(token::burn(alice, remintNftokenID)); - env.close(); - - // The new NFT minted will have the same ID - // as one of NFTs minted using tickets - BEAST_EXPECT( - std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) != - nftIDs.end()); - } + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); - // If fixNFTokenRemint is enabled, - // when an authorized minter mints and burns a batch of NFTokens, - // issuer's account needs to wait a longer time before it can deleted. - // After the issuer's account is re-created and mints a NFT, it should - // not have the same NFTokenID as the ones authorized minter minted. - if (features[fixNFTokenRemint]) - { - Env env{*this, features}; - Account const alice("alice"); - Account const becky("becky"); - Account const minter{"minter"}; + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); - env.fund(XRP(10000), alice, becky, minter); - env.close(); + // alice mints a NFT with same params as the first one before + // the account delete. + uint256 const remintNFTokenID = + token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); - // alice sets minter as her authorized minter - env(token::setMinter(alice, minter)); - env.close(); + // burn the NFT to make sure alice owns remintNFTokenID + env(token::burn(alice, remintNFTokenID)); + env.close(); - // minter mints 500 NFTs for alice - std::vector nftIDs; - nftIDs.reserve(500); - for (int i = 0; i < 500; i++) - { - uint256 const nftokenID = token::getNextID(env, alice, 0u); - nftIDs.push_back(nftokenID); - env(token::mint(minter), token::issuer(alice)); + // The new NFT minted has the same ID as one of the NFTs + // authorized minter minted for alice + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNFTokenID) != + nftIDs.end()); } - env.close(); - - // minter burns 500 NFTs - for (auto const nftokenID : nftIDs) + else if (features[fixNFTokenRemint]) { - env(token::burn(minter, nftokenID)); - } - env.close(); - - // Increment ledger sequence to the number that is - // enforced by the featureDeletableAccounts amendment - incLgrSeqForAcctDel(env, alice); - - // Verify that alice's account root is present. - Keylet const aliceAcctKey{keylet::account(alice.id())}; - BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); - BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - - // alice tries to delete her account, but is unsuccessful. - // Due to authorized minting, alice's account sequence does not - // advance while minter mints NFTokens for her. - // The new account deletion retriction enabled by this amendment will enforce - // alice to wait for more ledgers to close before she can delete her - // account, to prevent duplicate NFTokenIDs - auto const acctDelFee1{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); - env.close(); + // alice tries to delete her account, but is unsuccessful. + // Due to authorized minting, alice's account sequence does not + // advance while minter mints NFTokens for her. + // The new account deletion retriction enabled by this amendment will enforce + // alice to wait for more ledgers to close before she can + // delete her account, to prevent duplicate NFTokenIDs + env(acctdelete(alice, becky), + fee(acctDelFee), + ter(tecTOO_SOON)); + env.close(); - // alice's account is still present - BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + // alice's account is still present + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - // Close more ledgers until it is no longer within - // - // to be able to delete alice's account - incLgrSeqForFixNftRemint(env, alice); + // Close more ledgers until it is no longer within + // + // to be able to delete alice's account + incLgrSeqForFixNftRemint(env, alice); - // alice's account is deleted - auto const acctDelFee2{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee2)); - env.close(); + // alice's account is deleted + env(acctdelete(alice, becky), fee(acctDelFee)); + env.close(); - // alice's account account root is gone from the most recently - // closed ledger and the current ledger. - BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); - BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); - // Fund alice to re-create her account - env.fund(XRP(10000), alice); - env.close(); + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); - // alice's account now exists and has minted 0 NFTokens - BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); - BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); - // alice mints a NFT with same params as prevNftokenID - uint256 const remintNftokenID = token::getNextID(env, alice, 0u); - env(token::mint(alice)); - env.close(); + // alice mints a NFT with same params as the first one before + // the account delete. + uint256 const remintNFTokenID = + token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); - // burn the NFT to make sure alice owns remintNftokenID - env(token::burn(alice, remintNftokenID)); - env.close(); + // burn the NFT to make sure alice owns remintNFTokenID + env(token::burn(alice, remintNFTokenID)); + env.close(); - // The new NFT minted will not have the same ID - // as one of NFTs authorized minter minted - BEAST_EXPECT( - std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) == - nftIDs.end()); + // The new NFT minted will not have the same ID + // as any of the NFTs authorized minter minted + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNFTokenID) == + nftIDs.end()); + } } - // If fixNFTokenRemint is enabled, - // when an account mints and burns a batch of NFTokens using tickets, - // the account needs to wait a longer time before it can deleted. - // After the issuer's account is re-created and mints a NFT, it should - // not have the same NFTokenID as the ones minted using tickets. - if (features[fixNFTokenRemint]) + // When an account mints and burns a batch of NFTokens using tickets, + // see if the the account can be deleted. { Env env{*this, features}; @@ -6443,7 +6309,7 @@ class NFToken_test : public beast::unit_test::suite env.close(); // alice grab enough tickets for all of the following - // transactions. Note that once the tickets are acquired alice's + // transactions. Note that once the tickets are acquired alice's // account sequence number should not advance. std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; env(ticket::create(alice, 100)); @@ -6481,55 +6347,103 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - // alice tries to delete her account, but is unsuccessful. - // Because alice used tickets to mint and burn NFTs, her account - // sequence did not change while while submitting these - // transactions. Hence, her is still greater than the current ledger sequence - auto const acctDelFee1{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); - env.close(); + auto const acctDelFee{drops(env.current()->fees().increment)}; - // Close more ledgers until it is no longer within - // - // to be able to delete alice's account - incLgrSeqForFixNftRemint(env, alice); + if (!features[fixNFTokenRemint]) + { + // alice tries to delete her account, and is successful. + env(acctdelete(alice, becky), fee(acctDelFee)); + env.close(); - // alice's account is deleted - auto const acctDelFee2{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee2)); - env.close(); + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); - // alice's account account root is gone from the most recently - // closed ledger and the current ledger. - BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); - BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); - // Fund alice to re-create her account - env.fund(XRP(10000), alice); - env.close(); + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); - // alice's account now exists and has minted 0 NFTokens - BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); - BEAST_EXPECT(env.current()->exists(aliceAcctKey)); - BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + // alice mints a NFT with same params as the first one before + // the account delete. + uint256 const remintNFTokenID = + token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); - // alice mints a NFT with same params as prevNftokenID - uint256 const remintNftokenID = token::getNextID(env, alice, 0u); - env(token::mint(alice)); - env.close(); + // burn the NFT to make sure alice owns remintNFTokenID + env(token::burn(alice, remintNFTokenID)); + env.close(); - // burn the NFT to make sure alice owns remintNftokenID - env(token::burn(alice, remintNftokenID)); - env.close(); + // The new NFT minted will have the same ID + // as one of NFTs minted using tickets + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNFTokenID) != + nftIDs.end()); + } + else if (features[fixNFTokenRemint]) + { + // alice tries to delete her account, but is unsuccessful. + // Due to authorized minting, alice's account sequence does not + // advance while minter mints NFTokens for her using tickets. + // The new account deletion retriction enabled by this amendment will enforce + // alice to wait for more ledgers to close before she can + // delete her account, to prevent duplicate NFTokenIDs + env(acctdelete(alice, becky), + fee(acctDelFee), + ter(tecTOO_SOON)); + env.close(); - // The new NFT minted will not have the same ID - // as one of NFTs alice minted using tickets - BEAST_EXPECT( - std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) == - nftIDs.end()); - } + // alice's account is still present + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + + // Close more ledgers until it is no longer within + // + // to be able to delete alice's account + incLgrSeqForFixNftRemint(env, alice); + + // alice's account is deleted + env(acctdelete(alice, becky), fee(acctDelFee)); + env.close(); + + // alice's account account root is gone from the most recently + // closed ledger and the current ledger. + BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); + + // Fund alice to re-create her account + env.fund(XRP(10000), alice); + env.close(); + + // alice's account now exists and has minted 0 NFTokens + BEAST_EXPECT(env.closed()->exists(aliceAcctKey)); + BEAST_EXPECT(env.current()->exists(aliceAcctKey)); + BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); + + // alice mints a NFT with same params as the first one before + // the account delete. + uint256 const remintNFTokenID = + token::getNextID(env, alice, 0u); + env(token::mint(alice)); + env.close(); + // burn the NFT to make sure alice owns remintNFTokenID + env(token::burn(alice, remintNFTokenID)); + env.close(); + + // The new NFT minted will not have the same ID + // as any of the NFTs authorized minter minted using tickets + BEAST_EXPECT( + std::find(nftIDs.begin(), nftIDs.end(), remintNFTokenID) == + nftIDs.end()); + } + } // If fixNFTokenRemint is enabled, // when an authorized minter mints and burns a batch of NFTokens using // tickets, issuer's account needs to wait a longer time before it can @@ -6597,8 +6511,8 @@ class NFToken_test : public beast::unit_test::suite // MintedNFTokens + 256> enabled by this amendment will enforce // alice to wait for more ledgers to close before she can delete her // account, to prevent duplicate NFTokenIDs - auto const acctDelFee1{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee1), ter(tecTOO_SOON)); + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(alice, becky), fee(acctDelFee), ter(tecTOO_SOON)); env.close(); // alice's account is still present @@ -6610,8 +6524,7 @@ class NFToken_test : public beast::unit_test::suite incLgrSeqForFixNftRemint(env, alice); // alice's account is deleted - auto const acctDelFee2{drops(env.current()->fees().increment)}; - env(acctdelete(alice, becky), fee(acctDelFee2)); + env(acctdelete(alice, becky), fee(acctDelFee)); env.close(); // alice's account account root is gone from the most recently @@ -6628,19 +6541,19 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(env.current()->exists(aliceAcctKey)); BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); - // alice mints a NFT with same params as prevNftokenID - uint256 const remintNftokenID = token::getNextID(env, alice, 0u); + // alice mints a NFT with same params as prevNFTokenID + uint256 const remintNFTokenID = token::getNextID(env, alice, 0u); env(token::mint(alice)); env.close(); - // burn the NFT to make sure alice owns remintNftokenID - env(token::burn(alice, remintNftokenID)); + // burn the NFT to make sure alice owns remintNFTokenID + env(token::burn(alice, remintNFTokenID)); env.close(); // The new NFT minted will not have the same ID // as one of NFTs authorized minter minted using tickets BEAST_EXPECT( - std::find(nftIDs.begin(), nftIDs.end(), remintNftokenID) == + std::find(nftIDs.begin(), nftIDs.end(), remintNFTokenID) == nftIDs.end()); } } From f874f0fb131ce3aa7d3218eaac1cec0f8b7acf4b Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Tue, 7 Mar 2023 16:14:38 -0500 Subject: [PATCH 30/38] [fold] clang --- src/ripple/app/tx/impl/NFTokenMint.cpp | 3 ++- src/test/app/NFTokenBurn_test.cpp | 2 +- src/test/app/NFToken_test.cpp | 18 ++++++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index a769575093b..533e33069a8 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -204,7 +204,8 @@ NFTokenMint::doApply() : acctSeq - 1; } - std::uint32_t const mintedNftCnt = (*root)[~sfMintedNFTokens].value_or(0u); + std::uint32_t const mintedNftCnt = + (*root)[~sfMintedNFTokens].value_or(0u); (*root)[sfMintedNFTokens] = mintedNftCnt + 1u; if ((*root)[sfMintedNFTokens] == 0u) diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 2ca457ff043..1c8648cd7fb 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -380,7 +380,7 @@ class NFTokenBurn_test : public beast::unit_test::suite auto internalTaxon = [&env]( Account const& acct, std::uint32_t taxon) -> std::uint32_t { - std::uint32_t tokenSeq = + std::uint32_t tokenSeq = env.le(acct)->at(~sfMintedNFTokens).value_or(0); // If fixNFTokenRemint amendment is on, we must diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 86673405466..5754bcac553 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -320,7 +320,8 @@ class NFToken_test : public beast::unit_test::suite } // alice burns a non-existent NFT. - env(token::burn(alice, token::getID(env, alice, 197, 5)), ter(tecNO_ENTRY)); + env(token::burn(alice, token::getID(env, alice, 197, 5)), + ter(tecNO_ENTRY)); env.close(); checkAliceOwnerMintedBurned(0, 33, 33, __LINE__); @@ -644,7 +645,8 @@ class NFToken_test : public beast::unit_test::suite // preclaim // Try to burn a token that doesn't exist. - env(token::burn(alice, token::getID(env, alice, 0, 1)), ter(tecNO_ENTRY)); + env(token::burn(alice, token::getID(env, alice, 0, 1)), + ter(tecNO_ENTRY)); env.close(); BEAST_EXPECT(ownerCount(env, buyer) == 0); @@ -790,14 +792,16 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(ownerCount(env, buyer) == 0); // The nftID must be present in the ledger. - env(token::createOffer(buyer, token::getID(env, alice, 0, 1), XRP(1000)), + env(token::createOffer( + buyer, token::getID(env, alice, 0, 1), XRP(1000)), token::owner(alice), ter(tecNO_ENTRY)); env.close(); BEAST_EXPECT(ownerCount(env, buyer) == 0); // The nftID must be present in the ledger of a sell offer too. - env(token::createOffer(alice, token::getID(env, alice, 0, 1), XRP(1000)), + env(token::createOffer( + alice, token::getID(env, alice, 0, 1), XRP(1000)), txflags(tfSellNFToken), ter(tecNO_ENTRY)); env.close(); @@ -2574,7 +2578,8 @@ class NFToken_test : public beast::unit_test::suite } }; - uint256 const nftAliceID = token::getID(env, + uint256 const nftAliceID = token::getID( + env, alice, taxon, rand_int(), @@ -2582,7 +2587,8 @@ class NFToken_test : public beast::unit_test::suite rand_int()); check(taxon, nftAliceID); - uint256 const nftBeckyID = token::getID(env, + uint256 const nftBeckyID = token::getID( + env, becky, taxon, rand_int(), From fdb7a9291a7742d060437f2ffff1e5bddb000d07 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 8 Mar 2023 09:06:23 -0500 Subject: [PATCH 31/38] [fold]readd comment --- src/test/jtx/impl/token.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/jtx/impl/token.cpp b/src/test/jtx/impl/token.cpp index 76c5388880e..6c5cae4147a 100644 --- a/src/test/jtx/impl/token.cpp +++ b/src/test/jtx/impl/token.cpp @@ -65,6 +65,7 @@ getNextID( std::uint16_t flags, std::uint16_t xferFee) { + // Get the nftSeq from the account root of the issuer. std::uint32_t const nftSeq = { env.le(issuer)->at(~sfMintedNFTokens).value_or(0)}; return token::getID(env, issuer, nfTokenTaxon, nftSeq, flags, xferFee); From a217f293602df403aa087efcdbcab1f6819473f0 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 8 Mar 2023 09:17:29 -0500 Subject: [PATCH 32/38] [fold] refactor tokenSeq code --- src/ripple/app/tx/impl/NFTokenMint.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ripple/app/tx/impl/NFTokenMint.cpp b/src/ripple/app/tx/impl/NFTokenMint.cpp index 533e33069a8..c26fb1fb12a 100644 --- a/src/ripple/app/tx/impl/NFTokenMint.cpp +++ b/src/ripple/app/tx/impl/NFTokenMint.cpp @@ -193,12 +193,12 @@ NFTokenMint::doApply() // o The first token is being minted by an authorized minter. In // this case the issuer's Sequence field has been left untouched. // We use the issuer's Sequence value as is. - if (auto fts = (*root)[~sfFirstNFTokenSequence]; !fts) + if (!root->isFieldPresent(sfFirstNFTokenSequence)) { - std::uint32_t const acctSeq = (*root)[sfSequence]; + std::uint32_t const acctSeq = root->at(sfSequence); - (*root)[sfFirstNFTokenSequence] = - (*root)[~sfNFTokenMinter] == ctx_.tx[sfAccount] || + root->at(sfFirstNFTokenSequence) = + ctx_.tx.isFieldPresent(sfIssuer) || ctx_.tx.getSeqProxy().isTicket() ? acctSeq : acctSeq - 1; From ae09383826df1c1f3673d176d56757a87c74f7f9 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 8 Mar 2023 09:26:43 -0500 Subject: [PATCH 33/38] [fold] fix comments --- src/test/app/NFToken_test.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 5754bcac553..92c7e677a8d 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6137,7 +6137,7 @@ class NFToken_test : public beast::unit_test::suite env(acctdelete(alice, becky), fee(acctDelFee)); env.close(); - // alice's account account root is gone from the most recently + // alice's account root is gone from the most recently // closed ledger and the current ledger. BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); @@ -6271,7 +6271,7 @@ class NFToken_test : public beast::unit_test::suite env(acctdelete(alice, becky), fee(acctDelFee)); env.close(); - // alice's account account root is gone from the most recently + // alice's account root is gone from the most recently // closed ledger and the current ledger. BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); @@ -6361,7 +6361,7 @@ class NFToken_test : public beast::unit_test::suite env(acctdelete(alice, becky), fee(acctDelFee)); env.close(); - // alice's account account root is gone from the most recently + // alice's account root is gone from the most recently // closed ledger and the current ledger. BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); @@ -6418,7 +6418,7 @@ class NFToken_test : public beast::unit_test::suite env(acctdelete(alice, becky), fee(acctDelFee)); env.close(); - // alice's account account root is gone from the most recently + // alice's account root is gone from the most recently // closed ledger and the current ledger. BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); @@ -6533,7 +6533,7 @@ class NFToken_test : public beast::unit_test::suite env(acctdelete(alice, becky), fee(acctDelFee)); env.close(); - // alice's account account root is gone from the most recently + // alice's account root is gone from the most recently // closed ledger and the current ledger. BEAST_EXPECT(!env.closed()->exists(aliceAcctKey)); BEAST_EXPECT(!env.current()->exists(aliceAcctKey)); From ac8712e312906bc975284bf00d8abb7ee851450f Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Wed, 8 Mar 2023 16:00:30 -0500 Subject: [PATCH 34/38] udpate comment --- src/test/app/NFToken_test.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 92c7e677a8d..ff54904f583 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6547,7 +6547,8 @@ class NFToken_test : public beast::unit_test::suite BEAST_EXPECT(env.current()->exists(aliceAcctKey)); BEAST_EXPECT((*env.le(alice))[sfMintedNFTokens] == 0); - // alice mints a NFT with same params as prevNFTokenID + // The new NFT minted will not have the same ID + // as any of the NFTs authorized minter minted using tickets uint256 const remintNFTokenID = token::getNextID(env, alice, 0u); env(token::mint(alice)); env.close(); From 7d6644f52a9f5bcf08dc391a324ed6d2223887e9 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Thu, 16 Mar 2023 11:46:19 -0400 Subject: [PATCH 35/38] enable/disable `fixNFTokenRemint` flag in test --- src/test/app/NFToken_test.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index ff54904f583..223a378f3fd 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6607,9 +6607,10 @@ class NFToken_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixNFTDir - fixNonFungibleTokensV1_2); - testWithFeats(all - disallowIncoming - fixNonFungibleTokensV1_2); - testWithFeats(all - fixNonFungibleTokensV1_2); + testWithFeats(all - fixNFTDir - fixNonFungibleTokensV1_2 - fixNFTokenRemint); + testWithFeats(all - disallowIncoming - fixNonFungibleTokensV1_2 - fixNFTokenRemint); + testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTokenRemint); + testWithFeats(all - fixNFTokenRemint); testWithFeats(all); } }; From dacbb5240e4b3dfe3f2584077a4bd69568b3919d Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Thu, 16 Mar 2023 11:47:52 -0400 Subject: [PATCH 36/38] [fold]calng --- src/test/app/NFToken_test.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 223a378f3fd..2ebae5d13d8 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6607,8 +6607,11 @@ class NFToken_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixNFTDir - fixNonFungibleTokensV1_2 - fixNFTokenRemint); - testWithFeats(all - disallowIncoming - fixNonFungibleTokensV1_2 - fixNFTokenRemint); + testWithFeats( + all - fixNFTDir - fixNonFungibleTokensV1_2 - fixNFTokenRemint); + testWithFeats( + all - disallowIncoming - fixNonFungibleTokensV1_2 - + fixNFTokenRemint); testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTokenRemint); testWithFeats(all - fixNFTokenRemint); testWithFeats(all); From acf0873df2326021c03b64579b437209c9f653d8 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Thu, 16 Mar 2023 14:31:26 -0400 Subject: [PATCH 37/38] add test feature flag in burn test --- src/test/app/NFTokenBurn_test.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 1c8648cd7fb..34a83367de8 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -794,8 +794,9 @@ class NFTokenBurn_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTDir); - testWithFeats(all - fixNonFungibleTokensV1_2); + testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTDir - fixNFTokenRemint); + testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTokenRemint); + testWithFeats(all - fixNFTokenRemint); testWithFeats(all); } }; From 6461c587f2bd361e585cbd81658ddd3e3b7ee8e9 Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Thu, 16 Mar 2023 14:34:37 -0400 Subject: [PATCH 38/38] [fold]clang --- src/test/app/NFTokenBurn_test.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 34a83367de8..75c32385acf 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -794,7 +794,8 @@ class NFTokenBurn_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTDir - fixNFTokenRemint); + testWithFeats( + all - fixNonFungibleTokensV1_2 - fixNFTDir - fixNFTokenRemint); testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTokenRemint); testWithFeats(all - fixNFTokenRemint); testWithFeats(all);