@@ -4607,7 +4607,7 @@ class NFToken_test : public beast::unit_test::suite
4607
4607
txflags (tfTransferable));
4608
4608
env.close ();
4609
4609
4610
- // At the momement issuer and minter cannot delete themselves.
4610
+ // At the moment issuer and minter cannot delete themselves.
4611
4611
// o issuer has an issued NFT in the ledger.
4612
4612
// o minter owns an NFT.
4613
4613
env (acctdelete (issuer, daria), fee (XRP (50 )), ter (tecHAS_OBLIGATIONS));
@@ -5888,6 +5888,115 @@ class NFToken_test : public beast::unit_test::suite
5888
5888
}
5889
5889
}
5890
5890
5891
+ void
5892
+ testBrokeredSaleToSelf (FeatureBitset features)
5893
+ {
5894
+ // There was a bug that if an account had...
5895
+ //
5896
+ // 1. An NFToken, and
5897
+ // 2. An offer on the ledger to buy that same token, and
5898
+ // 3. Also an offer of the ledger to sell that same token,
5899
+ //
5900
+ // Then someone could broker the two offers. This would result in
5901
+ // the NFToken being bought and returned to the original owner and
5902
+ // the broker pocketing the profit.
5903
+ //
5904
+ // This unit test verifies that the fixUnburnableNFToken amendment
5905
+ // fixes that bug.
5906
+ testcase (" Brokered sale to self" );
5907
+
5908
+ using namespace test ::jtx;
5909
+
5910
+ Account const alice{" alice" };
5911
+ Account const bob{" bob" };
5912
+ Account const broker{" broker" };
5913
+
5914
+ Env env{*this , features};
5915
+ env.fund (XRP (10000 ), alice, bob, broker);
5916
+ env.close ();
5917
+
5918
+ // For this scenario to occur we need the following steps:
5919
+ //
5920
+ // 1. alice mints NFT.
5921
+ // 2. bob creates a buy offer for it for 5 XRP.
5922
+ // 3. alice decides to gift the NFT to bob for 0.
5923
+ // creating a sell offer (hopefully using a destination too)
5924
+ // 4. Bob accepts the sell offer, because it is better than
5925
+ // paying 5 XRP.
5926
+ // 5. At this point, bob has the NFT and still has their buy
5927
+ // offer from when they did not have the NFT! This is because
5928
+ // the order book is not cleared when an NFT changes hands.
5929
+ // 6. Now that Bob owns the NFT, he cannot create new buy offers.
5930
+ // However he still has one left over from when he did not own
5931
+ // it. He can create new sell offers and does.
5932
+ // 7. Now that bob has both a buy and a sell offer for the same NFT,
5933
+ // a broker can sell the NFT that bob owns to bob and pocket the
5934
+ // difference.
5935
+ uint256 const nftId{token::getNextID (env, alice, 0u , tfTransferable)};
5936
+ env (token::mint (alice, 0u ), txflags (tfTransferable));
5937
+ env.close ();
5938
+
5939
+ // Bob creates a buy offer for 5 XRP. Alice creates a sell offer
5940
+ // for 0 XRP.
5941
+ uint256 const bobBuyOfferIndex =
5942
+ keylet::nftoffer (bob, env.seq (bob)).key ;
5943
+ env (token::createOffer (bob, nftId, XRP (5 )), token::owner (alice));
5944
+
5945
+ uint256 const aliceSellOfferIndex =
5946
+ keylet::nftoffer (alice, env.seq (alice)).key ;
5947
+ env (token::createOffer (alice, nftId, XRP (0 )),
5948
+ token::destination (bob),
5949
+ txflags (tfSellNFToken));
5950
+ env.close ();
5951
+
5952
+ // bob accepts alice's offer but forgets to remove the old buy offer.
5953
+ env (token::acceptSellOffer (bob, aliceSellOfferIndex));
5954
+ env.close ();
5955
+
5956
+ // Note that bob still has a buy offer on the books.
5957
+ BEAST_EXPECT (env.le (keylet::nftoffer (bobBuyOfferIndex)));
5958
+
5959
+ // Bob creates a sell offer for the gift NFT from alice.
5960
+ uint256 const bobSellOfferIndex =
5961
+ keylet::nftoffer (bob, env.seq (bob)).key ;
5962
+ env (token::createOffer (bob, nftId, XRP (4 )), txflags (tfSellNFToken));
5963
+ env.close ();
5964
+
5965
+ // bob now has a buy offer and a sell offer on the books. A broker
5966
+ // spots this and swoops in to make a profit.
5967
+ BEAST_EXPECT (nftCount (env, bob) == 1 );
5968
+ auto const bobsPriorBalance = env.balance (bob);
5969
+ auto const brokersPriorBalance = env.balance (broker);
5970
+ TER expectTer = features[fixUnburnableNFToken]
5971
+ ? TER (tecCANT_ACCEPT_OWN_NFTOKEN_OFFER)
5972
+ : TER (tesSUCCESS);
5973
+ env (token::brokerOffers (broker, bobBuyOfferIndex, bobSellOfferIndex),
5974
+ token::brokerFee (XRP (1 )),
5975
+ ter (expectTer));
5976
+ env.close ();
5977
+
5978
+ if (expectTer == tesSUCCESS)
5979
+ {
5980
+ // bob should still have the NFT from alice, but be XRP(1) poorer.
5981
+ // broker should be almost XRP(1) richer because they also paid a
5982
+ // transaction fee.
5983
+ BEAST_EXPECT (nftCount (env, bob) == 1 );
5984
+ BEAST_EXPECT (env.balance (bob) == bobsPriorBalance - XRP (1 ));
5985
+ BEAST_EXPECT (
5986
+ env.balance (broker) ==
5987
+ brokersPriorBalance + XRP (1 ) - drops (10 ));
5988
+ }
5989
+ else
5990
+ {
5991
+ // A tec result was returned, so no state should change other
5992
+ // than the broker burning their transaction fee.
5993
+ BEAST_EXPECT (nftCount (env, bob) == 1 );
5994
+ BEAST_EXPECT (env.balance (bob) == bobsPriorBalance);
5995
+ BEAST_EXPECT (
5996
+ env.balance (broker) == brokersPriorBalance - drops (10 ));
5997
+ }
5998
+ }
5999
+
5891
6000
void
5892
6001
testWithFeats (FeatureBitset features)
5893
6002
{
@@ -5918,6 +6027,7 @@ class NFToken_test : public beast::unit_test::suite
5918
6027
testNftXxxOffers (features);
5919
6028
testFixNFTokenNegOffer (features);
5920
6029
testIOUWithTransferFee (features);
6030
+ testBrokeredSaleToSelf (features);
5921
6031
}
5922
6032
5923
6033
public:
@@ -5928,10 +6038,9 @@ class NFToken_test : public beast::unit_test::suite
5928
6038
FeatureBitset const all{supported_amendments ()};
5929
6039
FeatureBitset const fixNFTDir{fixNFTokenDirV1};
5930
6040
5931
- // TODO too many tests are being run - ths fixNFTDir check should be
5932
- // pushed into the tests that use it
5933
- testWithFeats (all - fixNFTDir);
5934
- testWithFeats (all - disallowIncoming);
6041
+ testWithFeats (all - fixNFTDir - fixUnburnableNFToken);
6042
+ testWithFeats (all - disallowIncoming - fixUnburnableNFToken);
6043
+ testWithFeats (all - fixUnburnableNFToken);
5935
6044
testWithFeats (all);
5936
6045
}
5937
6046
};
0 commit comments