Introduce fixRemoveNFTokenAutoTrustLine amendment:

It turns out that the feature enabled by the tfTrustLine flag
on an NFTokenMint transaction could be used as a means to
attack the NFToken issuer.  Details are in
https://github.com/XRPLF/rippled/issues/4300

The fixRemoveNFTokenAutoTrustLine amendment removes the
ability to set the tfTrustLine flag on an NFTokenMint
transaction.

Closes 4300.
This commit is contained in:
Scott Schurr
2022-09-13 10:16:31 -07:00
committed by Nik Bougalis
parent f5af42a640
commit e40e38e8d3
5 changed files with 192 additions and 135 deletions

View File

@@ -40,7 +40,23 @@ NFTokenMint::preflight(PreflightContext const& ctx)
if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret; return ret;
if (ctx.tx.getFlags() & tfNFTokenMintMask) // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between
// accounts allowed a TrustLine to be added to the issuer of that token
// without explicit permission from that issuer. This was enabled by
// minting the NFToken with the tfTrustLine flag set.
//
// That capability could be used to attack the NFToken issuer. It
// would be possible for two accounts to trade the NFToken back and forth
// building up any number of TrustLines on the issuer, increasing the
// issuer's reserve without bound.
//
// The fixRemoveNFTokenAutoTrustLine amendment disables minting with the
// tfTrustLine flag as a way to prevent the attack. But until the
// amendment passes we still need to keep the old behavior available.
std::uint32_t const NFTokenMintMask =
ctx.rules.enabled(fixRemoveNFTokenAutoTrustLine) ? tfNFTokenMintMask
: tfNFTokenMintOldMask;
if (ctx.tx.getFlags() & NFTokenMintMask)
return temINVALID_FLAG; return temINVALID_FLAG;
if (auto const f = ctx.tx[~sfTransferFee]) if (auto const f = ctx.tx[~sfTransferFee])

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how // Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this. // the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 50; static constexpr std::size_t numFeatures = 51;
/** Amendments that this server supports and the default voting behavior. /** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated Whether they are enabled depends on the Rules defined in the validated
@@ -338,6 +338,7 @@ extern uint256 const featureExpandedSignerList;
extern uint256 const fixNFTokenDirV1; extern uint256 const fixNFTokenDirV1;
extern uint256 const fixNFTokenNegOffer; extern uint256 const fixNFTokenNegOffer;
extern uint256 const featureNonFungibleTokensV1_1; extern uint256 const featureNonFungibleTokensV1_1;
extern uint256 const fixRemoveNFTokenAutoTrustLine;
} // namespace ripple } // namespace ripple

View File

@@ -120,9 +120,25 @@ constexpr std::uint32_t const tfOnlyXRP = 0x00000002;
constexpr std::uint32_t const tfTrustLine = 0x00000004; constexpr std::uint32_t const tfTrustLine = 0x00000004;
constexpr std::uint32_t const tfTransferable = 0x00000008; constexpr std::uint32_t const tfTransferable = 0x00000008;
constexpr std::uint32_t const tfNFTokenMintMask = // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between
// accounts allowed a TrustLine to be added to the issuer of that token
// without explicit permission from that issuer. This was enabled by
// minting the NFToken with the tfTrustLine flag set.
//
// That capability could be used to attack the NFToken issuer. It
// would be possible for two accounts to trade the NFToken back and forth
// building up any number of TrustLines on the issuer, increasing the
// issuer's reserve without bound.
//
// The fixRemoveNFTokenAutoTrustLine amendment disables minting with the
// tfTrustLine flag as a way to prevent the attack. But until the
// amendment passes we still need to keep the old behavior available.
constexpr std::uint32_t const tfNFTokenMintOldMask =
~(tfUniversal | tfBurnable | tfOnlyXRP | tfTrustLine | tfTransferable); ~(tfUniversal | tfBurnable | tfOnlyXRP | tfTrustLine | tfTransferable);
constexpr std::uint32_t const tfNFTokenMintMask =
~(tfUniversal | tfBurnable | tfOnlyXRP | tfTransferable);
// NFTokenCreateOffer flags: // NFTokenCreateOffer flags:
constexpr std::uint32_t const tfSellNFToken = 0x00000001; constexpr std::uint32_t const tfSellNFToken = 0x00000001;
constexpr std::uint32_t const tfNFTokenCreateOfferMask = constexpr std::uint32_t const tfNFTokenCreateOfferMask =

View File

@@ -447,6 +447,7 @@ REGISTER_FEATURE(ExpandedSignerList, Supported::yes, DefaultVote::no)
REGISTER_FIX (fixNFTokenDirV1, Supported::yes, DefaultVote::no); REGISTER_FIX (fixNFTokenDirV1, Supported::yes, DefaultVote::no);
REGISTER_FIX (fixNFTokenNegOffer, Supported::yes, DefaultVote::no); REGISTER_FIX (fixNFTokenNegOffer, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(NonFungibleTokensV1_1, Supported::yes, DefaultVote::no); REGISTER_FEATURE(NonFungibleTokensV1_1, Supported::yes, DefaultVote::no);
REGISTER_FIX (fixRemoveNFTokenAutoTrustLine, Supported::yes, DefaultVote::yes);
// The following amendments have been active for at least two years. Their // The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated. // pre-amendment code has been removed and the identifiers are deprecated.

View File

@@ -1574,7 +1574,6 @@ class NFToken_test : public beast::unit_test::suite
using namespace test::jtx; using namespace test::jtx;
Env env{*this, features};
Account const alice{"alice"}; Account const alice{"alice"};
Account const becky{"becky"}; Account const becky{"becky"};
Account const cheri{"cheri"}; Account const cheri{"cheri"};
@@ -1583,155 +1582,179 @@ class NFToken_test : public beast::unit_test::suite
IOU const gwCAD(gw["CAD"]); IOU const gwCAD(gw["CAD"]);
IOU const gwEUR(gw["EUR"]); IOU const gwEUR(gw["EUR"]);
env.fund(XRP(1000), alice, becky, cheri, gw); // The behavior of this test changes dramatically based on the
env.close(); // presence (or absence) of the fixRemoveNFTokenAutoTrustLine
// amendment. So we test both cases here.
// Set trust lines so becky and cheri can use gw's currency. for (auto const& tweakedFeatures :
env(trust(becky, gwAUD(1000))); {features - fixRemoveNFTokenAutoTrustLine,
env(trust(cheri, gwAUD(1000))); features | fixRemoveNFTokenAutoTrustLine})
env(trust(becky, gwCAD(1000)));
env(trust(cheri, gwCAD(1000)));
env(trust(becky, gwEUR(1000)));
env(trust(cheri, gwEUR(1000)));
env.close();
env(pay(gw, becky, gwAUD(500)));
env(pay(gw, becky, gwCAD(500)));
env(pay(gw, becky, gwEUR(500)));
env(pay(gw, cheri, gwAUD(500)));
env(pay(gw, cheri, gwCAD(500)));
env.close();
// An nft without flagCreateTrustLines but with a non-zero transfer
// fee will not allow creating offers that use IOUs for payment.
for (std::uint32_t xferFee : {0, 1})
{ {
uint256 const nftNoAutoTrustID{ Env env{*this, tweakedFeatures};
token::getNextID(env, alice, 0u, tfTransferable, xferFee)}; env.fund(XRP(1000), alice, becky, cheri, gw);
env(token::mint(alice, 0u),
token::xferFee(xferFee),
txflags(tfTransferable));
env.close(); env.close();
// becky buys the nft for 1 drop. // Set trust lines so becky and cheri can use gw's currency.
uint256 const beckyBuyOfferIndex = env(trust(becky, gwAUD(1000)));
keylet::nftoffer(becky, env.seq(becky)).key; env(trust(cheri, gwAUD(1000)));
env(token::createOffer(becky, nftNoAutoTrustID, drops(1)), env(trust(becky, gwCAD(1000)));
token::owner(alice)); env(trust(cheri, gwCAD(1000)));
env(trust(becky, gwEUR(1000)));
env(trust(cheri, gwEUR(1000)));
env.close(); env.close();
env(token::acceptBuyOffer(alice, beckyBuyOfferIndex)); env(pay(gw, becky, gwAUD(500)));
env(pay(gw, becky, gwCAD(500)));
env(pay(gw, becky, gwEUR(500)));
env(pay(gw, cheri, gwAUD(500)));
env(pay(gw, cheri, gwCAD(500)));
env.close(); env.close();
// becky attempts to sell the nft for AUD. // An nft without flagCreateTrustLines but with a non-zero transfer
TER const createOfferTER = // fee will not allow creating offers that use IOUs for payment.
xferFee ? TER(tecNO_LINE) : TER(tesSUCCESS); for (std::uint32_t xferFee : {0, 1})
uint256 const beckyOfferIndex = {
keylet::nftoffer(becky, env.seq(becky)).key; uint256 const nftNoAutoTrustID{
env(token::createOffer(becky, nftNoAutoTrustID, gwAUD(100)), token::getNextID(env, alice, 0u, tfTransferable, xferFee)};
txflags(tfSellNFToken), env(token::mint(alice, 0u),
ter(createOfferTER)); token::xferFee(xferFee),
env.close(); txflags(tfTransferable));
env.close();
// cheri offers to buy the nft for CAD. // becky buys the nft for 1 drop.
uint256 const cheriOfferIndex = uint256 const beckyBuyOfferIndex =
keylet::nftoffer(cheri, env.seq(cheri)).key; keylet::nftoffer(becky, env.seq(becky)).key;
env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)), env(token::createOffer(becky, nftNoAutoTrustID, drops(1)),
token::owner(becky), token::owner(alice));
ter(createOfferTER)); env.close();
env.close(); env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
env.close();
// To keep things tidy, cancel the offers. // becky attempts to sell the nft for AUD.
env(token::cancelOffer(becky, {beckyOfferIndex})); TER const createOfferTER =
env(token::cancelOffer(cheri, {cheriOfferIndex})); xferFee ? TER(tecNO_LINE) : TER(tesSUCCESS);
env.close(); uint256 const beckyOfferIndex =
} keylet::nftoffer(becky, env.seq(becky)).key;
// An nft with flagCreateTrustLines but with a non-zero transfer env(token::createOffer(becky, nftNoAutoTrustID, gwAUD(100)),
// fee allows transfers using IOUs for payment. txflags(tfSellNFToken),
{ ter(createOfferTER));
std::uint16_t transferFee = 10000; // 10% env.close();
uint256 const nftAutoTrustID{token::getNextID( // cheri offers to buy the nft for CAD.
env, alice, 0u, tfTransferable | tfTrustLine, transferFee)}; uint256 const cheriOfferIndex =
env(token::mint(alice, 0u), keylet::nftoffer(cheri, env.seq(cheri)).key;
token::xferFee(transferFee), env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)),
txflags(tfTransferable | tfTrustLine)); token::owner(becky),
env.close(); ter(createOfferTER));
env.close();
// becky buys the nft for 1 drop. // To keep things tidy, cancel the offers.
uint256 const beckyBuyOfferIndex = env(token::cancelOffer(becky, {beckyOfferIndex}));
keylet::nftoffer(becky, env.seq(becky)).key; env(token::cancelOffer(cheri, {cheriOfferIndex}));
env(token::createOffer(becky, nftAutoTrustID, drops(1)), env.close();
token::owner(alice)); }
env.close(); // An nft with flagCreateTrustLines but with a non-zero transfer
env(token::acceptBuyOffer(alice, beckyBuyOfferIndex)); // fee allows transfers using IOUs for payment.
env.close(); {
std::uint16_t transferFee = 10000; // 10%
// becky sells the nft for AUD. uint256 const nftAutoTrustID{token::getNextID(
uint256 const beckySellOfferIndex = env, alice, 0u, tfTransferable | tfTrustLine, transferFee)};
keylet::nftoffer(becky, env.seq(becky)).key;
env(token::createOffer(becky, nftAutoTrustID, gwAUD(100)),
txflags(tfSellNFToken));
env.close();
env(token::acceptSellOffer(cheri, beckySellOfferIndex));
env.close();
// alice should now have a trust line for gwAUD. // If the fixRemoveNFTokenAutoTrustLine amendment is active
BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(10)); // then this transaction fails.
{
TER const mintTER =
tweakedFeatures[fixRemoveNFTokenAutoTrustLine]
? static_cast<TER>(temINVALID_FLAG)
: static_cast<TER>(tesSUCCESS);
// becky buys the nft back for CAD. env(token::mint(alice, 0u),
uint256 const beckyBuyBackOfferIndex = token::xferFee(transferFee),
keylet::nftoffer(becky, env.seq(becky)).key; txflags(tfTransferable | tfTrustLine),
env(token::createOffer(becky, nftAutoTrustID, gwCAD(50)), ter(mintTER));
token::owner(cheri)); env.close();
env.close();
env(token::acceptBuyOffer(cheri, beckyBuyBackOfferIndex));
env.close();
// alice should now have a trust line for gwAUD and gwCAD. // If fixRemoveNFTokenAutoTrustLine is active the rest
BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(10)); // of this test falls on its face.
BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(5)); if (tweakedFeatures[fixRemoveNFTokenAutoTrustLine])
} break;
// Now that alice has trust lines already established, an nft without }
// flagCreateTrustLines will work for preestablished trust lines. // becky buys the nft for 1 drop.
{ uint256 const beckyBuyOfferIndex =
std::uint16_t transferFee = 5000; // 5% keylet::nftoffer(becky, env.seq(becky)).key;
uint256 const nftNoAutoTrustID{ env(token::createOffer(becky, nftAutoTrustID, drops(1)),
token::getNextID(env, alice, 0u, tfTransferable, transferFee)}; token::owner(alice));
env(token::mint(alice, 0u), env.close();
token::xferFee(transferFee), env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
txflags(tfTransferable)); env.close();
env.close();
// alice sells the nft using AUD. // becky sells the nft for AUD.
uint256 const aliceSellOfferIndex = uint256 const beckySellOfferIndex =
keylet::nftoffer(alice, env.seq(alice)).key; keylet::nftoffer(becky, env.seq(becky)).key;
env(token::createOffer(alice, nftNoAutoTrustID, gwAUD(200)), env(token::createOffer(becky, nftAutoTrustID, gwAUD(100)),
txflags(tfSellNFToken)); txflags(tfSellNFToken));
env.close(); env.close();
env(token::acceptSellOffer(cheri, aliceSellOfferIndex)); env(token::acceptSellOffer(cheri, beckySellOfferIndex));
env.close(); env.close();
// alice should now have AUD(210): // alice should now have a trust line for gwAUD.
// o 200 for this sale and BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(10));
// o 10 for the previous sale's fee.
BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(210));
// cheri can't sell the NFT for EUR, but can for CAD. // becky buys the nft back for CAD.
env(token::createOffer(cheri, nftNoAutoTrustID, gwEUR(50)), uint256 const beckyBuyBackOfferIndex =
txflags(tfSellNFToken), keylet::nftoffer(becky, env.seq(becky)).key;
ter(tecNO_LINE)); env(token::createOffer(becky, nftAutoTrustID, gwCAD(50)),
env.close(); token::owner(cheri));
uint256 const cheriSellOfferIndex = env.close();
keylet::nftoffer(cheri, env.seq(cheri)).key; env(token::acceptBuyOffer(cheri, beckyBuyBackOfferIndex));
env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)), env.close();
txflags(tfSellNFToken));
env.close();
env(token::acceptSellOffer(becky, cheriSellOfferIndex));
env.close();
// alice should now have CAD(10): // alice should now have a trust line for gwAUD and gwCAD.
// o 5 from this sale's fee and BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(10));
// o 5 for the previous sale's fee. BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(5));
BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(10)); }
// Now that alice has trust lines preestablished, an nft without
// flagCreateTrustLines will work for preestablished trust lines.
{
std::uint16_t transferFee = 5000; // 5%
uint256 const nftNoAutoTrustID{token::getNextID(
env, alice, 0u, tfTransferable, transferFee)};
env(token::mint(alice, 0u),
token::xferFee(transferFee),
txflags(tfTransferable));
env.close();
// alice sells the nft using AUD.
uint256 const aliceSellOfferIndex =
keylet::nftoffer(alice, env.seq(alice)).key;
env(token::createOffer(alice, nftNoAutoTrustID, gwAUD(200)),
txflags(tfSellNFToken));
env.close();
env(token::acceptSellOffer(cheri, aliceSellOfferIndex));
env.close();
// alice should now have AUD(210):
// o 200 for this sale and
// o 10 for the previous sale's fee.
BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(210));
// cheri can't sell the NFT for EUR, but can for CAD.
env(token::createOffer(cheri, nftNoAutoTrustID, gwEUR(50)),
txflags(tfSellNFToken),
ter(tecNO_LINE));
env.close();
uint256 const cheriSellOfferIndex =
keylet::nftoffer(cheri, env.seq(cheri)).key;
env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)),
txflags(tfSellNFToken));
env.close();
env(token::acceptSellOffer(becky, cheriSellOfferIndex));
env.close();
// alice should now have CAD(10):
// o 5 from this sale's fee and
// o 5 for the previous sale's fee.
BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(10));
}
} }
} }