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