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  // Ensure that the buyer is willing to pay at least as much as the
111  // seller is requesting:
112  if ((*so)[sfAmount] > (*bo)[sfAmount])
114 
115  // If the buyer specified a destination, that destination must be
116  // the seller or the broker.
117  if (auto const dest = bo->at(~sfDestination))
118  {
119  if (*dest != so->at(sfOwner) && *dest != ctx.tx[sfAccount])
121  }
122 
123  // If the seller specified a destination, that destination must be
124  // the buyer or the broker.
125  if (auto const dest = so->at(~sfDestination))
126  {
127  if (*dest != bo->at(sfOwner) && *dest != ctx.tx[sfAccount])
129  }
130 
131  // The broker can specify an amount that represents their cut; if they
132  // have, ensure that the seller will get at least as much as they want
133  // to get *after* this fee is accounted for (but before the issuer's
134  // cut, if any).
135  if (auto const brokerFee = ctx.tx[~sfNFTokenBrokerFee])
136  {
137  if (brokerFee->issue() != (*bo)[sfAmount].issue())
139 
140  if (brokerFee >= (*bo)[sfAmount])
142 
143  if ((*so)[sfAmount] > (*bo)[sfAmount] - *brokerFee)
145  }
146  }
147 
148  if (bo)
149  {
150  if (((*bo)[sfFlags] & lsfSellNFToken) == lsfSellNFToken)
152 
153  // An account can't accept an offer it placed:
154  if ((*bo)[sfOwner] == ctx.tx[sfAccount])
156 
157  // If not in bridged mode, the account must own the token:
158  if (!so &&
159  !nft::findToken(ctx.view, ctx.tx[sfAccount], (*bo)[sfNFTokenID]))
160  return tecNO_PERMISSION;
161 
162  // If not in bridged mode...
163  if (!so)
164  {
165  // If the offer has a Destination field, the acceptor must be the
166  // Destination.
167  if (auto const dest = bo->at(~sfDestination);
168  dest.has_value() && *dest != ctx.tx[sfAccount])
169  return tecNO_PERMISSION;
170  }
171  // The account offering to buy must have funds:
172  auto const needed = bo->at(sfAmount);
173 
174  if (accountHolds(
175  ctx.view,
176  (*bo)[sfOwner],
177  needed.getCurrency(),
178  needed.getIssuer(),
180  ctx.j) < needed)
181  return tecINSUFFICIENT_FUNDS;
182  }
183 
184  if (so)
185  {
186  if (((*so)[sfFlags] & lsfSellNFToken) != lsfSellNFToken)
188 
189  // An account can't accept an offer it placed:
190  if ((*so)[sfOwner] == ctx.tx[sfAccount])
192 
193  // The seller must own the token.
194  if (!nft::findToken(ctx.view, (*so)[sfOwner], (*so)[sfNFTokenID]))
195  return tecNO_PERMISSION;
196 
197  // If not in bridged mode...
198  if (!bo)
199  {
200  // If the offer has a Destination field, the acceptor must be the
201  // Destination.
202  if (auto const dest = so->at(~sfDestination);
203  dest.has_value() && *dest != ctx.tx[sfAccount])
204  return tecNO_PERMISSION;
205  }
206 
207  // The account offering to buy must have funds:
208  auto const needed = so->at(sfAmount);
209 
210  if (accountHolds(
211  ctx.view,
212  ctx.tx[sfAccount],
213  needed.getCurrency(),
214  needed.getIssuer(),
216  ctx.j) < needed)
217  return tecINSUFFICIENT_FUNDS;
218  }
219 
220  return tesSUCCESS;
221 }
222 
223 TER
225  AccountID const& from,
226  AccountID const& to,
227  STAmount const& amount)
228 {
229  // This should never happen, but it's easy and quick to check.
230  if (amount < beast::zero)
231  return tecINTERNAL;
232 
233  return accountSend(view(), from, to, amount, j_);
234 }
235 
236 TER
238 {
239  bool const isSell = offer->isFlag(lsfSellNFToken);
240  AccountID const owner = (*offer)[sfOwner];
241  AccountID const& seller = isSell ? owner : account_;
242  AccountID const& buyer = isSell ? account_ : owner;
243 
244  auto const nftokenID = (*offer)[sfNFTokenID];
245 
246  if (auto amount = offer->getFieldAmount(sfAmount); amount != beast::zero)
247  {
248  // Calculate the issuer's cut from this sale, if any:
249  if (auto const fee = nft::getTransferFee(nftokenID); fee != 0)
250  {
251  auto const cut = multiply(amount, nft::transferFeeAsRate(fee));
252 
253  if (auto const issuer = nft::getIssuer(nftokenID);
254  cut != beast::zero && seller != issuer && buyer != issuer)
255  {
256  if (auto const r = pay(buyer, issuer, cut); !isTesSuccess(r))
257  return r;
258  amount -= cut;
259  }
260  }
261 
262  // Send the remaining funds to the seller of the NFT
263  if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
264  return r;
265  }
266 
267  // Now transfer the NFT:
268  auto tokenAndPage = nft::findTokenAndPage(view(), seller, nftokenID);
269 
270  if (!tokenAndPage)
271  return tecINTERNAL;
272 
273  if (auto const ret = nft::removeToken(
274  view(), seller, nftokenID, std::move(tokenAndPage->page));
275  !isTesSuccess(ret))
276  return ret;
277 
278  return nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
279 }
280 
281 TER
283 {
284  auto const loadToken = [this](std::optional<uint256> const& id) {
286  if (id)
287  sle = view().peek(keylet::nftoffer(*id));
288  return sle;
289  };
290 
291  auto bo = loadToken(ctx_.tx[~sfNFTokenBuyOffer]);
292  auto so = loadToken(ctx_.tx[~sfNFTokenSellOffer]);
293 
294  if (bo && !nft::deleteTokenOffer(view(), bo))
295  {
296  JLOG(j_.fatal()) << "Unable to delete buy offer '"
297  << to_string(bo->key()) << "': ignoring";
298  return tecINTERNAL;
299  }
300 
301  if (so && !nft::deleteTokenOffer(view(), so))
302  {
303  JLOG(j_.fatal()) << "Unable to delete sell offer '"
304  << to_string(so->key()) << "': ignoring";
305  return tecINTERNAL;
306  }
307 
308  // Bridging two different offers
309  if (bo && so)
310  {
311  AccountID const buyer = (*bo)[sfOwner];
312  AccountID const seller = (*so)[sfOwner];
313 
314  auto const nftokenID = (*so)[sfNFTokenID];
315 
316  // The amount is what the buyer of the NFT pays:
317  STAmount amount = (*bo)[sfAmount];
318 
319  // Three different folks may be paid. The order of operations is
320  // important.
321  //
322  // o The broker is paid the cut they requested.
323  // o The issuer's cut is calculated from what remains after the
324  // broker is paid. The issuer can take up to 50% of the remainder.
325  // o Finally, the seller gets whatever is left.
326  //
327  // It is important that the issuer's cut be calculated after the
328  // broker's portion is already removed. Calculating the issuer's
329  // cut before the broker's cut is removed can result in more money
330  // being paid out than the seller authorized. That would be bad!
331 
332  // Send the broker the amount they requested.
333  if (auto const cut = ctx_.tx[~sfNFTokenBrokerFee];
334  cut && cut.value() != beast::zero)
335  {
336  if (auto const r = pay(buyer, account_, cut.value());
337  !isTesSuccess(r))
338  return r;
339 
340  amount -= cut.value();
341  }
342 
343  // Calculate the issuer's cut, if any.
344  if (auto const fee = nft::getTransferFee(nftokenID);
345  amount != beast::zero && fee != 0)
346  {
347  auto cut = multiply(amount, nft::transferFeeAsRate(fee));
348 
349  if (auto const issuer = nft::getIssuer(nftokenID);
350  seller != issuer && buyer != issuer)
351  {
352  if (auto const r = pay(buyer, issuer, cut); !isTesSuccess(r))
353  return r;
354 
355  amount -= cut;
356  }
357  }
358 
359  // And send whatever remains to the seller.
360  if (amount > beast::zero)
361  {
362  if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
363  return r;
364  }
365 
366  auto tokenAndPage = nft::findTokenAndPage(view(), seller, nftokenID);
367 
368  if (!tokenAndPage)
369  return tecINTERNAL;
370 
371  if (auto const ret = nft::removeToken(
372  view(), seller, nftokenID, std::move(tokenAndPage->page));
373  !isTesSuccess(ret))
374  return ret;
375 
376  return nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
377  }
378 
379  if (bo)
380  return acceptOffer(bo);
381 
382  if (so)
383  return acceptOffer(so);
384 
385  return tecINTERNAL;
386 }
387 
388 } // namespace ripple
beast::Journal::fatal
Stream fatal() const
Definition: Journal.h:339
ripple::tecOBJECT_NOT_FOUND
@ tecOBJECT_NOT_FOUND
Definition: TER.h:290
ripple::preflight2
NotTEC preflight2(PreflightContext const &ctx)
Checks whether the signature appears valid.
Definition: Transactor.cpp:109
ripple::fixNFTokenNegOffer
const uint256 fixNFTokenNegOffer
ripple::Rules::enabled
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition: Rules.cpp:81
ripple::temBAD_OFFER
@ temBAD_OFFER
Definition: TER.h:90
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:151
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:502
ripple::isTesSuccess
bool isTesSuccess(TER x)
Definition: TER.h:594
ripple::NFTokenAcceptOffer::pay
TER pay(AccountID const &from, AccountID const &to, STAmount const &amount)
Definition: NFTokenAcceptOffer.cpp:224
std::pair
ripple::tecINSUFFICIENT_FUNDS
@ tecINSUFFICIENT_FUNDS
Definition: TER.h:289
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:480
ripple::tecCANT_ACCEPT_OWN_NFTOKEN_OFFER
@ tecCANT_ACCEPT_OWN_NFTOKEN_OFFER
Definition: TER.h:288
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:346
ripple::preflight1
NotTEC preflight1(PreflightContext const &ctx)
Performs early sanity checks on the account and fee fields.
Definition: Transactor.cpp:57
ripple::lsfSellNFToken
@ lsfSellNFToken
Definition: LedgerFormats.h:258
ripple::sfExpiration
const SF_UINT32 sfExpiration
ripple::nft::getIssuer
AccountID getIssuer(uint256 const &id)
Definition: NFTokenUtils.h:176
ripple::base_uint< 160, detail::AccountIDTag >
ripple::temINVALID_FLAG
@ temINVALID_FLAG
Definition: TER.h:106
ripple::tecNFTOKEN_OFFER_TYPE_MISMATCH
@ tecNFTOKEN_OFFER_TYPE_MISMATCH
Definition: TER.h:287
ripple::TERSubset< CanCvtToTER >
ripple::STAmount::value
STAmount const & value() const noexcept
Definition: STAmount.h:425
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:44
ripple::tecINTERNAL
@ tecINTERNAL
Definition: TER.h:274
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:237
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::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:282
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:286
ripple::tecINSUFFICIENT_PAYMENT
@ tecINSUFFICIENT_PAYMENT
Definition: TER.h:291
ripple::Transactor::view
ApplyView & view()
Definition: Transactor.h:107
ripple::tecEXPIRED
@ tecEXPIRED
Definition: TER.h:278
ripple::temDISABLED
@ temDISABLED
Definition: TER.h:109
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:269
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:82
ripple::PreflightContext::tx
STTx const & tx
Definition: Transactor.h:35
ripple::nft::getTransferFee
std::uint16_t getTransferFee(uint256 const &id)
Definition: NFTokenUtils.h:124
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:219
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:240
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:581
ripple::sfNFTokenBrokerFee
const SF_AMOUNT sfNFTokenBrokerFee
ripple::NotTEC
TERSubset< CanCvtToNotTEC > NotTEC
Definition: TER.h:525