Skip to content

Commit 305c9a8

Browse files
authored
fixNFTokenRemint: prevent NFT re-mint: (#4406)
Without the protocol amendment introduced by this commit, an NFT ID can be reminted in this manner: 1. Alice creates an account and mints an NFT. 2. Alice burns the NFT with an `NFTokenBurn` transaction. 3. Alice deletes her account with an `AccountDelete` transaction. 4. Alice re-creates her account. 5. Alice mints an NFT with an `NFTokenMint` transaction with params: `NFTokenTaxon` = 0, `Flags` = 9). This will mint a NFT with the same `NFTokenID` as the one minted in step 1. The params that construct the NFT ID will cause a collision in `NFTokenID` if their values are equal before and after the remint. With the `fixNFTokenRemint` amendment, there is a new sequence number construct which avoids this scenario: - A new `AccountRoot` field, `FirstNFTSequence`, stays constant over time. - This field is set to the current account sequence when the account issues their first NFT. - Otherwise, it is not set. - The sequence of a newly-minted NFT is computed by: `FirstNFTSequence + MintedNFTokens`. - `MintedNFTokens` is then incremented by 1 for each mint. Furthermore, there is a new account deletion restriction: - An account can only be deleted if `FirstNFTSequence + MintedNFTokens + 256` is less than the current ledger sequence. - 256 was chosen because it already exists in the current account deletion constraint. Without this restriction, an NFT may still be remintable. Example scenario: 1. Alice's account sequence is at 1. 2. Bob is Alice's authorized minter. 3. Bob mints 500 NFTs for Alice. The NFTs will have sequences 1-501, as NFT sequence is computed by `FirstNFTokenSequence + MintedNFTokens`). 4. Alice deletes her account at ledger 257 (as required by the existing `AccountDelete` amendment). 5. Alice re-creates her account at ledger 258. 6. Alice mints an NFT. `FirstNFTokenSequence` initializes to her account sequence (258), and `MintedNFTokens` initializes as 0. This newly-minted NFT would have a sequence number of 258, which is a duplicate of what she issued through authorized minting before she deleted her account. --------- Signed-off-by: Shawn Xie <[email protected]>
1 parent 9b2d563 commit 305c9a8

12 files changed

+695
-43
lines changed

src/ripple/app/tx/impl/DeleteAccount.cpp

+17
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,23 @@ DeleteAccount::preclaim(PreclaimContext const& ctx)
214214
if ((*sleAccount)[sfSequence] + seqDelta > ctx.view.seq())
215215
return tecTOO_SOON;
216216

217+
// When fixNFTokenRemint is enabled, we don't allow an account to be
218+
// deleted if <FirstNFTokenSequence + MintedNFTokens> is within 256 of the
219+
// current ledger. This is to prevent having duplicate NFTokenIDs after
220+
// account re-creation.
221+
//
222+
// Without this restriction, duplicate NFTokenIDs can be reproduced when
223+
// authorized minting is involved. Because when the minter mints a NFToken,
224+
// the issuer's sequence does not change. So when the issuer re-creates
225+
// their account and mints a NFToken, it is possible that the
226+
// NFTokenSequence of this NFToken is the same as the one that the
227+
// authorized minter minted in a previous ledger.
228+
if (ctx.view.rules().enabled(fixNFTokenRemint) &&
229+
((*sleAccount)[~sfFirstNFTokenSequence].value_or(0) +
230+
(*sleAccount)[~sfMintedNFTokens].value_or(0) + seqDelta >
231+
ctx.view.seq()))
232+
return tecTOO_SOON;
233+
217234
// Verify that the account does not own any objects that would prevent
218235
// the account from being deleted.
219236
Keylet const ownerDirKeylet{keylet::ownerDir(account)};

src/ripple/app/tx/impl/NFTokenMint.cpp

+57-6
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,66 @@ NFTokenMint::doApply()
160160
// Should not happen. Checked in preclaim.
161161
return Unexpected(tecNO_ISSUER);
162162

