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