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