rippled
Loading...
Searching...
No Matches
NFTokenMint.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2021 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpld/app/tx/detail/NFTokenMint.h>
21
22#include <xrpl/basics/Expected.h>
23#include <xrpl/ledger/View.h>
24#include <xrpl/protocol/Feature.h>
25#include <xrpl/protocol/InnerObjectFormats.h>
26#include <xrpl/protocol/Rate.h>
27#include <xrpl/protocol/TxFlags.h>
28
29#include <boost/endian/conversion.hpp>
30
31#include <array>
32
33namespace ripple {
34
35static std::uint16_t
37{
38 return static_cast<std::uint16_t>(txFlags & 0x0000FFFF);
39}
40
41static bool
43{
44 return ctx.tx.isFieldPresent(sfAmount) ||
45 ctx.tx.isFieldPresent(sfDestination) ||
46 ctx.tx.isFieldPresent(sfExpiration);
47}
48
49bool
51{
52 return ctx.rules.enabled(featureNFTokenMintOffer) || !hasOfferFields(ctx);
53}
54
57{
58 // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between
59 // accounts allowed a TrustLine to be added to the issuer of that token
60 // without explicit permission from that issuer. This was enabled by
61 // minting the NFToken with the tfTrustLine flag set.
62 //
63 // That capability could be used to attack the NFToken issuer. It
64 // would be possible for two accounts to trade the NFToken back and forth
65 // building up any number of TrustLines on the issuer, increasing the
66 // issuer's reserve without bound.
67 //
68 // The fixRemoveNFTokenAutoTrustLine amendment disables minting with the
69 // tfTrustLine flag as a way to prevent the attack. But until the
70 // amendment passes we still need to keep the old behavior available.
71 std::uint32_t const nfTokenMintMask =
72 ctx.rules.enabled(fixRemoveNFTokenAutoTrustLine)
73 // if featureDynamicNFT enabled then new flag allowing mutable URI
74 // available
75 ? ctx.rules.enabled(featureDynamicNFT) ? tfNFTokenMintMaskWithMutable
77 : ctx.rules.enabled(featureDynamicNFT) ? tfNFTokenMintOldMaskWithMutable
79
80 return nfTokenMintMask;
81}
82
85{
86 if (auto const f = ctx.tx[~sfTransferFee])
87 {
88 if (f > maxTransferFee)
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->length() == 0 || uri->length() > maxTokenURILength)
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 "ripple::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];
194 minter != ctx.tx[sfAccount])
195 return tecNO_PERMISSION;
196 }
197
198 if (ctx.tx.isFieldPresent(sfAmount))
199 {
200 // The Amount field says create an offer for the minted token.
201 if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
202 return tecEXPIRED;
203
204 // Rely on the common code shared with NFTokenCreateOffer to
205 // do the validation. We pass tfSellNFToken as the transaction flags
206 // because a Mint is only allowed to create a sell offer.
207 if (TER const ter = nft::tokenOfferCreatePreclaim(
208 ctx.view,
209 ctx.tx[sfAccount],
210 ctx.tx[~sfIssuer].value_or(ctx.tx[sfAccount]),
211 ctx.tx[sfAmount],
212 ctx.tx[~sfDestination],
214 ctx.tx[~sfTransferFee].value_or(0),
215 ctx.j);
216 !isTesSuccess(ter))
217 return ter;
218 }
219 return tesSUCCESS;
220}
221
222TER
224{
225 auto const issuer = ctx_.tx[~sfIssuer].value_or(account_);
226
227 auto const tokenSeq = [this, &issuer]() -> Expected<std::uint32_t, TER> {
228 auto const root = view().peek(keylet::account(issuer));
229 if (root == nullptr)
230 // Should not happen. Checked in preclaim.
231 return Unexpected(tecNO_ISSUER);
232
233 // If the issuer hasn't minted an NFToken before we must add a
234 // FirstNFTokenSequence field to the issuer's AccountRoot. The
235 // value of the FirstNFTokenSequence must equal the issuer's
236 // current account sequence.
237 //
238 // There are three situations:
239 // o If the first token is being minted by the issuer and
240 // * If the transaction consumes a Sequence number, then the
241 // Sequence has been pre-incremented by the time we get here in
242 // doApply. We must decrement the value in the Sequence field.
243 // * Otherwise the transaction uses a Ticket so the Sequence has
244 // not been pre-incremented. We use the Sequence value as is.
245 // o The first token is being minted by an authorized minter. In
246 // this case the issuer's Sequence field has been left untouched.
247 // We use the issuer's Sequence value as is.
248 if (!root->isFieldPresent(sfFirstNFTokenSequence))
249 {
250 std::uint32_t const acctSeq = root->at(sfSequence);
251
252 root->at(sfFirstNFTokenSequence) =
253 ctx_.tx.isFieldPresent(sfIssuer) ||
255 ? acctSeq
256 : acctSeq - 1;
257 }
258
259 std::uint32_t const mintedNftCnt =
260 (*root)[~sfMintedNFTokens].value_or(0u);
261
262 (*root)[sfMintedNFTokens] = mintedNftCnt + 1u;
263 if ((*root)[sfMintedNFTokens] == 0u)
265
266 // Get the unique sequence number of this token by
267 // sfFirstNFTokenSequence + sfMintedNFTokens
268 std::uint32_t const offset = (*root)[sfFirstNFTokenSequence];
269 std::uint32_t const tokenSeq = offset + mintedNftCnt;
270
271 // Check for more overflow cases
272 if (tokenSeq + 1u == 0u || tokenSeq < offset)
274
275 ctx_.view().update(root);
276 return tokenSeq;
277 }();
278
279 if (!tokenSeq.has_value())
280 return (tokenSeq.error());
281
282 std::uint32_t const ownerCountBefore =
283 view().read(keylet::account(account_))->getFieldU32(sfOwnerCount);
284
285 // Assemble the new NFToken.
286 SOTemplate const* nfTokenTemplate =
288
289 if (nfTokenTemplate == nullptr)
290 // Should never happen.
291 return tecINTERNAL; // LCOV_EXCL_LINE
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(
301 *nfTokenTemplate, sfNFToken, [this, &nftokenID](STObject& object) {
302 object.setFieldH256(sfNFTokenID, nftokenID);
303
304 if (auto const uri = ctx_.tx[~sfURI])
305 object.setFieldVL(sfURI, *uri);
306 });
307
308 if (TER const ret =
309 nft::insertToken(ctx_.view(), account_, std::move(newToken));
310 ret != tesSUCCESS)
311 return ret;
312
313 if (ctx_.tx.isFieldPresent(sfAmount))
314 {
315 // Rely on the common code shared with NFTokenCreateOffer to create
316 // the offer. We pass tfSellNFToken as the transaction flags
317 // because a Mint is only allowed to create a sell offer.
318 if (TER const ter = nft::tokenOfferCreateApply(
319 view(),
320 ctx_.tx[sfAccount],
321 ctx_.tx[sfAmount],
322 ctx_.tx[~sfDestination],
323 ctx_.tx[~sfExpiration],
325 nftokenID,
327 j_);
328 !isTesSuccess(ter))
329 return ter;
330 }
331
332 // Only check the reserve if the owner count actually changed. This
333 // allows NFTs to be added to the page (and burn fees) without
334 // requiring the reserve to be met each time. The reserve is
335 // only managed when a new NFT page or sell offer is added.
336 if (auto const ownerCountAfter =
337 view().read(keylet::account(account_))->getFieldU32(sfOwnerCount);
338 ownerCountAfter > ownerCountBefore)
339 {
340 if (auto const reserve = view().fees().accountReserve(ownerCountAfter);
341 mPriorBalance < reserve)
343 }
344 return tesSUCCESS;
345}
346
347} // namespace ripple
ApplyView & view()
virtual void update(std::shared_ptr< SLE > const &sle)=0
Indicate changes to a peeked SLE.
virtual std::shared_ptr< SLE > peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
SOTemplate const * findSOTemplateBySField(SField const &sField) const
static InnerObjectFormats const & getInstance()
static NotTEC preflight(PreflightContext const &ctx)
static bool checkExtraFeatures(PreflightContext const &ctx)
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
static uint256 createNFTokenID(std::uint16_t flags, std::uint16_t fee, AccountID const &issuer, nft::Taxon taxon, std::uint32_t tokenSeq)
static TER preclaim(PreclaimContext const &ctx)
TER doApply() override
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
virtual Fees const & fees() const =0
Returns the fees for the base ledger.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:130
Defines the fields and their attributes within a STObject.
Definition SOTemplate.h:113
bool isFlag(std::uint32_t) const
Definition STObject.cpp:531
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:484
std::uint32_t getFlags() const
Definition STObject.cpp:537
SeqProxy getSeqProxy() const
Definition STTx.cpp:216
constexpr bool isTicket() const
Definition SeqProxy.h:94
AccountID const account_
Definition Transactor.h:147
ApplyView & view()
Definition Transactor.h:163
beast::Journal const j_
Definition Transactor.h:145
XRPAmount mPriorBalance
Definition Transactor.h:148
ApplyContext & ctx_
Definition Transactor.h:143
static base_uint fromVoid(void const *data)
Definition base_uint.h:319
static constexpr std::size_t size()
Definition base_uint.h:526
A type-safe wrap around standard integral types.
T data(T... args)
T distance(T... args)
T memcpy(T... args)
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:184
std::uint32_t toUInt32(Taxon t)
Definition nft.h:48
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::uint32_t txFlags)
Preflight checks shared by NFTokenCreateOffer and NFTokenMint.
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)
doApply implementation shared by NFTokenCreateOffer and NFTokenMint
Taxon cipheredTaxon(std::uint32_t tokenSeq, Taxon taxon)
Definition nft.h:84
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::uint32_t txFlags)
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:42
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
static bool hasOfferFields(PreflightContext const &ctx)
constexpr std::uint32_t const tfNFTokenMintOldMaskWithMutable
Definition TxFlags.h:223
constexpr std::uint32_t const tfNFTokenMintMaskWithMutable
Definition TxFlags.h:226
constexpr std::uint32_t const tfNFTokenMintOldMask
Definition TxFlags.h:219
std::uint16_t constexpr maxTransferFee
The maximum token transfer fee allowed.
Definition Protocol.h:85
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp)
Determines whether the given expiration time has passed.
Definition View.cpp:173
std::size_t constexpr maxTokenURILength
The maximum length of a URI inside an NFT.
Definition Protocol.h:88
@ tecNO_ISSUER
Definition TER.h:300
@ tecINTERNAL
Definition TER.h:311
@ tecNO_PERMISSION
Definition TER.h:306
@ tecMAX_SEQUENCE_REACHED
Definition TER.h:321
@ tecINSUFFICIENT_RESERVE
Definition TER.h:308
@ tecEXPIRED
Definition TER.h:315
@ tesSUCCESS
Definition TER.h:245
bool isTesSuccess(TER x) noexcept
Definition TER.h:678
Number root(Number f, unsigned d)
Definition Number.cpp:636
static std::uint16_t extractNFTokenFlagsFromTxFlags(std::uint32_t txFlags)
constexpr std::uint32_t const tfTransferable
Definition TxFlags.h:142
constexpr std::uint32_t const tfNFTokenMintMask
Definition TxFlags.h:216
@ temMALFORMED
Definition TER.h:87
@ temBAD_NFTOKEN_TRANSFER_FEE
Definition TER.h:127
XRPAmount accountReserve(std::size_t ownerCount) const
Returns the account reserve given the owner count, in drops.
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:80
ReadView const & view
Definition Transactor.h:83
beast::Journal const j
Definition Transactor.h:88
State information when preflighting a tx.
Definition Transactor.h:35