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