rippled
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 <ripple/app/tx/impl/NFTokenAcceptOffer.h>
21 #include <ripple/app/tx/impl/details/NFTokenUtils.h>
22 #include <ripple/ledger/View.h>
23 #include <ripple/protocol/Feature.h>
24 #include <ripple/protocol/Rate.h>
25 #include <ripple/protocol/TxFlags.h>
26 #include <ripple/protocol/st.h>
27 
28 namespace ripple {
29 
30 NotTEC
32 {
34  return temDISABLED;
35 
36  if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
37  return ret;
38 
40  return temINVALID_FLAG;
41 
42  auto const bo = ctx.tx[~sfNFTokenBuyOffer];
43  auto const so = ctx.tx[~sfNFTokenSellOffer];
44 
45  // At least one of these MUST be specified
46  if (!bo && !so)
47  return temMALFORMED;
48 
49  // The `BrokerFee` field must not be present in direct mode but may be
50  // present and greater than zero in brokered mode.
51  if (auto const bf = ctx.tx[~sfNFTokenBrokerFee])
52  {
53  if (!bo || !so)
54  return temMALFORMED;
55 
56  if (*bf <= beast::zero)
57  return temMALFORMED;
58  }
59 
60  return preflight2(ctx);
61 }
62 
63 TER
65 {
66  auto const checkOffer = [&ctx](std::optional<uint256> id)
68  if (id)
69  {
70  if (id->isZero())
71  return {nullptr, tecOBJECT_NOT_FOUND};
72 
73  auto offerSLE = ctx.view.read(keylet::nftoffer(*id));
74 
75  if (!offerSLE)
76  return {nullptr, tecOBJECT_NOT_FOUND};
77 
78  if (hasExpired(ctx.view, (*offerSLE)[~sfExpiration]))
79  return {nullptr, tecEXPIRED};
80 
81  // The initial implementation had a bug that allowed a negative
82  // amount. The fixNFTokenNegOffer amendment fixes that.
83  if ((*offerSLE)[sfAmount].negative() &&
85  return {nullptr, temBAD_OFFER};
86 
87  return {std::move(offerSLE), tesSUCCESS};
88  }
89  return {nullptr, tesSUCCESS};
90  };
91 
92  auto const [bo, err1] = checkOffer(ctx.tx[~sfNFTokenBuyOffer]);
93  if (!isTesSuccess(err1))
94  return err1;
95  auto const [so, err2] = checkOffer(ctx.tx[~sfNFTokenSellOffer]);
96  if (!isTesSuccess(err2))
97  return err2;
98 
99  if (bo && so)
100  {
101  // Brokered mode:
102  // The two offers being brokered must be for the same token:
103  if ((*bo)[sfNFTokenID] != (*so)[sfNFTokenID])
105 
106  // The two offers being brokered must be for the same asset:
107  if ((*bo)[sfAmount].issue() != (*so)[sfAmount].issue())
109 
110  // The two offers may not form a loop. A broker may not sell the
111  // token to the current owner of the token.
113  ((*bo)[sfOwner] == (*so)[sfOwner]))
115 
116  // Ensure that the buyer is willing to pay at least as much as the
117  // seller is requesting:
118  if ((*so)[sfAmount] > (*bo)[sfAmount])
120 
121  // If the buyer specified a destination
122  if (auto const dest = bo->at(~sfDestination))
123  {
124  // Before this fix the destination could be either the seller or
125  // a broker. After, it must be whoever is submitting the tx.
127  {
128  if (*dest != ctx.tx[sfAccount])
129  return tecNO_PERMISSION;
130  }
131  else if (*dest != so->at(sfOwner) && *dest != ctx.tx[sfAccount])
133  }
134 
135  // If the seller specified a destination
136  if (auto const dest = so->at(~sfDestination))
137  {
138  // Before this fix the destination could be either the seller or
139  // a broker. After, it must be whoever is submitting the tx.
141  {
142  if (*dest != ctx.tx[sfAccount])
143  return tecNO_PERMISSION;
144  }
145  else if (*dest != bo->at(sfOwner) && *dest != ctx.tx[sfAccount])
147  }
148 
149  // The broker can specify an amount that represents their cut; if they
150  // have, ensure that the seller will get at least as much as they want
151  // to get *after* this fee is accounted for (but before the issuer's
152  // cut, if any).
153  if (auto const brokerFee = ctx.tx[~sfNFTokenBrokerFee])
154  {
155  if (brokerFee->issue() != (*bo)[sfAmount].issue())
157 
158  if (brokerFee >= (*bo)[sfAmount])
160 
161  if ((*so)[sfAmount] > (*bo)[sfAmount] - *brokerFee)
163  }
164  }
165 
166  if (bo)
167  {
168  if (((*bo)[sfFlags] & lsfSellNFToken) == lsfSellNFToken)
170 
171  // An account can't accept an offer it placed:
172  if ((*bo)[sfOwner] == ctx.tx[sfAccount])
174 
175  // If not in bridged mode, the account must own the token:
176  if (!so &&
177  !nft::findToken(ctx.view, ctx.tx[sfAccount], (*bo)[sfNFTokenID]))
178  return tecNO_PERMISSION;
179 
180  // If not in bridged mode...
181  if (!so)
182  {
183  // If the offer has a Destination field, the acceptor must be the
184  // Destination.
185  if (auto const dest = bo->at(~sfDestination);
186  dest.has_value() && *dest != ctx.tx[sfAccount])
187  return tecNO_PERMISSION;
188  }
189 
190  // The account offering to buy must have funds:
191  //
192  // After this amendment, we allow an IOU issuer to buy an NFT with their
193  // own currency
194  auto const needed = bo->at(sfAmount);
196  {
197  if (accountFunds(
198  ctx.view, (*bo)[sfOwner], needed, fhZERO_IF_FROZEN, ctx.j) <
199  needed)
200  return tecINSUFFICIENT_FUNDS;
201  }
202  else if (
203  accountHolds(
204  ctx.view,
205  (*bo)[sfOwner],
206  needed.getCurrency(),
207  needed.getIssuer(),
209  ctx.j) < needed)
210  return tecINSUFFICIENT_FUNDS;
211  }
212 
213  if (so)
214  {
215  if (((*so)[sfFlags] & lsfSellNFToken) != lsfSellNFToken)
217 
218  // An account can't accept an offer it placed:
219  if ((*so)[sfOwner] == ctx.tx[sfAccount])
221 
222  // The seller must own the token.
223  if (!nft::findToken(ctx.view, (*so)[sfOwner], (*so)[sfNFTokenID]))
224  return tecNO_PERMISSION;
225 
226  // If not in bridged mode...
227  if (!bo)
228  {
229  // If the offer has a Destination field, the acceptor must be the
230  // Destination.
231  if (auto const dest = so->at(~sfDestination);
232  dest.has_value() && *dest != ctx.tx[sfAccount])
233  return tecNO_PERMISSION;
234  }
235 
236  // The account offering to buy must have funds:
237  auto const needed = so->at(sfAmount);
239  {
240  if (accountHolds(
241  ctx.view,
242  ctx.tx[sfAccount],
243  needed.getCurrency(),
244  needed.getIssuer(),
246  ctx.j) < needed)
247  return tecINSUFFICIENT_FUNDS;
248  }
249  else if (!bo)
250  {
251  // After this amendment, we allow buyers to buy with their own
252  // issued currency.
253  //
254  // In the case of brokered mode, this check is essentially
255  // redundant, since we have already confirmed that buy offer is >
256  // than the sell offer, and that the buyer can cover the buy
257  // offer.
258  //
259  // We also _must not_ check the tx submitter in brokered
260  // mode, because then we are confirming that the broker can
261  // cover what the buyer will pay, which doesn't make sense, causes
262  // an unncessary tec, and is also resolved with this amendment.
263  if (accountFunds(
264  ctx.view,
265  ctx.tx[sfAccount],
266  needed,
268  ctx.j) < needed)
269  return tecINSUFFICIENT_FUNDS;
270  }
271  }
272 
273  return tesSUCCESS;
274 }
275 
276 TER
278  AccountID const& from,
279  AccountID const& to,
280  STAmount const& amount)
281 {
282  // This should never happen, but it's easy and quick to check.
283  if (amount < beast::zero)
284  return tecINTERNAL;
285 
286  auto const result = accountSend(view(), from, to, amount, j_);
287 
288  // After this amendment, if any payment would cause a non-IOU-issuer to
289  // have a negative balance, or an IOU-issuer to have a positive balance in
290  // their own currency, we know that something went wrong. This was
291  // originally found in the context of IOU transfer fees. Since there are
292  // several payouts in this tx, just confirm that the end state is OK.
293  if (!view().rules().enabled(fixNonFungibleTokensV1_2))
294  return result;
295  if (result != tesSUCCESS)
296  return result;
297  if (accountFunds(view(), from, amount, fhZERO_IF_FROZEN, j_).signum() < 0)
298  return tecINSUFFICIENT_FUNDS;
299  if (accountFunds(view(), to, amount, fhZERO_IF_FROZEN, j_).signum() < 0)
300  return tecINSUFFICIENT_FUNDS;
301  return tesSUCCESS;
302 }
303 
304 TER
306 {
307  bool const isSell = offer->isFlag(lsfSellNFToken);
308  AccountID const owner = (*offer)[sfOwner];
309  AccountID const& seller = isSell ? owner : account_;
310  AccountID const& buyer = isSell ? account_ : owner;
311 
312  auto const nftokenID = (*offer)[sfNFTokenID];
313 
314  if (auto amount = offer->getFieldAmount(sfAmount); amount != beast::zero)
315  {
316  // Calculate the issuer's cut from this sale, if any:
317  if (auto const fee = nft::getTransferFee(nftokenID); fee != 0)
318  {
319  auto const cut = multiply(amount, nft::transferFeeAsRate(fee));
320 
321  if (auto const issuer = nft::getIssuer(nftokenID);
322  cut != beast::zero && seller != issuer && buyer != issuer)
323  {
324  if (auto const r = pay(buyer, issuer, cut); !isTesSuccess(r))
325  return r;
326  amount -= cut;
327  }
328  }
329 
330  // Send the remaining funds to the seller of the NFT
331  if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
332  return r;
333  }
334 
335  // Now transfer the NFT:
336  auto tokenAndPage = nft::findTokenAndPage(view(), seller, nftokenID);
337 
338  if (!tokenAndPage)
339  return tecINTERNAL;
340 
341  if (auto const ret = nft::removeToken(
342  view(), seller, nftokenID, std::move(tokenAndPage->page));
343  !isTesSuccess(ret))
344  return ret;
345 
346  return nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
347 }
348 
349 TER
351 {
352  auto const loadToken = [this](std::optional<uint256> const& id) {
354  if (id)
355  sle = view().peek(keylet::nftoffer(*id));
356  return sle;
357  };
358 
359  auto bo = loadToken(ctx_.tx[~sfNFTokenBuyOffer]);
360  auto so = loadToken(ctx_.tx[~sfNFTokenSellOffer]);
361 
362  if (bo && !nft::deleteTokenOffer(view(), bo))
363  {
364  JLOG(j_.fatal()) << "Unable to delete buy offer '"
365  << to_string(bo->key()) << "': ignoring";
366  return tecINTERNAL;
367  }
368 
369  if (so && !nft::deleteTokenOffer(view(), so))
370  {
371  JLOG(j_.fatal()) << "Unable to delete sell offer '"
372  << to_string(so->key()) << "': ignoring";
373  return tecINTERNAL;
374  }
375 
376  // Bridging two different offers
377  if (bo && so)
378  {
379  AccountID const buyer = (*bo)[sfOwner];
380  AccountID const seller = (*so)[sfOwner];
381 
382  auto const nftokenID = (*so)[sfNFTokenID];
383 
384  // The amount is what the buyer of the NFT pays:
385  STAmount amount = (*bo)[sfAmount];
386 
387  // Three different folks may be paid. The order of operations is
388  // important.
389  //
390  // o The broker is paid the cut they requested.
391  // o The issuer's cut is calculated from what remains after the
392  // broker is paid. The issuer can take up to 50% of the remainder.
393  // o Finally, the seller gets whatever is left.
394  //
395  // It is important that the issuer's cut be calculated after the
396  // broker's portion is already removed. Calculating the issuer's
397  // cut before the broker's cut is removed can result in more money
398  // being paid out than the seller authorized. That would be bad!
399 
400  // Send the broker the amount they requested.
401  if (auto const cut = ctx_.tx[~sfNFTokenBrokerFee];
402  cut && cut.value() != beast::zero)
403  {
404  if (auto const r = pay(buyer, account_, cut.value());
405  !isTesSuccess(r))
406  return r;
407 
408  amount -= cut.value();
409  }
410 
411  // Calculate the issuer's cut, if any.
412  if (auto const fee = nft::getTransferFee(nftokenID);
413  amount != beast::zero && fee != 0)
414  {
415  auto cut = multiply(amount, nft::transferFeeAsRate(fee));
416 
417  if (auto const issuer = nft::getIssuer(nftokenID);
418  seller != issuer && buyer != issuer)
419  {
420  if (auto const r = pay(buyer, issuer, cut); !isTesSuccess(r))
421  return r;
422 
423  amount -= cut;
424  }
425  }
426 
427  // And send whatever remains to the seller.
428  if (amount > beast::zero)
429  {
430  if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
431  return r;
432  }
433 
434  auto tokenAndPage = nft::findTokenAndPage(view(), seller, nftokenID);
435 
436  if (!tokenAndPage)
437  return tecINTERNAL;
438 
439  if (auto const ret = nft::removeToken(
440  view(), seller, nftokenID, std::move(tokenAndPage->page));
441  !isTesSuccess(ret))
442  return ret;
443 
444  return nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
445  }
446 
447  if (bo)
448  return acceptOffer(bo);
449 
450  if (so)
451  return acceptOffer(so);
452 
453  return tecINTERNAL;
454 }
455 
456 } // namespace ripple
beast::Journal::fatal
Stream fatal() const
Definition: Journal.h:339
ripple::tecOBJECT_NOT_FOUND
@ tecOBJECT_NOT_FOUND
Definition: TER.h:293
ripple::preflight2
NotTEC preflight2(PreflightContext const &ctx)
Checks whether the signature appears valid.
Definition: Transactor.cpp:130
ripple::fixNFTokenNegOffer
const uint256 fixNFTokenNegOffer
ripple::Rules::enabled
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition: Rules.cpp:94
ripple::temBAD_OFFER
@ temBAD_OFFER
Definition: TER.h:93
std::shared_ptr
STL class.
ripple::PreclaimContext::view
ReadView const & view
Definition: Transactor.h:56
ripple::fhZERO_IF_FROZEN
@ fhZERO_IF_FROZEN
Definition: View.h:76
ripple::PreclaimContext::j
const beast::Journal j
Definition: Transactor.h:60
ripple::ApplyView::peek
virtual std::shared_ptr< SLE > peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
ripple::sfDestination
const SF_ACCOUNT sfDestination
ripple::tfNFTokenAcceptOfferMask
constexpr const std::uint32_t tfNFTokenAcceptOfferMask
Definition: TxFlags.h:160
ripple::sfAmount
const SF_AMOUNT sfAmount
ripple::sfNFTokenID
const SF_UINT256 sfNFTokenID
ripple::Transactor::j_
const beast::Journal j_
Definition: Transactor.h:89
ripple::nft::findTokenAndPage
std::optional< TokenAndPage > findTokenAndPage(ApplyView &view, AccountID const &owner, uint256 const &nftokenID)
Definition: NFTokenUtils.cpp:505
ripple::isTesSuccess
bool isTesSuccess(TER x)
Definition: TER.h:597
ripple::NFTokenAcceptOffer::pay
TER pay(AccountID const &from, AccountID const &to, STAmount const &amount)
Definition: NFTokenAcceptOffer.cpp:277
std::pair
ripple::tecINSUFFICIENT_FUNDS
@ tecINSUFFICIENT_FUNDS
Definition: TER.h:292
ripple::sfOwner
const SF_ACCOUNT sfOwner
ripple::nft::transferFeeAsRate
Rate transferFeeAsRate(std::uint16_t fee)
Given a transfer fee (in basis points) convert it to a transfer rate.
Definition: Rate2.cpp:39
ripple::NFTokenAcceptOffer::preflight
static NotTEC preflight(PreflightContext const &ctx)
Definition: NFTokenAcceptOffer.cpp:31
ripple::accountHolds
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j)
Definition: View.cpp:223
ripple::keylet::nftoffer
Keylet nftoffer(AccountID const &owner, std::uint32_t seq)
An offer from an account to buy or sell an NFT.
Definition: Indexes.cpp:355
ripple::NFTokenAcceptOffer::preclaim
static TER preclaim(PreclaimContext const &ctx)
Definition: NFTokenAcceptOffer.cpp:64
ripple::hasExpired
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp)
Determines whether the given expiration time has passed.
Definition: View.cpp:179
ripple::nft::findToken
std::optional< STObject > findToken(ReadView const &view, AccountID const &owner, uint256 const &nftokenID)
Finds the specified token in the owner's token directory.
Definition: NFTokenUtils.cpp:483
ripple::tecCANT_ACCEPT_OWN_NFTOKEN_OFFER
@ tecCANT_ACCEPT_OWN_NFTOKEN_OFFER
Definition: TER.h:291
ripple::nft::removeToken
TER removeToken(ApplyView &view, AccountID const &owner, uint256 const &nftokenID)
Remove the token from the owner's token directory.
Definition: NFTokenUtils.cpp:349
ripple::preflight1
NotTEC preflight1(PreflightContext const &ctx)
Performs early sanity checks on the account and fee fields.
Definition: Transactor.cpp:78
ripple::lsfSellNFToken
@ lsfSellNFToken
Definition: LedgerFormats.h:269
ripple::sfExpiration
const SF_UINT32 sfExpiration
ripple::nft::getIssuer
AccountID getIssuer(uint256 const &id)
Definition: NFTokenUtils.h:180
ripple::base_uint< 160, detail::AccountIDTag >
ripple::temINVALID_FLAG
@ temINVALID_FLAG
Definition: TER.h:109
ripple::tecNFTOKEN_OFFER_TYPE_MISMATCH
@ tecNFTOKEN_OFFER_TYPE_MISMATCH
Definition: TER.h:290
ripple::TERSubset< CanCvtToTER >
ripple::STAmount::value
STAmount const & value() const noexcept
Definition: STAmount.h:440
ripple::accountSend
TER accountSend(ApplyView &view, AccountID const &uSenderID, AccountID const &uReceiverID, STAmount const &saAmount, beast::Journal j)
Definition: View.cpp:1122
ripple::STAmount
Definition: STAmount.h:45
ripple::tecINTERNAL
@ tecINTERNAL
Definition: TER.h:277
ripple::STObject::getFlags
std::uint32_t getFlags() const
Definition: STObject.cpp:481
ripple::NFTokenAcceptOffer::acceptOffer
TER acceptOffer(std::shared_ptr< SLE > const &offer)
Definition: NFTokenAcceptOffer.cpp:305
ripple::ReadView::read
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
ripple::PreclaimContext::tx
STTx const & tx
Definition: Transactor.h:58
ripple::accountFunds
STAmount accountFunds(ReadView const &view, AccountID const &id, STAmount const &saDefault, FreezeHandling freezeHandling, beast::Journal j)
Definition: View.cpp:267
ripple::multiply
STAmount multiply(STAmount const &amount, Rate const &rate)
Definition: Rate2.cpp:47
ripple::PreclaimContext
State information when determining if a tx is likely to claim a fee.
Definition: Transactor.h:52
ripple::sfNFTokenBuyOffer
const SF_UINT256 sfNFTokenBuyOffer
ripple::NFTokenAcceptOffer::doApply
TER doApply() override
Definition: NFTokenAcceptOffer.cpp:350
ripple
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: RCLCensorshipDetector.h:29
ripple::featureNonFungibleTokensV1
const uint256 featureNonFungibleTokensV1
ripple::tecNFTOKEN_BUY_SELL_MISMATCH
@ tecNFTOKEN_BUY_SELL_MISMATCH
Definition: TER.h:289
ripple::tecINSUFFICIENT_PAYMENT
@ tecINSUFFICIENT_PAYMENT
Definition: TER.h:294
ripple::Transactor::view
ApplyView & view()
Definition: Transactor.h:107
ripple::tecEXPIRED
@ tecEXPIRED
Definition: TER.h:281
ripple::temDISABLED
@ temDISABLED
Definition: TER.h:112
ripple::ReadView::rules
virtual Rules const & rules() const =0
Returns the tx processing rules.
ripple::sfFlags
const SF_UINT32 sfFlags
ripple::tecNO_PERMISSION
@ tecNO_PERMISSION
Definition: TER.h:272
ripple::Transactor::ctx_
ApplyContext & ctx_
Definition: Transactor.h:88
std::optional
ripple::to_string
std::string to_string(Manifest const &m)
Format the specified manifest to a string for debugging purposes.
Definition: app/misc/impl/Manifest.cpp:41
ripple::sfAccount
const SF_ACCOUNT sfAccount
ripple::temMALFORMED
@ temMALFORMED
Definition: TER.h:85
ripple::fixNonFungibleTokensV1_2
const uint256 fixNonFungibleTokensV1_2
ripple::PreflightContext::tx
STTx const & tx
Definition: Transactor.h:35
ripple::nft::getTransferFee
std::uint16_t getTransferFee(uint256 const &id)
Definition: NFTokenUtils.h:128
ripple::PreflightContext
State information when preflighting a tx.
Definition: Transactor.h:31
ripple::PreflightContext::rules
const Rules rules
Definition: Transactor.h:36
ripple::tesSUCCESS
@ tesSUCCESS
Definition: TER.h:222
ripple::Transactor::account_
const AccountID account_
Definition: Transactor.h:91
ripple::sfNFTokenSellOffer
const SF_UINT256 sfNFTokenSellOffer
ripple::nft::insertToken
TER insertToken(ApplyView &view, AccountID owner, STObject &&nft)
Insert the token in the owner's token directory.
Definition: NFTokenUtils.cpp:243
ripple::ApplyContext::tx
STTx const & tx
Definition: ApplyContext.h:48
ripple::nft::deleteTokenOffer
bool deleteTokenOffer(ApplyView &view, std::shared_ptr< SLE > const &offer)
Deletes the given token offer.
Definition: NFTokenUtils.cpp:605
ripple::sfNFTokenBrokerFee
const SF_AMOUNT sfNFTokenBrokerFee
ripple::NotTEC
TERSubset< CanCvtToNotTEC > NotTEC
Definition: TER.h:528