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,6 +1582,14 @@ 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"]);
// The behavior of this test changes dramatically based on the
// presence (or absence) of the fixRemoveNFTokenAutoTrustLine
// amendment. So we test both cases here.
for (auto const& tweakedFeatures :
{features - fixRemoveNFTokenAutoTrustLine,
features | fixRemoveNFTokenAutoTrustLine})
{
Env env{*this, tweakedFeatures};
env.fund(XRP(1000), alice, becky, cheri, gw); env.fund(XRP(1000), alice, becky, cheri, gw);
env.close(); env.close();
@@ -1651,11 +1658,26 @@ class NFToken_test : public beast::unit_test::suite
uint256 const nftAutoTrustID{token::getNextID( uint256 const nftAutoTrustID{token::getNextID(
env, alice, 0u, tfTransferable | tfTrustLine, transferFee)}; env, alice, 0u, tfTransferable | tfTrustLine, transferFee)};
// If the fixRemoveNFTokenAutoTrustLine amendment is active
// then this transaction fails.
{
TER const mintTER =
tweakedFeatures[fixRemoveNFTokenAutoTrustLine]
? static_cast<TER>(temINVALID_FLAG)
: static_cast<TER>(tesSUCCESS);
env(token::mint(alice, 0u), env(token::mint(alice, 0u),
token::xferFee(transferFee), token::xferFee(transferFee),
txflags(tfTransferable | tfTrustLine)); txflags(tfTransferable | tfTrustLine),
ter(mintTER));
env.close(); env.close();
// If fixRemoveNFTokenAutoTrustLine is active the rest
// of this test falls on its face.
if (tweakedFeatures[fixRemoveNFTokenAutoTrustLine])
break;
}
// becky buys the nft for 1 drop. // becky buys the nft for 1 drop.
uint256 const beckyBuyOfferIndex = uint256 const beckyBuyOfferIndex =
keylet::nftoffer(becky, env.seq(becky)).key; keylet::nftoffer(becky, env.seq(becky)).key;
@@ -1690,12 +1712,12 @@ class NFToken_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(10)); BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(10));
BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(5)); BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(5));
} }
// Now that alice has trust lines already established, an nft without // Now that alice has trust lines preestablished, an nft without
// flagCreateTrustLines will work for preestablished trust lines. // flagCreateTrustLines will work for preestablished trust lines.
{ {
std::uint16_t transferFee = 5000; // 5% std::uint16_t transferFee = 5000; // 5%
uint256 const nftNoAutoTrustID{ uint256 const nftNoAutoTrustID{token::getNextID(
token::getNextID(env, alice, 0u, tfTransferable, transferFee)}; env, alice, 0u, tfTransferable, transferFee)};
env(token::mint(alice, 0u), env(token::mint(alice, 0u),
token::xferFee(transferFee), token::xferFee(transferFee),
txflags(tfTransferable)); txflags(tfTransferable));
@@ -1734,6 +1756,7 @@ class NFToken_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(10)); BEAST_EXPECT(env.balance(alice, gwCAD) == gwCAD(10));
} }
} }
}
void void
testMintFlagTransferable(FeatureBitset features) testMintFlagTransferable(FeatureBitset features)