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