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  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  // The initial implementation had a bug that allowed a negative
79  // amount. The fixNFTokenNegOffer amendment fixes that.
80  if ((*offerSLE)[sfAmount].negative() &&
82  return {nullptr, temBAD_OFFER};
83 
84  return {std::move(offerSLE), tesSUCCESS};
85  }
86  return {nullptr, tesSUCCESS};
87  };
88 
89  auto const [bo, err1] = checkOffer(ctx.tx[~sfNFTokenBuyOffer]);
90  if (!isTesSuccess(err1))
91  return err1;
92  auto const [so, err2] = checkOffer(ctx.tx[~sfNFTokenSellOffer]);
93  if (!isTesSuccess(err2))
94  return err2;
95 
96  if (bo && so)
97  {
98  // Brokered mode:
99  // The two offers being brokered must be for the same token:
100  if ((*bo)[sfNFTokenID] != (*so)[sfNFTokenID])
102 
103  // The two offers being brokered must be for the same asset:
104  if ((*bo)[sfAmount].issue() != (*so)[sfAmount].issue())
106 
107  // Ensure that the buyer is willing to pay at least as much as the
108  // seller is requesting:
109  if ((*so)[sfAmount] > (*bo)[sfAmount])
111 
112  // If the buyer specified a destination, that destination must be
113  // the seller or the broker.
114  if (auto const dest = bo->at(~sfDestination))
115  {
116  if (*dest != so->at(sfOwner) && *dest != ctx.tx[sfAccount])
118  }
119 
120  // If the seller specified a destination, that destination must be
121  // the buyer or the broker.
122  if (auto const dest = so->at(~sfDestination))
123  {
124  if (*dest != bo->at(sfOwner) && *dest != ctx.tx[sfAccount])
126  }
127 
128  // The broker can specify an amount that represents their cut; if they
129  // have, ensure that the seller will get at least as much as they want
130  // to get *after* this fee is accounted for (but before the issuer's
131  // cut, if any).
132  if (auto const brokerFee = ctx.tx[~sfNFTokenBrokerFee])
133  {
134  if (brokerFee->issue() != (*bo)[sfAmount].issue())
136 
137  if (brokerFee >= (*bo)[sfAmount])
139 
140  if ((*so)[sfAmount] > (*bo)[sfAmount] - *brokerFee)
142  }
143  }
144 
145  if (bo)
146  {
147  if (((*bo)[sfFlags] & lsfSellNFToken) == lsfSellNFToken)
149 
150  // An account can't accept an offer it placed:
151  if ((*bo)[sfOwner] == ctx.tx[sfAccount])
153 
154  // If not in bridged mode, the account must own the token:
155  if (!so &&
156  !nft::findToken(ctx.view, ctx.tx[sfAccount], (*bo)[sfNFTokenID]))
157  return tecNO_PERMISSION;
158 
159  // If not in bridged mode...
160  if (!so)
161  {
162  // If the offer has a Destination field, the acceptor must be the
163  // Destination.
164  if (auto const dest = bo->at(~sfDestination);
165  dest.has_value() && *dest != ctx.tx[sfAccount])
166  return tecNO_PERMISSION;
167  }
168  // The account offering to buy must have funds:
169  auto const needed = bo->at(sfAmount);
170 
171  if (accountHolds(
172  ctx.view,
173  (*bo)[sfOwner],
174  needed.getCurrency(),
175  needed.getIssuer(),
177  ctx.j) < needed)
178  return tecINSUFFICIENT_FUNDS;
179  }
180 
181  if (so)
182  {
183  if (((*so)[sfFlags] & lsfSellNFToken) != lsfSellNFToken)
185 
186  // An account can't accept an offer it placed:
187  if ((*so)[sfOwner] == ctx.tx[sfAccount])
189 
190  // The seller must own the token.
191  if (!nft::findToken(ctx.view, (*so)[sfOwner], (*so)[sfNFTokenID]))
192  return tecNO_PERMISSION;
193 
194  // If not in bridged mode...
195  if (!bo)
196  {
197  // If the offer has a Destination field, the acceptor must be the
198  // Destination.
199  if (auto const dest = so->at(~sfDestination);
200  dest.has_value() && *dest != ctx.tx[sfAccount])
201  return tecNO_PERMISSION;
202  }
203 
204  // The account offering to buy must have funds:
205  auto const needed = so->at(sfAmount);
206 
207  if (accountHolds(
208  ctx.view,
209  ctx.tx[sfAccount],
210  needed.getCurrency(),
211  needed.getIssuer(),
213  ctx.j) < needed)
214  return tecINSUFFICIENT_FUNDS;
215  }
216 
217  return tesSUCCESS;
218 }
219 
220 TER
222  AccountID const& from,
223  AccountID const& to,
224  STAmount const& amount)
225 {
226  // This should never happen, but it's easy and quick to check.
227  if (amount < beast::zero)
228  return tecINTERNAL;
229 
230  return accountSend(view(), from, to, amount, j_);
231 }
232 
233 TER
235 {
236  bool const isSell = offer->isFlag(lsfSellNFToken);
237  AccountID const owner = (*offer)[sfOwner];
238  AccountID const& seller = isSell ? owner : account_;
239  AccountID const& buyer = isSell ? account_ : owner;
240 
241  auto const nftokenID = (*offer)[sfNFTokenID];
242 
243  if (auto amount = offer->getFieldAmount(sfAmount); amount != beast::zero)
244  {
245  // Calculate the issuer's cut from this sale, if any:
246  if (auto const fee = nft::getTransferFee(nftokenID); fee != 0)
247  {
248  auto const cut = multiply(amount, nft::transferFeeAsRate(fee));
249 
250  if (auto const issuer = nft::getIssuer(nftokenID);
251  cut != beast::zero && seller != issuer && buyer != issuer)
252  {
253  if (auto const r = pay(buyer, issuer, cut); !isTesSuccess(r))
254  return r;
255  amount -= cut;
256  }
257  }
258 
259  // Send the remaining funds to the seller of the NFT
260  if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
261  return r;
262  }
263 
264  // Now transfer the NFT:
265  auto tokenAndPage = nft::findTokenAndPage(view(), seller, nftokenID);
266 
267  if (!tokenAndPage)
268  return tecINTERNAL;
269 
270  if (auto const ret = nft::removeToken(
271  view(), seller, nftokenID, std::move(tokenAndPage->page));
272  !isTesSuccess(ret))
273  return ret;
274 
275  return nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
276 }
277 
278 TER
280 {
281  auto const loadToken = [this](std::optional<uint256> const& id) {
283  if (id)
284  sle = view().peek(keylet::nftoffer(*id));
285  return sle;
286  };
287 
288  auto bo = loadToken(ctx_.tx[~sfNFTokenBuyOffer]);
289  auto so = loadToken(ctx_.tx[~sfNFTokenSellOffer]);
290 
291  if (bo && !nft::deleteTokenOffer(view(), bo))
292  {
293  JLOG(j_.fatal()) << "Unable to delete buy offer '"
294  << to_string(bo->key()) << "': ignoring";
295  return tecINTERNAL;
296  }
297 
298  if (so && !nft::deleteTokenOffer(view(), so))
299  {
300  JLOG(j_.fatal()) << "Unable to delete sell offer '"
301  << to_string(so->key()) << "': ignoring";
302  return tecINTERNAL;
303  }
304 
305  // Bridging two different offers
306  if (bo && so)
307  {
308  AccountID const buyer = (*bo)[sfOwner];
309  AccountID const seller = (*so)[sfOwner];
310 
311  auto const nftokenID = (*so)[sfNFTokenID];
312 
313  // The amount is what the buyer of the NFT pays:
314  STAmount amount = (*bo)[sfAmount];
315 
316  // Three different folks may be paid. The order of operations is
317  // important.
318  //
319  // o The broker is paid the cut they requested.
320  // o The issuer's cut is calculated from what remains after the
321  // broker is paid. The issuer can take up to 50% of the remainder.
322  // o Finally, the seller gets whatever is left.
323  //
324  // It is important that the issuer's cut be calculated after the
325  // broker's portion is already removed. Calculating the issuer's
326  // cut before the broker's cut is removed can result in more money
327  // being paid out than the seller authorized. That would be bad!
328 
329  // Send the broker the amount they requested.
330  if (auto const cut = ctx_.tx[~sfNFTokenBrokerFee];
331  cut && cut.value() != beast::zero)
332  {
333  if (auto const r = pay(buyer, account_, cut.value());
334  !isTesSuccess(r))
335  return r;
336 
337  amount -= cut.value();
338  }
339 
340  // Calculate the issuer's cut, if any.
341  if (auto const fee = nft::getTransferFee(nftokenID);
342  amount != beast::zero && fee != 0)
343  {
344  auto cut = multiply(amount, nft::transferFeeAsRate(fee));
345 
346  if (auto const issuer = nft::getIssuer(nftokenID);
347  seller != issuer && buyer != issuer)
348  {
349  if (auto const r = pay(buyer, issuer, cut); !isTesSuccess(r))
350  return r;
351 
352  amount -= cut;
353  }
354  }
355 
356  // And send whatever remains to the seller.
357  if (amount > beast::zero)
358  {
359  if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
360  return r;
361  }
362 
363  auto tokenAndPage = nft::findTokenAndPage(view(), seller, nftokenID);
364 
365  if (!tokenAndPage)
366  return tecINTERNAL;
367 
368  if (auto const ret = nft::removeToken(
369  view(), seller, nftokenID, std::move(tokenAndPage->page));
370  !isTesSuccess(ret))
371  return ret;
372 
373  return nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
374  }
375 
376  if (bo)
377  return acceptOffer(bo);
378 
379  if (so)
380  return acceptOffer(so);
381 
382  return tecINTERNAL;
383 }
384 
385 } // 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:221
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:234
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:279
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