xrpld
Loading...
Searching...
No Matches
NFTokenMint.cpp
1#include <xrpl/tx/transactors/nft/NFTokenMint.h>
2
3#include <xrpl/basics/base_uint.h>
4#include <xrpl/beast/utility/instrumentation.h>
5#include <xrpl/ledger/View.h>
6#include <xrpl/ledger/helpers/NFTokenHelpers.h>
7#include <xrpl/protocol/AccountID.h>
8#include <xrpl/protocol/Feature.h>
9#include <xrpl/protocol/Indexes.h>
10#include <xrpl/protocol/InnerObjectFormats.h>
11#include <xrpl/protocol/Protocol.h>
12#include <xrpl/protocol/SField.h>
13#include <xrpl/protocol/SOTemplate.h>
14#include <xrpl/protocol/STLedgerEntry.h>
15#include <xrpl/protocol/STObject.h>
16#include <xrpl/protocol/STTx.h>
17#include <xrpl/protocol/TER.h>
18#include <xrpl/protocol/TxFlags.h>
19#include <xrpl/protocol/XRPAmount.h>
20#include <xrpl/protocol/nft.h>
21#include <xrpl/tx/Transactor.h>
22
23#include <boost/endian/conversion.hpp>
24
25#include <array>
26#include <cstdint>
27#include <cstring>
28#include <expected>
29#include <iterator> // IWYU pragma: keep
30#include <utility>
31
32namespace xrpl {
33
34static std::uint16_t
36{
37 return static_cast<std::uint16_t>(txFlags & 0x0000FFFF);
38}
39
40static bool
42{
43 return ctx.tx.isFieldPresent(sfAmount) || ctx.tx.isFieldPresent(sfDestination) ||
44 ctx.tx.isFieldPresent(sfExpiration);
45}
46
47bool
49{
50 return ctx.rules.enabled(featureNFTokenMintOffer) || !hasOfferFields(ctx);
51}
52
55{
56 // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between
57 // accounts allowed a TrustLine to be added to the issuer of that token
58 // without explicit permission from that issuer. This was enabled by
59 // minting the NFToken with the tfTrustLine flag set.
60 //
61 // That capability could be used to attack the NFToken issuer. It
62 // would be possible for two accounts to trade the NFToken back and forth
63 // building up any number of TrustLines on the issuer, increasing the
64 // issuer's reserve without bound.
65 //
66 // The fixRemoveNFTokenAutoTrustLine amendment disables minting with the
67 // tfTrustLine flag as a way to prevent the attack. But until the
68 // amendment passes we still need to keep the old behavior available.
69 std::uint32_t const nfTokenMintMask = [&]() -> std::uint32_t {
70 if (ctx.rules.enabled(fixRemoveNFTokenAutoTrustLine))
71 {
72 // if featureDynamicNFT enabled then new flag allowing mutable URI available
73 return ctx.rules.enabled(featureDynamicNFT) ? tfNFTokenMintMask
75 }
76 return ctx.rules.enabled(featureDynamicNFT) ? tfNFTokenMintOldMaskWithMutable
78 }();
79
80 return nfTokenMintMask;
81}
82
85{
86 if (auto const f = ctx.tx[~sfTransferFee])
87 {
88 if (f > kMaxTransferFee)
90
91 // If a non-zero TransferFee is set then the tfTransferable flag
92 // must also be set.
93 if (f > 0u && !ctx.tx.isFlag(tfTransferable))
94 return temMALFORMED;
95 }
96
97 // An issuer must only be set if the tx is executed by the minter
98 if (auto iss = ctx.tx[~sfIssuer]; iss == ctx.tx[sfAccount])
99 return temMALFORMED;
100
101 if (auto uri = ctx.tx[~sfURI])
102 {
103 if (uri->empty() || uri->length() > kMaxTokenUriLength)
104 return temMALFORMED;
105 }
106
107 if (hasOfferFields(ctx))
108 {
109 // The Amount field must be present if either the Destination or
110 // Expiration fields are present.
111 if (!ctx.tx.isFieldPresent(sfAmount))
112 return temMALFORMED;
113
114 // Rely on the common code shared with NFTokenCreateOffer to
115 // do the validation. We pass tfSellNFToken as the transaction flags
116 // because a Mint is only allowed to create a sell offer.
118 ctx.tx[sfAccount],
119 ctx.tx[sfAmount],
120 ctx.tx[~sfDestination],
121 ctx.tx[~sfExpiration],
123 ctx.rules);
124 !isTesSuccess(notTec))
125 {
126 return notTec;
127 }
128 }
129
130 return tesSUCCESS;
131}
132
135 std::uint16_t flags,
136 std::uint16_t fee,
137 AccountID const& issuer,
138 nft::Taxon taxon,
139 std::uint32_t tokenSeq)
140{
141 // An issuer may issue several NFTs with the same taxon; to ensure that NFTs
142 // are spread across multiple pages we lightly mix the taxon up by using the
143 // sequence (which is not under the issuer's direct control) as the seed for
144 // a simple linear congruential generator. cipheredTaxon() does this work.
145 taxon = nft::cipheredTaxon(tokenSeq, taxon);
146
147 // The values are packed inside a 32-byte buffer, so we need to make sure
148 // that the endianess is fixed.
149 flags = boost::endian::native_to_big(flags);
150 fee = boost::endian::native_to_big(fee);
151 taxon = nft::toTaxon(boost::endian::native_to_big(nft::toUInt32(taxon)));
152 tokenSeq = boost::endian::native_to_big(tokenSeq);
153
155
156 auto ptr = buf.data();
157
158 // This code is awkward but the idea is to pack these values into a single
159 // 256-bit value that uniquely identifies this NFT.
160 std::memcpy(ptr, &flags, sizeof(flags));
161 ptr += sizeof(flags);
162
163 std::memcpy(ptr, &fee, sizeof(fee));
164 ptr += sizeof(fee);
165
166 std::memcpy(ptr, issuer.data(), issuer.size());
167 ptr += issuer.size();
168
169 std::memcpy(ptr, &taxon, sizeof(taxon));
170 ptr += sizeof(taxon);
171
172 std::memcpy(ptr, &tokenSeq, sizeof(tokenSeq));
173 ptr += sizeof(tokenSeq);
174 XRPL_ASSERT(
175 std::distance(buf.data(), ptr) == buf.size(),
176 "xrpl::NFTokenMint::createNFTokenID : data size matches the buffer");
177
178 return uint256::fromVoid(buf.data());
179}
180
181TER
183{
184 // The issuer of the NFT may or may not be the account executing this
185 // transaction. Check that and verify that this is allowed:
186 if (auto issuer = ctx.tx[~sfIssuer])
187 {
188 auto const sle = ctx.view.read(keylet::account(*issuer));
189
190 if (!sle)
191 return tecNO_ISSUER;
192
193 if (auto const minter = (*sle)[~sfNFTokenMinter]; minter != ctx.tx[sfAccount])
194 return tecNO_PERMISSION;
195 }
196
197 if (ctx.tx.isFieldPresent(sfAmount))
198 {
199 // The Amount field says create an offer for the minted token.
200 if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
201 return tecEXPIRED;
202
203 // Rely on the common code shared with NFTokenCreateOffer to
204 // do the validation. We pass tfSellNFToken as the transaction flags
205 // because a Mint is only allowed to create a sell offer.
206 if (TER const ter = nft::tokenOfferCreatePreclaim(
207 ctx.view,
208 ctx.tx[sfAccount],
209 ctx.tx[~sfIssuer].value_or(ctx.tx[sfAccount]),
210 ctx.tx[sfAmount],
211 ctx.tx[~sfDestination],
213 ctx.tx[~sfTransferFee].value_or(0),
214 ctx.j);
215 !isTesSuccess(ter))
216 return ter;
217 }
218 return tesSUCCESS;
219}
220
221TER
223{
224 auto const issuer = ctx_.tx[~sfIssuer].value_or(accountID_);
225
226 auto const tokenSeq = [this, &issuer]() -> std::expected<std::uint32_t, TER> {
227 auto const root = view().peek(keylet::account(issuer));
228 if (root == nullptr)
229 {
230 // Should not happen. Checked in preclaim.
232 }
233
234 // If the issuer hasn't minted an NFToken before we must add a
235 // FirstNFTokenSequence field to the issuer's AccountRoot. The
236 // value of the FirstNFTokenSequence must equal the issuer's
237 // current account sequence.
238 //
239 // There are three situations:
240 // o If the first token is being minted by the issuer and
241 // * If the transaction consumes a Sequence number, then the
242 // Sequence has been pre-incremented by the time we get here in
243 // doApply. We must decrement the value in the Sequence field.
244 // * Otherwise the transaction uses a Ticket so the Sequence has
245 // not been pre-incremented. We use the Sequence value as is.
246 // o The first token is being minted by an authorized minter. In
247 // this case the issuer's Sequence field has been left untouched.
248 // We use the issuer's Sequence value as is.
249 if (!root->isFieldPresent(sfFirstNFTokenSequence))
250 {
251 std::uint32_t const acctSeq = root->at(sfSequence);
252
253 root->at(sfFirstNFTokenSequence) =
254 ctx_.tx.isFieldPresent(sfIssuer) || ctx_.tx.getSeqProxy().isTicket() ? acctSeq
255 : acctSeq - 1;
256 }
257
258 std::uint32_t const mintedNftCnt = (*root)[~sfMintedNFTokens].valueOr(0u);
259
260 (*root)[sfMintedNFTokens] = mintedNftCnt + 1u;
261 if ((*root)[sfMintedNFTokens] == 0u)
263
264 // Get the unique sequence number of this token by
265 // sfFirstNFTokenSequence + sfMintedNFTokens
266 std::uint32_t const offset = (*root)[sfFirstNFTokenSequence];
267 std::uint32_t const tokenSeq = offset + mintedNftCnt;
268
269 // Check for more overflow cases
270 if (tokenSeq + 1u == 0u || tokenSeq < offset)
272
273 ctx_.view().update(root);
274 return tokenSeq;
275 }();
276
277 if (!tokenSeq.has_value())
278 return (tokenSeq.error());
279
280 std::uint32_t const ownerCountBefore =
281 view().read(keylet::account(accountID_))->getFieldU32(sfOwnerCount);
282
283 // Assemble the new NFToken.
284 SOTemplate const* nfTokenTemplate =
286
287 if (nfTokenTemplate == nullptr)
288 {
289 // Should never happen.
290 return tecINTERNAL; // LCOV_EXCL_LINE
291 }
292
293 auto const nftokenID = createNFTokenID(
295 ctx_.tx[~sfTransferFee].value_or(0),
296 issuer,
297 nft::toTaxon(ctx_.tx[sfNFTokenTaxon]),
298 tokenSeq.value());
299
300 STObject newToken(*nfTokenTemplate, sfNFToken, [this, &nftokenID](STObject& object) {
301 object.setFieldH256(sfNFTokenID, nftokenID);
302
303 if (auto const uri = ctx_.tx[~sfURI])
304 object.setFieldVL(sfURI, *uri);
305 });
306
307 if (TER const ret = nft::insertToken(ctx_.view(), accountID_, std::move(newToken));
308 !isTesSuccess(ret))
309 return ret;
310
311 if (ctx_.tx.isFieldPresent(sfAmount))
312 {
313 // Rely on the common code shared with NFTokenCreateOffer to create
314 // the offer. We pass tfSellNFToken as the transaction flags
315 // because a Mint is only allowed to create a sell offer.
316 if (TER const ter = nft::tokenOfferCreateApply(
317 view(),
318 ctx_.tx[sfAccount],
319 ctx_.tx[sfAmount],
320 ctx_.tx[~sfDestination],
321 ctx_.tx[~sfExpiration],
322 ctx_.tx.getSeqProxy(),
323 nftokenID,
325 j_);
326 !isTesSuccess(ter))
327 return ter;
328 }
329
330 // Only check the reserve if the owner count actually changed. This
331 // allows NFTs to be added to the page (and burn fees) without
332 // requiring the reserve to be met each time. The reserve is
333 // only managed when a new NFT page or sell offer is added.
334 if (auto const ownerCountAfter =
335 view().read(keylet::account(accountID_))->getFieldU32(sfOwnerCount);
336 ownerCountAfter > ownerCountBefore)
337 {
338 if (auto const reserve = view().fees().accountReserve(ownerCountAfter);
339 preFeeBalance_ < reserve)
341 }
342 return tesSUCCESS;
343}
344
345void
347{
348 // No transaction-specific invariants yet (future work).
349}
350
351bool
353{
354 // No transaction-specific invariants yet (future work).
355 return true;
356}
357
358} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:38
virtual SLE::pointer peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
static BaseUInt fromVoid(void const *data)
Definition base_uint.h:322
pointer data()
Definition base_uint.h:106
static constexpr std::size_t size()
Definition base_uint.h:530
SOTemplate const * findSOTemplateBySField(SField const &sField) const
static InnerObjectFormats const & getInstance()
static uint256 createNFTokenID(std::uint16_t flags, std::uint16_t fee, AccountID const &issuer, nft::Taxon taxon, std::uint32_t tokenSeq)
bool finalizeInvariants(STTx const &tx, TER result, XRPAmount fee, ReadView const &view, beast::Journal const &j) override
Check transaction-specific post-conditions after all entries have been visited.
static NotTEC preflight(PreflightContext const &ctx)
void visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override
Inspect a single ledger entry modified by this transaction.
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
TER doApply() override
static TER preclaim(PreclaimContext const &ctx)
static bool checkExtraFeatures(PreflightContext const &ctx)
A view into a ledger.
Definition ReadView.h:31
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:171
Defines the fields and their attributes within a STObject.
Definition SOTemplate.h:96
std::shared_ptr< STLedgerEntry const > const & const_ref
bool isFlag(std::uint32_t) const
Definition STObject.cpp:501
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:454
std::uint32_t getFlags() const
Definition STObject.cpp:507
beast::Journal const j_
Definition Transactor.h:118
ApplyView & view()
Definition Transactor.h:136
AccountID const accountID_
Definition Transactor.h:120
XRPAmount preFeeBalance_
Definition Transactor.h:121
ApplyContext & ctx_
Definition Transactor.h:116
T data(T... args)
T distance(T... args)
T memcpy(T... args)
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
std::uint32_t toUInt32(Taxon t)
Definition nft.h:27
TER tokenOfferCreatePreclaim(ReadView const &view, AccountID const &acctID, AccountID const &nftIssuer, STAmount const &amount, std::optional< AccountID > const &dest, std::uint16_t nftFlags, std::uint16_t xferFee, beast::Journal j, std::optional< AccountID > const &owner=std::nullopt, std::uint32_t txFlags=tfSellNFToken)
Preclaim checks shared by NFTokenCreateOffer and NFTokenMint.
TER insertToken(ApplyView &view, AccountID owner, STObject &&nft)
Insert the token in the owner's token directory.
Taxon toTaxon(std::uint32_t i)
Definition nft.h:21
Taxon cipheredTaxon(std::uint32_t tokenSeq, Taxon taxon)
Definition nft.h:63
TER tokenOfferCreateApply(ApplyView &view, AccountID const &acctID, STAmount const &amount, std::optional< AccountID > const &dest, std::optional< std::uint32_t > const &expiration, SeqProxy seqProxy, uint256 const &nftokenID, XRPAmount const &priorBalance, beast::Journal j, std::uint32_t txFlags=tfSellNFToken)
doApply implementation shared by NFTokenCreateOffer and NFTokenMint
NotTEC tokenOfferCreatePreflight(AccountID const &acctID, STAmount const &amount, std::optional< AccountID > const &dest, std::optional< std::uint32_t > const &expiration, std::uint16_t nftFlags, Rules const &rules, std::optional< AccountID > const &owner=std::nullopt, std::uint32_t txFlags=tfSellNFToken)
Preflight checks shared by NFTokenCreateOffer and NFTokenMint.
TaggedInteger< std::uint32_t, TaxonTag > Taxon
Definition nft.h:18
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp)
Determines whether the given expiration time has passed.
Definition View.cpp:47
constexpr std::size_t kMaxTokenUriLength
The maximum length of a URI inside an NFT.
Definition Protocol.h:207
constexpr FlagValue tfNFTokenMintMaskWithoutMutable
Definition TxFlags.h:388
constexpr FlagValue tfNFTokenMintOldMaskWithMutable
Definition TxFlags.h:394
static bool hasOfferFields(PreflightContext const &ctx)
Number root(Number f, unsigned d)
Definition Number.cpp:1201
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:594
constexpr FlagValue tfNFTokenMintOldMask
Definition TxFlags.h:391
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
constexpr std::uint16_t kMaxTransferFee
The maximum token transfer fee allowed.
Definition Protocol.h:70
@ temMALFORMED
Definition TER.h:73
@ temBAD_NFTOKEN_TRANSFER_FEE
Definition TER.h:113
bool isTesSuccess(TER x) noexcept
Definition TER.h:663
TERSubset< CanCvtToTER > TER
Definition TER.h:634
@ tecINTERNAL
Definition TER.h:308
@ tecEXPIRED
Definition TER.h:312
@ tecINSUFFICIENT_RESERVE
Definition TER.h:305
@ tecMAX_SEQUENCE_REACHED
Definition TER.h:318
@ tecNO_PERMISSION
Definition TER.h:303
@ tecNO_ISSUER
Definition TER.h:297
BaseUInt< 256 > uint256
Definition base_uint.h:562
static std::uint16_t extractNFTokenFlagsFromTxFlags(std::uint32_t txFlags)
@ tesSUCCESS
Definition TER.h:240
T size(T... args)
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:61
ReadView const & view
Definition Transactor.h:64
beast::Journal const j
Definition Transactor.h:69
State information when preflighting a tx.
Definition Transactor.h:18
T unexpected(T... args)