rippled
Loading...
Searching...
No Matches
NFTokenAuth_test.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2025 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 <test/jtx.h>
21
22#include <xrpld/app/tx/detail/NFTokenUtils.h>
23
24namespace ripple {
25
27{
28 auto
30 test::jtx::Env& env,
31 test::jtx::Account const& account,
32 test::jtx::PrettyAmount const& currency,
33 uint32_t xfee = 0u)
34 {
35 using namespace test::jtx;
36 auto const nftID{
37 token::getNextID(env, account, 0u, tfTransferable, xfee)};
38 env(token::mint(account, 0),
39 token::xferFee(xfee),
40 txflags(tfTransferable));
41 env.close();
42
43 auto const sellIdx = keylet::nftoffer(account, env.seq(account)).key;
44 env(token::createOffer(account, nftID, currency),
45 txflags(tfSellNFToken));
46 env.close();
47
48 return std::make_tuple(nftID, sellIdx);
49 }
50
51public:
52 void
54 {
55 testcase("Unauthorized seller tries to accept buy offer");
56 using namespace test::jtx;
57
58 Env env(*this, features);
59 Account G1{"G1"};
60 Account A1{"A1"};
61 Account A2{"A2"};
62 auto const USD{G1["USD"]};
63
64 env.fund(XRP(10000), G1, A1, A2);
65 env(fset(G1, asfRequireAuth));
66 env.close();
67
68 auto const limit = USD(10000);
69
70 env(trust(A1, limit));
71 env(trust(G1, limit, A1, tfSetfAuth));
72 env(pay(G1, A1, USD(1000)));
73
74 auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1));
75 auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
76
77 // It should be possible to create a buy offer even if NFT owner is not
78 // authorized
79 env(token::createOffer(A1, nftID, USD(10)), token::owner(A2));
80
81 if (features[fixEnforceNFTokenTrustlineV2])
82 {
83 // test: G1 requires authorization of A2, no trust line exists
84 env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_LINE));
85 env.close();
86
87 // trust line created, but not authorized
88 env(trust(A2, limit));
89
90 // test: G1 requires authorization of A2
91 env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_AUTH));
92 env.close();
93 }
94 else
95 {
96 // Old behavior: it is possible to sell tokens and receive IOUs
97 // without the authorization
98 env(token::acceptBuyOffer(A2, buyIdx));
99 env.close();
100
101 BEAST_EXPECT(env.balance(A2, USD) == USD(10));
102 }
103 }
104
105 void
107 {
108 testcase("Unauthorized buyer tries to create buy offer");
109 using namespace test::jtx;
110
111 Env env(*this, features);
112 Account G1{"G1"};
113 Account A1{"A1"};
114 Account A2{"A2"};
115 auto const USD{G1["USD"]};
116
117 env.fund(XRP(10000), G1, A1, A2);
118 env(fset(G1, asfRequireAuth));
119 env.close();
120
121 auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1));
122
123 // test: check that buyer can't make an offer if they're not authorized.
124 env(token::createOffer(A1, nftID, USD(10)),
125 token::owner(A2),
126 ter(tecUNFUNDED_OFFER));
127 env.close();
128
129 // Artificially create an unauthorized trustline with balance. Don't
130 // close ledger before running the actual tests against this trustline.
131 // After ledger is closed, the trustline will not exist.
132 auto const unauthTrustline = [&](OpenView& view,
133 beast::Journal) -> bool {
134 auto const sleA1 =
135 std::make_shared<SLE>(keylet::line(A1, G1, G1["USD"].currency));
136 sleA1->setFieldAmount(sfBalance, A1["USD"](-1000));
137 view.rawInsert(sleA1);
138 return true;
139 };
140 env.app().openLedger().modify(unauthTrustline);
141
142 if (features[fixEnforceNFTokenTrustlineV2])
143 {
144 // test: check that buyer can't make an offer even with balance
145 env(token::createOffer(A1, nftID, USD(10)),
146 token::owner(A2),
147 ter(tecNO_AUTH));
148 }
149 else
150 {
151 // old behavior: can create an offer if balance allows, regardless
152 // ot authorization
153 env(token::createOffer(A1, nftID, USD(10)), token::owner(A2));
154 }
155 }
156
157 void
159 {
160 testcase("Seller tries to accept buy offer from unauth buyer");
161 using namespace test::jtx;
162
163 Env env(*this, features);
164 Account G1{"G1"};
165 Account A1{"A1"};
166 Account A2{"A2"};
167 auto const USD{G1["USD"]};
168
169 env.fund(XRP(10000), G1, A1, A2);
170 env(fset(G1, asfRequireAuth));
171 env.close();
172
173 auto const limit = USD(10000);
174
175 auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1));
176
177 // First we authorize buyer and seller so that he can create buy offer
178 env(trust(A1, limit));
179 env(trust(G1, limit, A1, tfSetfAuth));
180 env(pay(G1, A1, USD(10)));
181 env(trust(A2, limit));
182 env(trust(G1, limit, A2, tfSetfAuth));
183 env(pay(G1, A2, USD(10)));
184 env.close();
185
186 auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
187 env(token::createOffer(A1, nftID, USD(10)), token::owner(A2));
188 env.close();
189
190 env(pay(A1, G1, USD(10)));
191 env(trust(A1, USD(0)));
192 env(trust(G1, A1["USD"](0)));
193 env.close();
194
195 // Replace an existing authorized trustline with artificial unauthorized
196 // trustline with balance. Don't close ledger before running the actual
197 // tests against this trustline. After ledger is closed, the trustline
198 // will not exist.
199 auto const unauthTrustline = [&](OpenView& view,
200 beast::Journal) -> bool {
201 auto const sleA1 =
202 std::make_shared<SLE>(keylet::line(A1, G1, G1["USD"].currency));
203 sleA1->setFieldAmount(sfBalance, A1["USD"](-1000));
204 view.rawInsert(sleA1);
205 return true;
206 };
207 env.app().openLedger().modify(unauthTrustline);
208 if (features[fixEnforceNFTokenTrustlineV2])
209 {
210 // test: check that offer can't be accepted even with balance
211 env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_AUTH));
212 }
213 }
214
215 void
217 {
218 testcase(
219 "Authorized buyer tries to accept sell offer from unauthorized "
220 "seller");
221 using namespace test::jtx;
222
223 Env env(*this, features);
224 Account G1{"G1"};
225 Account A1{"A1"};
226 Account A2{"A2"};
227 auto const USD{G1["USD"]};
228
229 env.fund(XRP(10000), G1, A1, A2);
230 env(fset(G1, asfRequireAuth));
231 env.close();
232
233 auto const limit = USD(10000);
234
235 env(trust(A1, limit));
236 env(trust(G1, limit, A1, tfSetfAuth));
237 env(pay(G1, A1, USD(1000)));
238
239 auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1));
240 if (features[fixEnforceNFTokenTrustlineV2])
241 {
242 // test: can't create sell offer if there is no trustline but auth
243 // required
244 env(token::createOffer(A2, nftID, USD(10)),
245 txflags(tfSellNFToken),
246 ter(tecNO_LINE));
247
248 env(trust(A2, limit));
249 // test: can't create sell offer if not authorized to hold token
250 env(token::createOffer(A2, nftID, USD(10)),
251 txflags(tfSellNFToken),
252 ter(tecNO_AUTH));
253
254 // Authorizing trustline to make an offer creation possible
255 env(trust(G1, USD(0), A2, tfSetfAuth));
256 env.close();
257 auto const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key;
258 env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken));
259 env.close();
260 //
261
262 // Reseting trustline to delete it. This allows to check if
263 // already existing offers handled correctly
264 env(trust(A2, USD(0)));
265 env.close();
266
267 // test: G1 requires authorization of A1, no trust line exists
268 env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_LINE));
269 env.close();
270
271 // trust line created, but not authorized
272 env(trust(A2, limit));
273 env.close();
274
275 // test: G1 requires authorization of A1
276 env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_AUTH));
277 env.close();
278 }
279 else
280 {
281 auto const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key;
282
283 // Old behavior: sell offer can be created without authorization
284 env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken));
285 env.close();
286
287 // Old behavior: it is possible to sell NFT and receive IOUs
288 // without the authorization
289 env(token::acceptSellOffer(A1, sellIdx));
290 env.close();
291
292 BEAST_EXPECT(env.balance(A2, USD) == USD(10));
293 }
294 }
295
296 void
298 {
299 testcase("Unauthorized buyer tries to accept sell offer");
300 using namespace test::jtx;
301
302 Env env(*this, features);
303 Account G1{"G1"};
304 Account A1{"A1"};
305 Account A2{"A2"};
306 auto const USD{G1["USD"]};
307
308 env.fund(XRP(10000), G1, A1, A2);
309 env(fset(G1, asfRequireAuth));
310 env.close();
311
312 auto const limit = USD(10000);
313
314 env(trust(A2, limit));
315 env(trust(G1, limit, A2, tfSetfAuth));
316
317 auto const [_, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
318
319 // test: check that buyer can't accept an offer if they're not
320 // authorized.
321 env(token::acceptSellOffer(A1, sellIdx), ter(tecINSUFFICIENT_FUNDS));
322 env.close();
323
324 // Creating an artificial unauth trustline
325 auto const unauthTrustline = [&](OpenView& view,
326 beast::Journal) -> bool {
327 auto const sleA1 =
328 std::make_shared<SLE>(keylet::line(A1, G1, G1["USD"].currency));
329 sleA1->setFieldAmount(sfBalance, A1["USD"](-1000));
330 view.rawInsert(sleA1);
331 return true;
332 };
333 env.app().openLedger().modify(unauthTrustline);
334 if (features[fixEnforceNFTokenTrustlineV2])
335 {
336 env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_AUTH));
337 }
338 }
339
340 void
342 {
343 testcase("Unauthorized broker bridges authorized buyer and seller.");
344 using namespace test::jtx;
345
346 Env env(*this, features);
347 Account G1{"G1"};
348 Account A1{"A1"};
349 Account A2{"A2"};
350 Account broker{"broker"};
351 auto const USD{G1["USD"]};
352
353 env.fund(XRP(10000), G1, A1, A2, broker);
354 env(fset(G1, asfRequireAuth));
355 env.close();
356
357 auto const limit = USD(10000);
358
359 env(trust(A1, limit));
360 env(trust(G1, limit, A1, tfSetfAuth));
361 env(pay(G1, A1, USD(1000)));
362 env(trust(A2, limit));
363 env(trust(G1, limit, A2, tfSetfAuth));
364 env(pay(G1, A2, USD(1000)));
365 env.close();
366
367 auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
368 auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
369 env(token::createOffer(A1, nftID, USD(11)), token::owner(A2));
370 env.close();
371
372 if (features[fixEnforceNFTokenTrustlineV2])
373 {
374 // test: G1 requires authorization of broker, no trust line exists
375 env(token::brokerOffers(broker, buyIdx, sellIdx),
376 token::brokerFee(USD(1)),
377 ter(tecNO_LINE));
378 env.close();
379
380 // trust line created, but not authorized
381 env(trust(broker, limit));
382 env.close();
383
384 // test: G1 requires authorization of broker
385 env(token::brokerOffers(broker, buyIdx, sellIdx),
386 token::brokerFee(USD(1)),
387 ter(tecNO_AUTH));
388 env.close();
389
390 // test: can still be brokered without broker fee.
391 env(token::brokerOffers(broker, buyIdx, sellIdx));
392 env.close();
393 }
394 else
395 {
396 // Old behavior: broker can receive IOUs without the authorization
397 env(token::brokerOffers(broker, buyIdx, sellIdx),
398 token::brokerFee(USD(1)));
399 env.close();
400
401 BEAST_EXPECT(env.balance(broker, USD) == USD(1));
402 }
403 }
404
405 void
407 {
408 testcase(
409 "Authorized broker tries to bridge offers from unauthorized "
410 "buyer.");
411 using namespace test::jtx;
412
413 Env env(*this, features);
414 Account G1{"G1"};
415 Account A1{"A1"};
416 Account A2{"A2"};
417 Account broker{"broker"};
418 auto const USD{G1["USD"]};
419
420 env.fund(XRP(10000), G1, A1, A2, broker);
421 env(fset(G1, asfRequireAuth));
422 env.close();
423
424 auto const limit = USD(10000);
425
426 env(trust(A1, limit));
427 env(trust(G1, USD(0), A1, tfSetfAuth));
428 env(pay(G1, A1, USD(1000)));
429 env(trust(A2, limit));
430 env(trust(G1, USD(0), A2, tfSetfAuth));
431 env(pay(G1, A2, USD(1000)));
432 env(trust(broker, limit));
433 env(trust(G1, USD(0), broker, tfSetfAuth));
434 env(pay(G1, broker, USD(1000)));
435 env.close();
436
437 auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
438 auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
439 env(token::createOffer(A1, nftID, USD(11)), token::owner(A2));
440 env.close();
441
442 // Resetting buyer's trust line to delete it
443 env(pay(A1, G1, USD(1000)));
444 env(trust(A1, USD(0)));
445 env.close();
446
447 auto const unauthTrustline = [&](OpenView& view,
448 beast::Journal) -> bool {
449 auto const sleA1 =
450 std::make_shared<SLE>(keylet::line(A1, G1, G1["USD"].currency));
451 sleA1->setFieldAmount(sfBalance, A1["USD"](-1000));
452 view.rawInsert(sleA1);
453 return true;
454 };
455 env.app().openLedger().modify(unauthTrustline);
456
457 if (features[fixEnforceNFTokenTrustlineV2])
458 {
459 // test: G1 requires authorization of A2
460 env(token::brokerOffers(broker, buyIdx, sellIdx),
461 token::brokerFee(USD(1)),
462 ter(tecNO_AUTH));
463 env.close();
464 }
465 }
466
467 void
469 {
470 testcase(
471 "Authorized broker tries to bridge offers from unauthorized "
472 "seller.");
473 using namespace test::jtx;
474
475 Env env(*this, features);
476 Account G1{"G1"};
477 Account A1{"A1"};
478 Account A2{"A2"};
479 Account broker{"broker"};
480 auto const USD{G1["USD"]};
481
482 env.fund(XRP(10000), G1, A1, A2, broker);
483 env(fset(G1, asfRequireAuth));
484 env.close();
485
486 auto const limit = USD(10000);
487
488 env(trust(A1, limit));
489 env(trust(G1, limit, A1, tfSetfAuth));
490 env(pay(G1, A1, USD(1000)));
491 env(trust(broker, limit));
492 env(trust(G1, limit, broker, tfSetfAuth));
493 env(pay(G1, broker, USD(1000)));
494 env.close();
495
496 // Authorizing trustline to make an offer creation possible
497 env(trust(G1, USD(0), A2, tfSetfAuth));
498 env.close();
499
500 auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
501 auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
502 env(token::createOffer(A1, nftID, USD(11)), token::owner(A2));
503 env.close();
504
505 // Reseting trustline to delete it. This allows to check if
506 // already existing offers handled correctly
507 env(trust(A2, USD(0)));
508 env.close();
509
510 if (features[fixEnforceNFTokenTrustlineV2])
511 {
512 // test: G1 requires authorization of broker, no trust line exists
513 env(token::brokerOffers(broker, buyIdx, sellIdx),
514 token::brokerFee(USD(1)),
515 ter(tecNO_LINE));
516 env.close();
517
518 // trust line created, but not authorized
519 env(trust(A2, limit));
520 env.close();
521
522 // test: G1 requires authorization of A2
523 env(token::brokerOffers(broker, buyIdx, sellIdx),
524 token::brokerFee(USD(1)),
525 ter(tecNO_AUTH));
526 env.close();
527
528 // test: cannot be brokered even without broker fee.
529 env(token::brokerOffers(broker, buyIdx, sellIdx), ter(tecNO_AUTH));
530 env.close();
531 }
532 else
533 {
534 // Old behavior: broker can receive IOUs without the authorization
535 env(token::brokerOffers(broker, buyIdx, sellIdx),
536 token::brokerFee(USD(1)));
537 env.close();
538
539 BEAST_EXPECT(env.balance(A2, USD) == USD(10));
540 return;
541 }
542 }
543
544 void
546 {
547 testcase("Unauthorized minter receives transfer fee.");
548 using namespace test::jtx;
549
550 Env env(*this, features);
551 Account G1{"G1"};
552 Account minter{"minter"};
553 Account A1{"A1"};
554 Account A2{"A2"};
555 auto const USD{G1["USD"]};
556
557 env.fund(XRP(10000), G1, minter, A1, A2);
558 env(fset(G1, asfRequireAuth));
559 env.close();
560
561 auto const limit = USD(10000);
562
563 env(trust(A1, limit));
564 env(trust(G1, limit, A1, tfSetfAuth));
565 env(pay(G1, A1, USD(1000)));
566 env(trust(A2, limit));
567 env(trust(G1, limit, A2, tfSetfAuth));
568 env(pay(G1, A2, USD(1000)));
569
570 env(trust(minter, limit));
571 env.close();
572
573 // We authorized A1 and A2, but not the minter.
574 // Now mint NFT
575 auto const [nftID, minterSellIdx] =
576 mintAndOfferNFT(env, minter, drops(1), 1);
577 env(token::acceptSellOffer(A1, minterSellIdx));
578
579 uint256 const sellIdx = keylet::nftoffer(A1, env.seq(A1)).key;
580 env(token::createOffer(A1, nftID, USD(100)), txflags(tfSellNFToken));
581
582 if (features[fixEnforceNFTokenTrustlineV2])
583 {
584 // test: G1 requires authorization
585 env(token::acceptSellOffer(A2, sellIdx), ter(tecNO_AUTH));
586 env.close();
587 }
588 else
589 {
590 // Old behavior: can sell for USD. Minter can receive tokens
591 env(token::acceptSellOffer(A2, sellIdx));
592 env.close();
593
594 BEAST_EXPECT(env.balance(minter, USD) == USD(0.001));
595 }
596 }
597
598 void
599 run() override
600 {
601 using namespace test::jtx;
602 static FeatureBitset const all{supported_amendments()};
603
604 static std::array const features = {
605 all - fixEnforceNFTokenTrustlineV2, all};
606
607 for (auto const feature : features)
608 {
618 }
619 }
620};
621
622BEAST_DEFINE_TESTSUITE_PRIO(NFTokenAuth, tx, ripple, 2);
623
624} // namespace ripple
A generic endpoint for log messages.
Definition: Journal.h:60
A testsuite class.
Definition: suite.h:55
testcase_t testcase
Memberspace for declaring test cases.
Definition: suite.h:155
void testSellOffer_UnauthorizedBuyer(FeatureBitset features)
void run() override
Runs the suite.
void testCreateBuyOffer_UnauthorizedBuyer(FeatureBitset features)
void testBrokeredAcceptOffer_UnauthorizedBroker(FeatureBitset features)
void testBrokeredAcceptOffer_UnauthorizedSeller(FeatureBitset features)
void testTransferFee_UnauthorizedMinter(FeatureBitset features)
void testSellOffer_UnauthorizedSeller(FeatureBitset features)
void testBuyOffer_UnauthorizedSeller(FeatureBitset features)
auto mintAndOfferNFT(test::jtx::Env &env, test::jtx::Account const &account, test::jtx::PrettyAmount const &currency, uint32_t xfee=0u)
void testBrokeredAcceptOffer_UnauthorizedBuyer(FeatureBitset features)
void testAcceptBuyOffer_UnauthorizedBuyer(FeatureBitset features)
Writable ledger view that accumulates state and tx changes.
Definition: OpenView.h:66
void rawInsert(std::shared_ptr< SLE > const &sle) override
Unconditionally insert a state item.
Definition: OpenView.cpp:238
Immutable cryptographic account descriptor.
Definition: Account.h:39
A transaction testing environment.
Definition: Env.h:121
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition: Env.cpp:254
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition: Env.cpp:117
T make_tuple(T... args)
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 nftoffer(AccountID const &owner, std::uint32_t seq)
An offer from an account to buy or sell an NFT.
Definition: Indexes.cpp:427
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: algorithm.h:25
constexpr std::uint32_t const tfSellNFToken
Definition: TxFlags.h:194
constexpr std::uint32_t tfSetfAuth
Definition: TxFlags.h:115
@ tecUNFUNDED_OFFER
Definition: TER.h:284
@ tecINSUFFICIENT_FUNDS
Definition: TER.h:325
@ tecNO_LINE
Definition: TER.h:301
@ tecNO_AUTH
Definition: TER.h:300
constexpr std::uint32_t asfRequireAuth
Definition: TxFlags.h:78
constexpr std::uint32_t const tfTransferable
Definition: TxFlags.h:140
uint256 key
Definition: Keylet.h:40
Represents an XRP or IOU quantity This customizes the string conversion and supports XRP conversions ...