rippled
Loading...
Searching...
No Matches
NFTokenAcceptOffer.cpp
1#include <xrpld/app/tx/detail/NFTokenAcceptOffer.h>
2#include <xrpld/app/tx/detail/NFTokenUtils.h>
3
4#include <xrpl/ledger/View.h>
5#include <xrpl/protocol/Feature.h>
6#include <xrpl/protocol/Rate.h>
7#include <xrpl/protocol/TxFlags.h>
8
9namespace ripple {
10
16
19{
20 auto const bo = ctx.tx[~sfNFTokenBuyOffer];
21 auto const so = ctx.tx[~sfNFTokenSellOffer];
22
23 // At least one of these MUST be specified
24 if (!bo && !so)
25 return temMALFORMED;
26
27 // The `BrokerFee` field must not be present in direct mode but may be
28 // present and greater than zero in brokered mode.
29 if (auto const bf = ctx.tx[~sfNFTokenBrokerFee])
30 {
31 if (!bo || !so)
32 return temMALFORMED;
33
34 if (*bf <= beast::zero)
35 return temMALFORMED;
36 }
37
38 return tesSUCCESS;
39}
40
41TER
43{
44 auto const checkOffer = [&ctx](std::optional<uint256> id)
46 if (id)
47 {
48 if (id->isZero())
49 return {nullptr, tecOBJECT_NOT_FOUND};
50
51 auto offerSLE = ctx.view.read(keylet::nftoffer(*id));
52
53 if (!offerSLE)
54 return {nullptr, tecOBJECT_NOT_FOUND};
55
56 if (hasExpired(ctx.view, (*offerSLE)[~sfExpiration]))
57 return {nullptr, tecEXPIRED};
58
59 if ((*offerSLE)[sfAmount].negative())
60 return {nullptr, temBAD_OFFER};
61
62 return {std::move(offerSLE), tesSUCCESS};
63 }
64 return {nullptr, tesSUCCESS};
65 };
66
67 auto const [bo, err1] = checkOffer(ctx.tx[~sfNFTokenBuyOffer]);
68 if (!isTesSuccess(err1))
69 return err1;
70 auto const [so, err2] = checkOffer(ctx.tx[~sfNFTokenSellOffer]);
71 if (!isTesSuccess(err2))
72 return err2;
73
74 if (bo && so)
75 {
76 // Brokered mode:
77 // The two offers being brokered must be for the same token:
78 if ((*bo)[sfNFTokenID] != (*so)[sfNFTokenID])
80
81 // The two offers being brokered must be for the same asset:
82 if ((*bo)[sfAmount].issue() != (*so)[sfAmount].issue())
84
85 // The two offers may not form a loop. A broker may not sell the
86 // token to the current owner of the token.
87 if (((*bo)[sfOwner] == (*so)[sfOwner]))
89
90 // Ensure that the buyer is willing to pay at least as much as the
91 // seller is requesting:
92 if ((*so)[sfAmount] > (*bo)[sfAmount])
94
95 // The destination must be whoever is submitting the tx if the buyer
96 // specified it
97 if (auto const dest = bo->at(~sfDestination);
98 dest && *dest != ctx.tx[sfAccount])
99 {
100 return tecNO_PERMISSION;
101 }
102
103 // The destination must be whoever is submitting the tx if the seller
104 // specified it
105 if (auto const dest = so->at(~sfDestination);
106 dest && *dest != ctx.tx[sfAccount])
107 {
108 return tecNO_PERMISSION;
109 }
110
111 // The broker can specify an amount that represents their cut; if they
112 // have, ensure that the seller will get at least as much as they want
113 // to get *after* this fee is accounted for (but before the issuer's
114 // cut, if any).
115 if (auto const brokerFee = ctx.tx[~sfNFTokenBrokerFee])
116 {
117 if (brokerFee->issue() != (*bo)[sfAmount].issue())
119
120 if (brokerFee >= (*bo)[sfAmount])
122
123 if ((*so)[sfAmount] > (*bo)[sfAmount] - *brokerFee)
125
126 // Check if broker is allowed to receive the fee with these IOUs.
127 if (!brokerFee->native() &&
128 ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2))
129 {
131 ctx.view,
132 ctx.tx[sfAccount],
133 ctx.j,
134 brokerFee->asset().get<Issue>());
135 if (res != tesSUCCESS)
136 return res;
137
139 ctx.view,
140 ctx.tx[sfAccount],
141 ctx.j,
142 brokerFee->asset().get<Issue>());
143 if (res != tesSUCCESS)
144 return res;
145 }
146 }
147 }
148
149 if (bo)
150 {
151 if (((*bo)[sfFlags] & lsfSellNFToken) == lsfSellNFToken)
153
154 // An account can't accept an offer it placed:
155 if ((*bo)[sfOwner] == ctx.tx[sfAccount])
157
158 // If not in bridged mode, the account must own the token:
159 if (!so &&
160 !nft::findToken(ctx.view, ctx.tx[sfAccount], (*bo)[sfNFTokenID]))
161 return tecNO_PERMISSION;
162
163 // If not in bridged mode...
164 if (!so)
165 {
166 // If the offer has a Destination field, the acceptor must be the
167 // Destination.
168 if (auto const dest = bo->at(~sfDestination);
169 dest.has_value() && *dest != ctx.tx[sfAccount])
170 return tecNO_PERMISSION;
171 }
172
173 // The account offering to buy must have funds:
174 //
175 // After this amendment, we allow an IOU issuer to buy an NFT with their
176 // own currency
177 auto const needed = bo->at(sfAmount);
178
179 if (accountFunds(
180 ctx.view, (*bo)[sfOwner], needed, fhZERO_IF_FROZEN, ctx.j) <
181 needed)
183
184 // Check that the account accepting the buy offer (he's selling the NFT)
185 // is allowed to receive IOUs. Also check that this offer's creator is
186 // authorized. But we need to exclude the case when the transaction is
187 // created by the broker.
188 if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2) &&
189 !needed.native())
190 {
192 ctx.view, bo->at(sfOwner), ctx.j, needed.asset().get<Issue>());
193 if (res != tesSUCCESS)
194 return res;
195
196 if (!so)
197 {
199 ctx.view,
200 ctx.tx[sfAccount],
201 ctx.j,
202 needed.asset().get<Issue>());
203 if (res != tesSUCCESS)
204 return res;
205
207 ctx.view,
208 ctx.tx[sfAccount],
209 ctx.j,
210 needed.asset().get<Issue>());
211 if (res != tesSUCCESS)
212 return res;
213 }
214 }
215 }
216
217 if (so)
218 {
219 if (((*so)[sfFlags] & lsfSellNFToken) != lsfSellNFToken)
221
222 // An account can't accept an offer it placed:
223 if ((*so)[sfOwner] == ctx.tx[sfAccount])
225
226 // The seller must own the token.
227 if (!nft::findToken(ctx.view, (*so)[sfOwner], (*so)[sfNFTokenID]))
228 return tecNO_PERMISSION;
229
230 // If not in bridged mode...
231 if (!bo)
232 {
233 // If the offer has a Destination field, the acceptor must be the
234 // Destination.
235 if (auto const dest = so->at(~sfDestination);
236 dest.has_value() && *dest != ctx.tx[sfAccount])
237 return tecNO_PERMISSION;
238 }
239
240 // The account offering to buy must have funds:
241 auto const needed = so->at(sfAmount);
242 if (!bo)
243 {
244 // After this amendment, we allow buyers to buy with their own
245 // issued currency.
246 //
247 // In the case of brokered mode, this check is essentially
248 // redundant, since we have already confirmed that buy offer is >
249 // than the sell offer, and that the buyer can cover the buy
250 // offer.
251 //
252 // We also _must not_ check the tx submitter in brokered
253 // mode, because then we are confirming that the broker can
254 // cover what the buyer will pay, which doesn't make sense, causes
255 // an unnecessary tec, and is also resolved with this amendment.
256 if (accountFunds(
257 ctx.view,
258 ctx.tx[sfAccount],
259 needed,
261 ctx.j) < needed)
263 }
264
265 // Make sure that we are allowed to hold what the taker will pay us.
266 if (!needed.native())
267 {
268 if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2))
269 {
271 ctx.view,
272 (*so)[sfOwner],
273 ctx.j,
274 needed.asset().get<Issue>());
275 if (res != tesSUCCESS)
276 return res;
277
278 if (!bo)
279 {
281 ctx.view,
282 ctx.tx[sfAccount],
283 ctx.j,
284 needed.asset().get<Issue>());
285 if (res != tesSUCCESS)
286 return res;
287 }
288 }
289
290 auto const res = nft::checkTrustlineDeepFrozen(
291 ctx.view, (*so)[sfOwner], ctx.j, needed.asset().get<Issue>());
292 if (res != tesSUCCESS)
293 return res;
294 }
295 }
296
297 // Additional checks are required in case a minter set a transfer fee for
298 // this nftoken
299 auto const& offer = bo ? bo : so;
300 if (!offer)
301 // Purely defensive, should be caught in preflight.
302 return tecINTERNAL; // LCOV_EXCL_LINE
303
304 auto const& tokenID = offer->at(sfNFTokenID);
305 auto const& amount = offer->at(sfAmount);
306 auto const nftMinter = nft::getIssuer(tokenID);
307
308 if (nft::getTransferFee(tokenID) != 0 && !amount.native())
309 {
310 // Fix a bug where the transfer of an NFToken with a transfer fee could
311 // give the NFToken issuer an undesired trust line.
312 // Issuer doesn't need a trust line to accept their own currency.
313 if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline) &&
314 (nft::getFlags(tokenID) & nft::flagCreateTrustLines) == 0 &&
315 nftMinter != amount.getIssuer() &&
316 !ctx.view.read(keylet::line(nftMinter, amount.issue())))
317 return tecNO_LINE;
318
319 // Check that the issuer is allowed to receive IOUs.
320 if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2))
321 {
323 ctx.view, nftMinter, ctx.j, amount.asset().get<Issue>());
324 if (res != tesSUCCESS)
325 return res;
326
328 ctx.view, nftMinter, ctx.j, amount.asset().get<Issue>());
329 if (res != tesSUCCESS)
330 return res;
331 }
332 }
333
334 return tesSUCCESS;
335}
336
337TER
339 AccountID const& from,
340 AccountID const& to,
341 STAmount const& amount)
342{
343 // This should never happen, but it's easy and quick to check.
344 if (amount < beast::zero)
345 return tecINTERNAL;
346
347 auto const result = accountSend(view(), from, to, amount, j_);
348
349 // If any payment causes a non-IOU-issuer to have a negative balance,
350 // or an IOU-issuer to have a positive balance in their own currency,
351 // we know that something went wrong. This was originally found in the
352 // context of IOU transfer fees. Since there are several payouts in this tx,
353 // just confirm that the end state is OK.
354 if (result != tesSUCCESS)
355 return result;
356 if (accountFunds(view(), from, amount, fhZERO_IF_FROZEN, j_).signum() < 0)
358 if (accountFunds(view(), to, amount, fhZERO_IF_FROZEN, j_).signum() < 0)
360 return tesSUCCESS;
361}
362
363TER
365 AccountID const& buyer,
366 AccountID const& seller,
367 uint256 const& nftokenID)
368{
369 auto tokenAndPage = nft::findTokenAndPage(view(), seller, nftokenID);
370
371 if (!tokenAndPage)
372 return tecINTERNAL; // LCOV_EXCL_LINE
373
374 if (auto const ret = nft::removeToken(
375 view(), seller, nftokenID, std::move(tokenAndPage->page));
376 !isTesSuccess(ret))
377 return ret;
378
379 auto const sleBuyer = view().read(keylet::account(buyer));
380 if (!sleBuyer)
381 return tecINTERNAL; // LCOV_EXCL_LINE
382
383 std::uint32_t const buyerOwnerCountBefore =
384 sleBuyer->getFieldU32(sfOwnerCount);
385
386 auto const insertRet =
387 nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
388
389 // if fixNFTokenReserve is enabled, check if the buyer has sufficient
390 // reserve to own a new object, if their OwnerCount changed.
391 //
392 // There was an issue where the buyer accepts a sell offer, the ledger
393 // didn't check if the buyer has enough reserve, meaning that buyer can get
394 // NFTs free of reserve.
395 if (view().rules().enabled(fixNFTokenReserve))
396 {
397 // To check if there is sufficient reserve, we cannot use mPriorBalance
398 // because NFT is sold for a price. So we must use the balance after
399 // the deduction of the potential offer price. A small caveat here is
400 // that the balance has already deducted the transaction fee, meaning
401 // that the reserve requirement is a few drops higher.
402 auto const buyerBalance = sleBuyer->getFieldAmount(sfBalance);
403
404 auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount);
405 if (buyerOwnerCountAfter > buyerOwnerCountBefore)
406 {
407 if (auto const reserve =
408 view().fees().accountReserve(buyerOwnerCountAfter);
409 buyerBalance < reserve)
411 }
412 }
413
414 return insertRet;
415}
416
417TER
419{
420 bool const isSell = offer->isFlag(lsfSellNFToken);
421 AccountID const owner = (*offer)[sfOwner];
422 AccountID const& seller = isSell ? owner : account_;
423 AccountID const& buyer = isSell ? account_ : owner;
424
425 auto const nftokenID = (*offer)[sfNFTokenID];
426
427 if (auto amount = offer->getFieldAmount(sfAmount); amount != beast::zero)
428 {
429 // Calculate the issuer's cut from this sale, if any:
430 if (auto const fee = nft::getTransferFee(nftokenID); fee != 0)
431 {
432 auto const cut = multiply(amount, nft::transferFeeAsRate(fee));
433
434 if (auto const issuer = nft::getIssuer(nftokenID);
435 cut != beast::zero && seller != issuer && buyer != issuer)
436 {
437 if (auto const r = pay(buyer, issuer, cut); !isTesSuccess(r))
438 return r;
439 amount -= cut;
440 }
441 }
442
443 // Send the remaining funds to the seller of the NFT
444 if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
445 return r;
446 }
447
448 // Now transfer the NFT:
449 return transferNFToken(buyer, seller, nftokenID);
450}
451
452TER
454{
455 auto const loadToken = [this](std::optional<uint256> const& id) {
457 if (id)
458 sle = view().peek(keylet::nftoffer(*id));
459 return sle;
460 };
461
462 auto bo = loadToken(ctx_.tx[~sfNFTokenBuyOffer]);
463 auto so = loadToken(ctx_.tx[~sfNFTokenSellOffer]);
464
465 if (bo && !nft::deleteTokenOffer(view(), bo))
466 {
467 // LCOV_EXCL_START
468 JLOG(j_.fatal()) << "Unable to delete buy offer '"
469 << to_string(bo->key()) << "': ignoring";
470 return tecINTERNAL;
471 // LCOV_EXCL_STOP
472 }
473
474 if (so && !nft::deleteTokenOffer(view(), so))
475 {
476 // LCOV_EXCL_START
477 JLOG(j_.fatal()) << "Unable to delete sell offer '"
478 << to_string(so->key()) << "': ignoring";
479 return tecINTERNAL;
480 // LCOV_EXCL_STOP
481 }
482
483 // Bridging two different offers
484 if (bo && so)
485 {
486 AccountID const buyer = (*bo)[sfOwner];
487 AccountID const seller = (*so)[sfOwner];
488
489 auto const nftokenID = (*so)[sfNFTokenID];
490
491 // The amount is what the buyer of the NFT pays:
492 STAmount amount = (*bo)[sfAmount];
493
494 // Three different folks may be paid. The order of operations is
495 // important.
496 //
497 // o The broker is paid the cut they requested.
498 // o The issuer's cut is calculated from what remains after the
499 // broker is paid. The issuer can take up to 50% of the remainder.
500 // o Finally, the seller gets whatever is left.
501 //
502 // It is important that the issuer's cut be calculated after the
503 // broker's portion is already removed. Calculating the issuer's
504 // cut before the broker's cut is removed can result in more money
505 // being paid out than the seller authorized. That would be bad!
506
507 // Send the broker the amount they requested.
508 if (auto const cut = ctx_.tx[~sfNFTokenBrokerFee];
509 cut && cut.value() != beast::zero)
510 {
511 if (auto const r = pay(buyer, account_, cut.value());
512 !isTesSuccess(r))
513 return r;
514
515 amount -= cut.value();
516 }
517
518 // Calculate the issuer's cut, if any.
519 if (auto const fee = nft::getTransferFee(nftokenID);
520 amount != beast::zero && fee != 0)
521 {
522 auto cut = multiply(amount, nft::transferFeeAsRate(fee));
523
524 if (auto const issuer = nft::getIssuer(nftokenID);
525 seller != issuer && buyer != issuer)
526 {
527 if (auto const r = pay(buyer, issuer, cut); !isTesSuccess(r))
528 return r;
529
530 amount -= cut;
531 }
532 }
533
534 // And send whatever remains to the seller.
535 if (amount > beast::zero)
536 {
537 if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
538 return r;
539 }
540
541 // Now transfer the NFT:
542 return transferNFToken(buyer, seller, nftokenID);
543 }
544
545 if (bo)
546 return acceptOffer(bo);
547
548 if (so)
549 return acceptOffer(so);
550
551 return tecINTERNAL; // LCOV_EXCL_LINE
552}
553
554} // namespace ripple
Stream fatal() const
Definition Journal.h:333
virtual std::shared_ptr< SLE > peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
A currency issued by an account.
Definition Issue.h:14
TER acceptOffer(std::shared_ptr< SLE > const &offer)
static TER preclaim(PreclaimContext const &ctx)
TER pay(AccountID const &from, AccountID const &to, STAmount const &amount)
TER transferNFToken(AccountID const &buyer, AccountID const &seller, uint256 const &nfTokenID)
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
static NotTEC preflight(PreflightContext const &ctx)
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
virtual Rules const & rules() const =0
Returns the tx processing rules.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:111
STAmount const & value() const noexcept
Definition STAmount.h:575
AccountID const account_
Definition Transactor.h:128
ApplyView & view()
Definition Transactor.h:144
beast::Journal const j_
Definition Transactor.h:126
ApplyContext & ctx_
Definition Transactor.h:124
Keylet line(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:225
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:165
Keylet nftoffer(AccountID const &owner, std::uint32_t seq)
An offer from an account to buy or sell an NFT.
Definition Indexes.cpp:408
std::uint16_t getTransferFee(uint256 const &id)
Definition nft.h:49
std::uint16_t getFlags(uint256 const &id)
Definition nft.h:41
TER removeToken(ApplyView &view, AccountID const &owner, uint256 const &nftokenID)
Remove the token from the owner's token directory.
AccountID getIssuer(uint256 const &id)
Definition nft.h:101
TER checkTrustlineDeepFrozen(ReadView const &view, AccountID const id, beast::Journal const j, Issue const &issue)
bool deleteTokenOffer(ApplyView &view, std::shared_ptr< SLE > const &offer)
Deletes the given token offer.
std::optional< TokenAndPage > findTokenAndPage(ApplyView &view, AccountID const &owner, uint256 const &nftokenID)
TER insertToken(ApplyView &view, AccountID owner, STObject &&nft)
Insert the token in the owner's token directory.
std::optional< STObject > findToken(ReadView const &view, AccountID const &owner, uint256 const &nftokenID)
Finds the specified token in the owner's token directory.
constexpr std::uint16_t const flagCreateTrustLines
Definition nft.h:36
TER checkTrustlineAuthorized(ReadView const &view, AccountID const id, beast::Journal const j, Issue const &issue)
Rate transferFeeAsRate(std::uint16_t fee)
Given a transfer fee (in basis points) convert it to a transfer rate.
Definition Rate2.cpp:26
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
STAmount accountFunds(ReadView const &view, AccountID const &id, STAmount const &saDefault, FreezeHandling freezeHandling, beast::Journal j)
Definition View.cpp:535
@ fhZERO_IF_FROZEN
Definition View.h:58
STAmount multiply(STAmount const &amount, Rate const &rate)
Definition Rate2.cpp:34
TER accountSend(ApplyView &view, AccountID const &from, AccountID const &to, STAmount const &saAmount, beast::Journal j, WaiveTransferFee waiveFee=WaiveTransferFee::No)
Calls static accountSendIOU if saAmount represents Issue.
Definition View.cpp:2172
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp)
Determines whether the given expiration time has passed.
Definition View.cpp:154
@ tecOBJECT_NOT_FOUND
Definition TER.h:308
@ tecNFTOKEN_OFFER_TYPE_MISMATCH
Definition TER.h:305
@ tecINSUFFICIENT_FUNDS
Definition TER.h:307
@ tecNFTOKEN_BUY_SELL_MISMATCH
Definition TER.h:304
@ tecINTERNAL
Definition TER.h:292
@ tecNO_PERMISSION
Definition TER.h:287
@ tecNO_LINE
Definition TER.h:283
@ tecINSUFFICIENT_PAYMENT
Definition TER.h:309
@ tecINSUFFICIENT_RESERVE
Definition TER.h:289
@ tecCANT_ACCEPT_OWN_NFTOKEN_OFFER
Definition TER.h:306
@ tecEXPIRED
Definition TER.h:296
@ tesSUCCESS
Definition TER.h:226
bool isTesSuccess(TER x) noexcept
Definition TER.h:659
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:611
constexpr std::uint32_t const tfNFTokenAcceptOfferMask
Definition TxFlags.h:219
@ temMALFORMED
Definition TER.h:68
@ temBAD_OFFER
Definition TER.h:76
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:16