163-
// Get the unique sequence number for this token:
164-
std::uint32_t const tokenSeq = (*root)[~sfMintedNFTokens].value_or(0);
163+
if (!ctx_.view().rules().enabled(fixNFTokenRemint))
165164
{
166-
std::uint32_t const nextTokenSeq = tokenSeq + 1;
167-
if (nextTokenSeq < tokenSeq)
168-
return Unexpected(tecMAX_SEQUENCE_REACHED);
165+
// Get the unique sequence number for this token:
166+
std::uint32_t const tokenSeq =
167+
(*root)[~sfMintedNFTokens].value_or(0);
168+
{
169+
std::uint32_t const nextTokenSeq = tokenSeq + 1;
170+
if (nextTokenSeq < tokenSeq)
171+
return Unexpected(tecMAX_SEQUENCE_REACHED);
172+
173+
(*root)[sfMintedNFTokens] = nextTokenSeq;
174+
}
175+
ctx_.view().update(root);
176+
return tokenSeq;
177+
}
178+
179+
// With fixNFTokenRemint amendment enabled:
180+
//
181+
// If the issuer hasn't minted an NFToken before we must add a
182+
// FirstNFTokenSequence field to the issuer's AccountRoot. The
183+
// value of the FirstNFTokenSequence must equal the issuer's
184+
// current account sequence.
185+
//
186+
// There are three situations:
187+
// o If the first token is being minted by the issuer and
188+
// * If the transaction consumes a Sequence number, then the
189+
// Sequence has been pre-incremented by the time we get here in
190+
// doApply. We must decrement the value in the Sequence field.
191+
// * Otherwise the transaction uses a Ticket so the Sequence has
192+
// not been pre-incremented. We use the Sequence value as is.
193+
// o The first token is being minted by an authorized minter. In
194+
// this case the issuer's Sequence field has been left untouched.
195+
// We use the issuer's Sequence value as is.
196+
if (!root->isFieldPresent(sfFirstNFTokenSequence))
197+
{
198+
std::uint32_t const acctSeq = root->at(sfSequence);
169199

170-
(*root)[sfMintedNFTokens] = nextTokenSeq;
200+
root->at(sfFirstNFTokenSequence) =
201+
ctx_.tx.isFieldPresent(sfIssuer) ||
202+
ctx_.tx.getSeqProxy().isTicket()
203+
? acctSeq
204+
: acctSeq - 1;
171205
}
206+
207+
std::uint32_t const mintedNftCnt =
208+
(*root)[~sfMintedNFTokens].value_or(0u);
209+
210+
(*root)[sfMintedNFTokens] = mintedNftCnt + 1u;
211+
if ((*root)[sfMintedNFTokens] == 0u)
212+
return Unexpected(tecMAX_SEQUENCE_REACHED);
213+
214+
// Get the unique sequence number of this token by
215+
// sfFirstNFTokenSequence + sfMintedNFTokens
216+
std::uint32_t const offset = (*root)[sfFirstNFTokenSequence];
217+
std::uint32_t const tokenSeq = offset + mintedNftCnt;
218+
219+
// Check for more overflow cases
220+
if (tokenSeq + 1u == 0u || tokenSeq < offset)
221+
return Unexpected(tecMAX_SEQUENCE_REACHED);
222+
172223
ctx_.view().update(root);
173224
return tokenSeq;
174225
}();

