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 <shawnxie920@gmail.com>
This commit is contained in:
Shawn Xie
2023-02-02 18:48:03 -05:00
committed by Kenny Lei
parent f7a8d2de84
commit a828e24cf0
4 changed files with 356 additions and 108 deletions

View File

@@ -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<uint256>& 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};
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();
std::vector<uint256> offerIndexes;
offerIndexes.reserve(maxTokenOfferCancelCount);
for (uint32_t i = 0; i < maxTokenOfferCancelCount; ++i)
// Test what happens if a NFT is unburnable when there are
// more than 500 offers, before fixUnburnableNFToken goes live
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(1000), alice, becky);
env.close();
offerIndexes.push_back(keylet::nftoffer(acct, env.seq(acct)).key);
env(token::createOffer(acct, nftokenID, drops(1)),
// 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<uint256> 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])
{
Env env{*this, features};
Account const alice("alice");
Account const becky("becky");
env.fund(XRP(100000), alice, becky);
env.close();
// 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<uint256> 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();
}
// 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)
// Burn the token
env(token::burn(alice, nftokenID));
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();
// Burning the token should remove all 498 sell offers
// that alice created
for (uint256 const& offerIndex : offerIndexes)
{
BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
}
uint256 const aliceOfferIndex =
keylet::nftoffer(alice, env.seq(alice)).key;
env(token::createOffer(alice, nftokenID, drops(1)),
txflags(tfSellNFToken));
env.close();
// Burning the token should also remove the one buy offer
// that becky created
BEAST_EXPECT(!env.le(keylet::nftoffer(beckyOfferIndex)));
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)));
// alice and becky should have ownerCounts of zero
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, becky) == 0);
}
// 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 500 buy offers are removed when NFT is burned
// after fixUnburnableNFToken is enabled
if (features[fixUnburnableNFToken])
{
Env env{*this, features};
Account const alice("alice");
Account const becky("becky");
env.fund(XRP(100000), alice, becky);
env.close();
// 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<uint256> 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)));
}
// Burn the token
env(token::burn(alice, nftokenID));
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++;
}
BEAST_EXPECT(offerIndexes.size() == maxTokenOfferCancelCount + 1);
// 500 sell offers should be removed
BEAST_EXPECT(offerDeletedCount == maxTokenOfferCancelCount);
// alice should have ownerCounts of one for the orphaned sell offer
BEAST_EXPECT(ownerCount(env, alice) == 1);
}
// Test that up to 500 buy/sell offers are removed when NFT is burned
// after fixUnburnableNFToken is enabled
if (features[fixUnburnableNFToken])
{
Env env{*this, features};
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<uint256> 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 @@ public:
FeatureBitset const all{supported_amendments()};
FeatureBitset const fixNFTDir{fixNFTokenDirV1};
testWithFeats(all - fixNFTDir);
testWithFeats(all - fixUnburnableNFToken - fixNFTDir);
testWithFeats(all - fixUnburnableNFToken);
testWithFeats(all);
}
};