Add the fixEnforceNFTokenTrustline amendment: (#4946)

Fix interactions between NFTokenOffers and trust lines.

Since the NFTokenAcceptOffer does not check the trust line that
the issuer receives as a transfer fee in the NFTokenAcceptOffer,
if the issuer deletes the trust line after NFTokenCreateOffer,
the trust line is created for the issuer by the
NFTokenAcceptOffer.  That's fixed.

Resolves #4925.
This commit is contained in:
Scott Schurr
2024-06-17 23:47:54 -07:00
committed by tequ
parent 7996d08d0c
commit 01971ab1b9
5 changed files with 202 additions and 7 deletions

View File

@@ -7424,6 +7424,176 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
}
}
void
testUnaskedForAutoTrustline(FeatureBitset features)
{
testcase("Test fix unasked for auto-trustline.");
using namespace test::jtx;
Account const issuer{"issuer"};
Account const becky{"becky"};
Account const cheri{"cheri"};
Account const gw("gw");
IOU const gwAUD(gw["AUD"]);
// This test case covers issue...
// https://github.com/XRPLF/rippled/issues/4925
//
// For an NFToken with a transfer fee, the issuer must be able to
// accept the transfer fee or else a transfer should fail. If the
// NFToken is transferred for a non-XRP asset, then the issuer must
// have a trustline to that asset to receive the fee.
//
// This test looks at a situation where issuer would get a trustline
// for the fee without the issuer's consent. Here are the steps:
// 1. Issuer has a trustline (i.e., USD)
// 2. Issuer mints NFToken with transfer fee.
// 3. Becky acquires the NFToken, paying with XRP.
// 4. Becky creates offer to sell NFToken for USD(100).
// 5. Issuer deletes trustline for USD.
// 6. Carol buys NFToken from Becky for USD(100).
// 7. The transfer fee from Carol's purchase re-establishes issuer's
// USD trustline.
//
// The fixEnforceNFTokenTrustline amendment addresses this oversight.
//
// We run this test case both with and without
// fixEnforceNFTokenTrustline enabled so we can see the change
// in behavior.
//
// In both cases we remove the fixRemoveNFTokenAutoTrustLine amendment.
// Otherwise we can't create NFTokens with tfTrustLine enabled.
FeatureBitset const localFeatures =
features - fixRemoveNFTokenAutoTrustLine;
for (FeatureBitset feats :
{localFeatures - fixEnforceNFTokenTrustline,
localFeatures | fixEnforceNFTokenTrustline})
{
Env env{*this, feats};
env.fund(XRP(1000), issuer, becky, cheri, gw);
env.close();
// Set trust lines so becky and cheri can use gw's currency.
env(trust(becky, gwAUD(1000)));
env(trust(cheri, gwAUD(1000)));
env.close();
env(pay(gw, cheri, gwAUD(500)));
env.close();
// issuer creates two NFTs: one with and one without AutoTrustLine.
std::uint16_t xferFee = 5000; // 5%
uint256 const nftAutoTrustID{token::getNextID(
env, issuer, 0u, tfTransferable | tfTrustLine, xferFee)};
env(token::mint(issuer, 0u),
token::xferFee(xferFee),
txflags(tfTransferable | tfTrustLine));
env.close();
uint256 const nftNoAutoTrustID{
token::getNextID(env, issuer, 0u, tfTransferable, xferFee)};
env(token::mint(issuer, 0u),
token::xferFee(xferFee),
txflags(tfTransferable));
env.close();
// becky buys the nfts for 1 drop each.
{
uint256 const beckyBuyOfferIndex1 =
keylet::nftoffer(becky, env.seq(becky)).key;
env(token::createOffer(becky, nftAutoTrustID, drops(1)),
token::owner(issuer));
uint256 const beckyBuyOfferIndex2 =
keylet::nftoffer(becky, env.seq(becky)).key;
env(token::createOffer(becky, nftNoAutoTrustID, drops(1)),
token::owner(issuer));
env.close();
env(token::acceptBuyOffer(issuer, beckyBuyOfferIndex1));
env(token::acceptBuyOffer(issuer, beckyBuyOfferIndex2));
env.close();
}
// becky creates offers to sell the nfts for AUD.
uint256 const beckyAutoTrustOfferIndex =
keylet::nftoffer(becky, env.seq(becky)).key;
env(token::createOffer(becky, nftAutoTrustID, gwAUD(100)),
txflags(tfSellNFToken));
env.close();
// Creating an offer for the NFToken without tfTrustLine fails
// because issuer does not have a trust line for AUD.
env(token::createOffer(becky, nftNoAutoTrustID, gwAUD(100)),
txflags(tfSellNFToken),
ter(tecNO_LINE));
env.close();
// issuer creates a trust line. Now the offer create for the
// NFToken without tfTrustLine succeeds.
BEAST_EXPECT(ownerCount(env, issuer) == 0);
env(trust(issuer, gwAUD(1000)));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 1);
uint256 const beckyNoAutoTrustOfferIndex =
keylet::nftoffer(becky, env.seq(becky)).key;
env(token::createOffer(becky, nftNoAutoTrustID, gwAUD(100)),
txflags(tfSellNFToken));
env.close();
// Now that the offers are in place, issuer removes the trustline.
BEAST_EXPECT(ownerCount(env, issuer) == 1);
env(trust(issuer, gwAUD(0)));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
// cheri attempts to accept becky's offers. Behavior with the
// AutoTrustline NFT is uniform: issuer gets a new trust line.
env(token::acceptSellOffer(cheri, beckyAutoTrustOfferIndex));
env.close();
// Here's evidence that issuer got the new AUD trust line.
BEAST_EXPECT(ownerCount(env, issuer) == 1);
BEAST_EXPECT(env.balance(issuer, gwAUD) == gwAUD(5));
// issuer once again removes the trust line for AUD.
env(pay(issuer, gw, gwAUD(5)));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
// cheri attempts to accept the NoAutoTrustLine NFT. Behavior
// depends on whether fixEnforceNFTokenTrustline is enabled.
if (feats[fixEnforceNFTokenTrustline])
{
// With fixEnforceNFTokenTrustline cheri can't accept the
// offer because issuer could not get their transfer fee
// without the appropriate trustline.
env(token::acceptSellOffer(cheri, beckyNoAutoTrustOfferIndex),
ter(tecNO_LINE));
env.close();
// But if issuer re-establishes the trustline then the offer
// can be accepted.
env(trust(issuer, gwAUD(1000)));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 1);
env(token::acceptSellOffer(cheri, beckyNoAutoTrustOfferIndex));
env.close();
}
else
{
// Without fixEnforceNFTokenTrustline the offer just works
// and issuer gets a trustline that they did not request.
env(token::acceptSellOffer(cheri, beckyNoAutoTrustOfferIndex));
env.close();
}
BEAST_EXPECT(ownerCount(env, issuer) == 1);
BEAST_EXPECT(env.balance(issuer, gwAUD) == gwAUD(5));
} // for feats
}
void
testNFTIssuerIsIOUIssuer(FeatureBitset features)
{
@@ -7617,6 +7787,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
testFeatMintWithOffer(features);
testTxJsonMetaFields(features);
testFixNFTokenBuyerReserve(features);
testUnaskedForAutoTrustline(features);
testNFTIssuerIsIOUIssuer(features);
}

View File

@@ -5680,12 +5680,12 @@ class Offer_manual_test : public OfferBaseUtil_test
}
};
BEAST_DEFINE_TESTSUITE_PRIO(OfferBaseUtil, tx, ripple, 4);
BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFlowCross, tx, ripple, 4);
BEAST_DEFINE_TESTSUITE_PRIO(OfferWTakerDryOffer, tx, ripple, 4);
BEAST_DEFINE_TESTSUITE_PRIO(OfferWOSmallQOffers, tx, ripple, 4);
BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFillOrKill, tx, ripple, 4);
BEAST_DEFINE_TESTSUITE_PRIO(OfferAllFeatures, tx, ripple, 4);
BEAST_DEFINE_TESTSUITE_PRIO(OfferBaseUtil, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFlowCross, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(OfferWTakerDryOffer, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(OfferWOSmallQOffers, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFillOrKill, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(OfferAllFeatures, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(Offer_manual, tx, ripple, 20);
} // namespace test