src/ripple/protocol/Feature.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ namespace detail {
7474
// Feature.cpp. Because it's only used to reserve storage, and determine how
7575
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
7676
// the actual number of amendments. A LogicError on startup will verify this.
77-
static constexpr std::size_t numFeatures = 57;
77+
static constexpr std::size_t numFeatures = 58;
7878

7979
/** Amendments that this server supports and the default voting behavior.
8080
Whether they are enabled depends on the Rules defined in the validated
@@ -344,6 +344,7 @@ extern uint256 const featureDisallowIncoming;
344344
extern uint256 const featureXRPFees;
345345
extern uint256 const fixUniversalNumber;
346346
extern uint256 const fixNonFungibleTokensV1_2;
347+
extern uint256 const fixNFTokenRemint;
347348

348349
} // namespace ripple
349350

src/ripple/protocol/SField.h

+1
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ extern SF_UINT32 const sfMintedNFTokens;
400400
extern SF_UINT32 const sfBurnedNFTokens;
401401
extern SF_UINT32 const sfHookStateCount;
402402
extern SF_UINT32 const sfEmitGeneration;
403+
extern SF_UINT32 const sfFirstNFTokenSequence;
403404

404405
// 64-bit integers (common)
405406
extern SF_UINT64 const sfIndexNext;

src/ripple/protocol/impl/Feature.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no)
454454
REGISTER_FEATURE(XRPFees, Supported::yes, DefaultVote::no);
455455
REGISTER_FIX (fixUniversalNumber, Supported::yes, DefaultVote::no);
456456
REGISTER_FIX (fixNonFungibleTokensV1_2, Supported::yes, DefaultVote::no);
457+
REGISTER_FIX (fixNFTokenRemint, Supported::yes, DefaultVote::no);
457458

458459
// The following amendments have been active for at least two years. Their
459460
// pre-amendment code has been removed and the identifiers are deprecated.

src/ripple/protocol/impl/LedgerFormats.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ LedgerFormats::LedgerFormats()
5555
{sfNFTokenMinter, soeOPTIONAL},
5656
{sfMintedNFTokens, soeDEFAULT},
5757
{sfBurnedNFTokens, soeDEFAULT},
58+
{sfFirstNFTokenSequence, soeOPTIONAL},
5859
},
5960
commonFields);
6061

src/ripple/protocol/impl/SField.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ CONSTRUCT_TYPED_SFIELD(sfMintedNFTokens, "MintedNFTokens", UINT32,
150150
CONSTRUCT_TYPED_SFIELD(sfBurnedNFTokens, "BurnedNFTokens", UINT32, 44);
151151
CONSTRUCT_TYPED_SFIELD(sfHookStateCount, "HookStateCount", UINT32, 45);
152152
CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32, 46);
153+
// Three field values of 47, 48 and 49 are reserved for
154+
// LockCount(Hooks), VoteWeight(AMM), DiscountedFee(AMM)
155+
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
153156

154157
// 64-bit integers (common)
155158
CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1);

src/test/app/NFTokenBurn_test.cpp

+14-4
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,16 @@ class NFTokenBurn_test : public beast::unit_test::suite
380380
auto internalTaxon = [&env](
381381
Account const& acct,
382382
std::uint32_t taxon) -> std::uint32_t {
383-
std::uint32_t const tokenSeq = {
384-
env.le(acct)->at(~sfMintedNFTokens).value_or(0)};
383+
std::uint32_t tokenSeq =
384+
env.le(acct)->at(~sfMintedNFTokens).value_or(0);
385+
386+
// If fixNFTokenRemint amendment is on, we must
387+
// add FirstNFTokenSequence.
388+
if (env.current()->rules().enabled(fixNFTokenRemint))
389+
tokenSeq += env.le(acct)
390+
->at(~sfFirstNFTokenSequence)
391+
.value_or(env.seq(acct));
392+
385393
return toUInt32(
386394
nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon)));
387395
};
@@ -786,8 +794,10 @@ class NFTokenBurn_test : public beast::unit_test::suite
786794
FeatureBitset const all{supported_amendments()};
787795
FeatureBitset const fixNFTDir{fixNFTokenDirV1};
788796

789-
testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTDir);
790-
testWithFeats(all - fixNonFungibleTokensV1_2);
797+
testWithFeats(
798+
all - fixNonFungibleTokensV1_2 - fixNFTDir - fixNFTokenRemint);
799+
testWithFeats(all - fixNonFungibleTokensV1_2 - fixNFTokenRemint);
800+
testWithFeats(all - fixNFTokenRemint);
791801
testWithFeats(all);
792802
}
793803
};

src/test/app/NFTokenDir_test.cpp

+30-5
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,14 @@ class NFTokenDir_test : public beast::unit_test::suite
190190
Account const& account = accounts.emplace_back(
191191
Account::base58Seed, std::string(seed));
192192
env.fund(XRP(10000), account);
193-
env.close();
193+
194+
// Do not close the ledger inside the loop. If
195+
// fixNFTokenRemint is enabled and accounts are initialized
196+
// at different ledgers, they will have different account
197+
// sequences. That would cause the accounts to have
198+
// different NFTokenID sequence numbers.
194199
}
200+
env.close();
195201

196202
// All of the accounts create one NFT and and offer that NFT to
197203
// buyer.
@@ -408,8 +414,14 @@ class NFTokenDir_test : public beast::unit_test::suite
408414
Account const& account = accounts.emplace_back(
409415
Account::base58Seed, std::string(seed));
410416
env.fund(XRP(10000), account);
411-
env.close();
417+
418+
// Do not close the ledger inside the loop. If
419+
// fixNFTokenRemint is enabled and accounts are initialized
420+
// at different ledgers, they will have different account
421+
// sequences. That would cause the accounts to have
422+
// different NFTokenID sequence numbers.
412423
}
424+
env.close();
413425

414426
// All of the accounts create one NFT and and offer that NFT to
415427
// buyer.
@@ -652,8 +664,14 @@ class NFTokenDir_test : public beast::unit_test::suite
652664
Account const& account =
653665
accounts.emplace_back(Account::base58Seed, std::string(seed));
654666
env.fund(XRP(10000), account);
655-
env.close();
667+
668+
// Do not close the ledger inside the loop. If
669+
// fixNFTokenRemint is enabled and accounts are initialized
670+
// at different ledgers, they will have different account
671+
// sequences. That would cause the accounts to have
672+
// different NFTokenID sequence numbers.
656673
}
674+
env.close();
657675

658676
// All of the accounts create one NFT and and offer that NFT to buyer.
659677
std::vector<uint256> nftIDs;
@@ -827,8 +845,14 @@ class NFTokenDir_test : public beast::unit_test::suite
827845
Account const& account =
828846
accounts.emplace_back(Account::base58Seed, std::string(seed));
829847
env.fund(XRP(10000), account);
830-
env.close();
848+
849+
// Do not close the ledger inside the loop. If
850+
// fixNFTokenRemint is enabled and accounts are initialized
851+
// at different ledgers, they will have different account
852+
// sequences. That would cause the accounts to have
853+
// different NFTokenID sequence numbers.
831854
}
855+
env.close();
832856

833857
// All of the accounts create seven consecutive NFTs and and offer
834858
// those NFTs to buyer.
@@ -1078,7 +1102,8 @@ class NFTokenDir_test : public beast::unit_test::suite
10781102
FeatureBitset const fixNFTDir{
10791103
fixNFTokenDirV1, featureNonFungibleTokensV1_1};
10801104

1081-
testWithFeats(all - fixNFTDir);
1105+
testWithFeats(all - fixNFTDir - fixNFTokenRemint);
1106+
testWithFeats(all - fixNFTokenRemint);
10821107
testWithFeats(all);
10831108
}
10841109
};

0 commit comments

Comments
 (0)