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