rippled
Loading...
Searching...
No Matches
NFTokenAuth_test.cpp
1#include <test/jtx.h>
2
3#include <xrpld/app/tx/detail/NFTokenUtils.h>
4
5namespace xrpl {
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{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 G1{"G1"};
37 Account A1{"A1"};
38 Account 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 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().openLedger().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 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().openLedger().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 G1{"G1"};
194 Account A1{"A1"};
195 Account 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 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().openLedger().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 G1{"G1"};
311 Account A1{"A1"};
312 Account A2{"A2"};
313 Account 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), token::brokerFee(USD(1)), ter(tecNO_LINE));
339 env.close();
340
341 // trust line created, but not authorized
342 env(trust(broker, limit));
343 env.close();
344
345 // test: G1 requires authorization of broker
346 env(token::brokerOffers(broker, buyIdx, sellIdx), token::brokerFee(USD(1)), ter(tecNO_AUTH));
347 env.close();
348
349 // test: can still be brokered without broker fee.
350 env(token::brokerOffers(broker, buyIdx, sellIdx));
351 env.close();
352 }
353 else
354 {
355 // Old behavior: broker can receive IOUs without the authorization
356 env(token::brokerOffers(broker, buyIdx, sellIdx), token::brokerFee(USD(1)));
357 env.close();
358
359 BEAST_EXPECT(env.balance(broker, USD) == USD(1));
360 }
361 }
362
363 void
365 {
366 testcase(
367 "Authorized broker tries to bridge offers from unauthorized "
368 "buyer.");
369 using namespace test::jtx;
370
371 Env env(*this, features);
372 Account G1{"G1"};
373 Account A1{"A1"};
374 Account A2{"A2"};
375 Account broker{"broker"};
376 auto const USD{G1["USD"]};
377
378 env.fund(XRP(10000), G1, A1, A2, broker);
379 env(fset(G1, asfRequireAuth));
380 env.close();
381
382 auto const limit = USD(10000);
383
384 env(trust(A1, limit));
385 env(trust(G1, USD(0), A1, tfSetfAuth));
386 env(pay(G1, A1, USD(1000)));
387 env(trust(A2, limit));
388 env(trust(G1, USD(0), A2, tfSetfAuth));
389 env(pay(G1, A2, USD(1000)));
390 env(trust(broker, limit));
391 env(trust(G1, USD(0), broker, tfSetfAuth));
392 env(pay(G1, broker, USD(1000)));
393 env.close();
394
395 auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
396 auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
397 env(token::createOffer(A1, nftID, USD(11)), token::owner(A2));
398 env.close();
399
400 // Resetting buyer's trust line to delete it
401 env(pay(A1, G1, USD(1000)));
402 env(trust(A1, USD(0)));
403 env.close();
404
405 auto const unauthTrustline = [&](OpenView& view, beast::Journal) -> bool {
406 auto const sleA1 = std::make_shared<SLE>(keylet::line(A1, G1, G1["USD"].currency));
407 sleA1->setFieldAmount(sfBalance, A1["USD"](-1000));
408 view.rawInsert(sleA1);
409 return true;
410 };
411 env.app().openLedger().modify(unauthTrustline);
412
413 if (features[fixEnforceNFTokenTrustlineV2])
414 {
415 // test: G1 requires authorization of A2
416 env(token::brokerOffers(broker, buyIdx, sellIdx), token::brokerFee(USD(1)), ter(tecNO_AUTH));
417 env.close();
418 }
419 }
420
421 void
423 {
424 testcase(
425 "Authorized broker tries to bridge offers from unauthorized "
426 "seller.");
427 using namespace test::jtx;
428
429 Env env(*this, features);
430 Account G1{"G1"};
431 Account A1{"A1"};
432 Account A2{"A2"};
433 Account broker{"broker"};
434 auto const USD{G1["USD"]};
435
436 env.fund(XRP(10000), G1, A1, A2, broker);
437 env(fset(G1, asfRequireAuth));
438 env.close();
439
440 auto const limit = USD(10000);
441
442 env(trust(A1, limit));
443 env(trust(G1, limit, A1, tfSetfAuth));
444 env(pay(G1, A1, USD(1000)));
445 env(trust(broker, limit));
446 env(trust(G1, limit, broker, tfSetfAuth));
447 env(pay(G1, broker, USD(1000)));
448 env.close();
449
450 // Authorizing trustline to make an offer creation possible
451 env(trust(G1, USD(0), A2, tfSetfAuth));
452 env.close();
453
454 auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
455 auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
456 env(token::createOffer(A1, nftID, USD(11)), token::owner(A2));
457 env.close();
458
459 // Reseting trustline to delete it. This allows to check if
460 // already existing offers handled correctly
461 env(trust(A2, USD(0)));
462 env.close();
463
464 if (features[fixEnforceNFTokenTrustlineV2])
465 {
466 // test: G1 requires authorization of broker, no trust line exists
467 env(token::brokerOffers(broker, buyIdx, sellIdx), token::brokerFee(USD(1)), ter(tecNO_LINE));
468 env.close();
469
470 // trust line created, but not authorized
471 env(trust(A2, limit));
472 env.close();
473
474 // test: G1 requires authorization of A2
475 env(token::brokerOffers(broker, buyIdx, sellIdx), token::brokerFee(USD(1)), ter(tecNO_AUTH));
476 env.close();
477
478 // test: cannot be brokered even without broker fee.
479 env(token::brokerOffers(broker, buyIdx, sellIdx), ter(tecNO_AUTH));
480 env.close();
481 }
482 else
483 {
484 // Old behavior: broker can receive IOUs without the authorization
485 env(token::brokerOffers(broker, buyIdx, sellIdx), token::brokerFee(USD(1)));
486 env.close();
487
488 BEAST_EXPECT(env.balance(A2, USD) == USD(10));
489 return;
490 }
491 }
492
493 void
495 {
496 testcase("Unauthorized minter receives transfer fee.");
497 using namespace test::jtx;
498
499 Env env(*this, features);
500 Account G1{"G1"};
501 Account minter{"minter"};
502 Account A1{"A1"};
503 Account A2{"A2"};
504 auto const USD{G1["USD"]};
505
506 env.fund(XRP(10000), G1, minter, A1, A2);
507 env(fset(G1, asfRequireAuth));
508 env.close();
509
510 auto const limit = USD(10000);
511
512 env(trust(A1, limit));
513 env(trust(G1, limit, A1, tfSetfAuth));
514 env(pay(G1, A1, USD(1000)));
515 env(trust(A2, limit));
516 env(trust(G1, limit, A2, tfSetfAuth));
517 env(pay(G1, A2, USD(1000)));
518
519 env(trust(minter, limit));
520 env.close();
521
522 // We authorized A1 and A2, but not the minter.
523 // Now mint NFT
524 auto const [nftID, minterSellIdx] = mintAndOfferNFT(env, minter, drops(1), 1);
525 env(token::acceptSellOffer(A1, minterSellIdx));
526
527 uint256 const sellIdx = keylet::nftoffer(A1, env.seq(A1)).key;
528 env(token::createOffer(A1, nftID, USD(100)), txflags(tfSellNFToken));
529
530 if (features[fixEnforceNFTokenTrustlineV2])
531 {
532 // test: G1 requires authorization
533 env(token::acceptSellOffer(A2, sellIdx), ter(tecNO_AUTH));
534 env.close();
535 }
536 else
537 {
538 // Old behavior: can sell for USD. Minter can receive tokens
539 env(token::acceptSellOffer(A2, sellIdx));
540 env.close();
541
542 BEAST_EXPECT(env.balance(minter, USD) == USD(0.001));
543 }
544 }
545
546 void
547 run() override
548 {
549 using namespace test::jtx;
550 static FeatureBitset const all{testable_amendments()};
551
552 static std::array const features = {all - fixEnforceNFTokenTrustlineV2, all};
553
554 for (auto const feature : features)
555 {
565 }
566 }
567};
568
569BEAST_DEFINE_TESTSUITE_PRIO(NFTokenAuth, app, xrpl, 2);
570
571} // 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:147
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)
void run() override
Runs the suite.
void testSellOffer_UnauthorizedSeller(FeatureBitset features)
auto mintAndOfferNFT(test::jtx::Env &env, test::jtx::Account const &account, test::jtx::PrettyAmount const &currency, uint32_t xfee=0u)
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:207
Immutable cryptographic account descriptor.
Definition Account.h:19
A transaction testing environment.
Definition Env.h:119
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:98
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:240
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:375
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:214
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
constexpr std::uint32_t const tfTransferable
Definition TxFlags.h:122
constexpr std::uint32_t tfSetfAuth
Definition TxFlags.h:95
constexpr std::uint32_t asfRequireAuth
Definition TxFlags.h:58
@ tecNO_AUTH
Definition TER.h:281
@ tecINSUFFICIENT_FUNDS
Definition TER.h:306
@ tecUNFUNDED_OFFER
Definition TER.h:265
@ tecNO_LINE
Definition TER.h:282
constexpr std::uint32_t const tfSellNFToken
Definition TxFlags.h:210
uint256 key
Definition Keylet.h:20
Represents an XRP or IOU quantity This customizes the string conversion and supports XRP conversions ...