mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 18:45:52 +00:00
fix: Address NFT interactions with trustlines (#5297)
The changes are focused on fixing NFT transactions bypassing the trustline authorization requirement and potential invariant violation when interacting with deep frozen trustlines.
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -1885,6 +1885,31 @@ class Freeze_test : public beast::unit_test::suite
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Testing A1 nft buy offer when A2 deep frozen by issuer
|
||||
if (features[featureDeepFreeze] &&
|
||||
features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
env(trust(G1, A2["USD"](0), tfSetFreeze | tfSetDeepFreeze));
|
||||
env.close();
|
||||
|
||||
uint256 const nftID{token::getNextID(env, A2, 0u, tfTransferable)};
|
||||
env(token::mint(A2, 0), txflags(tfTransferable));
|
||||
env.close();
|
||||
|
||||
auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
|
||||
env(token::createOffer(A1, nftID, USD(10)), token::owner(A2));
|
||||
env.close();
|
||||
|
||||
env(token::acceptBuyOffer(A2, buyIdx), ter(tecFROZEN));
|
||||
env.close();
|
||||
|
||||
env(trust(G1, A2["USD"](0), tfClearFreeze | tfClearDeepFreeze));
|
||||
env.close();
|
||||
|
||||
env(token::acceptBuyOffer(A2, buyIdx));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Testing A2 nft offer sell when A2 frozen by currency holder
|
||||
{
|
||||
auto const sellOfferIndex = createNFTSellOffer(env, A2, USD(10));
|
||||
@@ -1944,6 +1969,68 @@ class Freeze_test : public beast::unit_test::suite
|
||||
env(trust(A2, limit, tfClearFreeze | tfClearDeepFreeze));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Testing brokered offer acceptance
|
||||
if (features[featureDeepFreeze] &&
|
||||
features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
Account broker{"broker"};
|
||||
env.fund(XRP(10000), broker);
|
||||
env.close();
|
||||
env(trust(G1, broker["USD"](1000), tfSetFreeze | tfSetDeepFreeze));
|
||||
env.close();
|
||||
|
||||
uint256 const nftID{token::getNextID(env, A2, 0u, tfTransferable)};
|
||||
env(token::mint(A2, 0), txflags(tfTransferable));
|
||||
env.close();
|
||||
|
||||
uint256 const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key;
|
||||
env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken));
|
||||
env.close();
|
||||
auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
|
||||
env(token::createOffer(A1, nftID, USD(11)), token::owner(A2));
|
||||
env.close();
|
||||
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx),
|
||||
token::brokerFee(USD(1)),
|
||||
ter(tecFROZEN));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Testing transfer fee
|
||||
if (features[featureDeepFreeze] &&
|
||||
features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
Account minter{"minter"};
|
||||
env.fund(XRP(10000), minter);
|
||||
env.close();
|
||||
env(trust(G1, minter["USD"](1000)));
|
||||
env.close();
|
||||
|
||||
uint256 const nftID{
|
||||
token::getNextID(env, minter, 0u, tfTransferable, 1u)};
|
||||
env(token::mint(minter, 0),
|
||||
token::xferFee(1u),
|
||||
txflags(tfTransferable));
|
||||
env.close();
|
||||
|
||||
uint256 const minterSellIdx =
|
||||
keylet::nftoffer(minter, env.seq(minter)).key;
|
||||
env(token::createOffer(minter, nftID, drops(1)),
|
||||
txflags(tfSellNFToken));
|
||||
env.close();
|
||||
env(token::acceptSellOffer(A2, minterSellIdx));
|
||||
env.close();
|
||||
|
||||
uint256 const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key;
|
||||
env(token::createOffer(A2, nftID, USD(100)),
|
||||
txflags(tfSellNFToken));
|
||||
env.close();
|
||||
env(trust(G1, minter["USD"](1000), tfSetFreeze | tfSetDeepFreeze));
|
||||
env.close();
|
||||
env(token::acceptSellOffer(A1, sellIdx), ter(tecFROZEN));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract trustline flags from open ledger
|
||||
@@ -2021,10 +2108,16 @@ public:
|
||||
using namespace test::jtx;
|
||||
auto const sa = supported_amendments();
|
||||
testAll(
|
||||
sa - featureFlowCross - featureDeepFreeze - featurePermissionedDEX);
|
||||
testAll(sa - featureFlowCross - featurePermissionedDEX);
|
||||
testAll(sa - featureDeepFreeze - featurePermissionedDEX);
|
||||
testAll(sa - featurePermissionedDEX);
|
||||
sa - featureFlowCross - featureDeepFreeze - featurePermissionedDEX -
|
||||
fixEnforceNFTokenTrustlineV2);
|
||||
testAll(
|
||||
sa - featureFlowCross - featurePermissionedDEX -
|
||||
fixEnforceNFTokenTrustlineV2);
|
||||
testAll(
|
||||
sa - featureDeepFreeze - featurePermissionedDEX -
|
||||
fixEnforceNFTokenTrustlineV2);
|
||||
testAll(sa - featurePermissionedDEX - fixEnforceNFTokenTrustlineV2);
|
||||
testAll(sa - fixEnforceNFTokenTrustlineV2);
|
||||
testAll(sa);
|
||||
}
|
||||
};
|
||||
|
||||
624
src/test/app/NFTokenAuth_test.cpp
Normal file
624
src/test/app/NFTokenAuth_test.cpp
Normal file
@@ -0,0 +1,624 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
|
||||
#include <xrpld/app/tx/detail/NFTokenUtils.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class NFTokenAuth_test : public beast::unit_test::suite
|
||||
{
|
||||
auto
|
||||
mintAndOfferNFT(
|
||||
test::jtx::Env& env,
|
||||
test::jtx::Account const& account,
|
||||
test::jtx::PrettyAmount const& currency,
|
||||
uint32_t xfee = 0u)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const nftID{
|
||||
token::getNextID(env, account, 0u, tfTransferable, xfee)};
|
||||
env(token::mint(account, 0),
|
||||
token::xferFee(xfee),
|
||||
txflags(tfTransferable));
|
||||
env.close();
|
||||
|
||||
auto const sellIdx = keylet::nftoffer(account, env.seq(account)).key;
|
||||
env(token::createOffer(account, nftID, currency),
|
||||
txflags(tfSellNFToken));
|
||||
env.close();
|
||||
|
||||
return std::make_tuple(nftID, sellIdx);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testBuyOffer_UnauthorizedSeller(FeatureBitset features)
|
||||
{
|
||||
testcase("Unauthorized seller tries to accept buy offer");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account G1{"G1"};
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
auto const USD{G1["USD"]};
|
||||
|
||||
env.fund(XRP(10000), G1, A1, A2);
|
||||
env(fset(G1, asfRequireAuth));
|
||||
env.close();
|
||||
|
||||
auto const limit = USD(10000);
|
||||
|
||||
env(trust(A1, limit));
|
||||
env(trust(G1, limit, A1, tfSetfAuth));
|
||||
env(pay(G1, A1, USD(1000)));
|
||||
|
||||
auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1));
|
||||
auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
|
||||
|
||||
// It should be possible to create a buy offer even if NFT owner is not
|
||||
// authorized
|
||||
env(token::createOffer(A1, nftID, USD(10)), token::owner(A2));
|
||||
|
||||
if (features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
// test: G1 requires authorization of A2, no trust line exists
|
||||
env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_LINE));
|
||||
env.close();
|
||||
|
||||
// trust line created, but not authorized
|
||||
env(trust(A2, limit));
|
||||
|
||||
// test: G1 requires authorization of A2
|
||||
env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_AUTH));
|
||||
env.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Old behavior: it is possible to sell tokens and receive IOUs
|
||||
// without the authorization
|
||||
env(token::acceptBuyOffer(A2, buyIdx));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(A2, USD) == USD(10));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCreateBuyOffer_UnauthorizedBuyer(FeatureBitset features)
|
||||
{
|
||||
testcase("Unauthorized buyer tries to create buy offer");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account G1{"G1"};
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
auto const USD{G1["USD"]};
|
||||
|
||||
env.fund(XRP(10000), G1, A1, A2);
|
||||
env(fset(G1, asfRequireAuth));
|
||||
env.close();
|
||||
|
||||
auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1));
|
||||
|
||||
// test: check that buyer can't make an offer if they're not authorized.
|
||||
env(token::createOffer(A1, nftID, USD(10)),
|
||||
token::owner(A2),
|
||||
ter(tecUNFUNDED_OFFER));
|
||||
env.close();
|
||||
|
||||
// Artificially create an unauthorized trustline with balance. Don't
|
||||
// close ledger before running the actual tests against this trustline.
|
||||
// After ledger is closed, the trustline will not exist.
|
||||
auto const unauthTrustline = [&](OpenView& view,
|
||||
beast::Journal) -> bool {
|
||||
auto const sleA1 =
|
||||
std::make_shared<SLE>(keylet::line(A1, G1, G1["USD"].currency));
|
||||
sleA1->setFieldAmount(sfBalance, A1["USD"](-1000));
|
||||
view.rawInsert(sleA1);
|
||||
return true;
|
||||
};
|
||||
env.app().openLedger().modify(unauthTrustline);
|
||||
|
||||
if (features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
// test: check that buyer can't make an offer even with balance
|
||||
env(token::createOffer(A1, nftID, USD(10)),
|
||||
token::owner(A2),
|
||||
ter(tecNO_AUTH));
|
||||
}
|
||||
else
|
||||
{
|
||||
// old behavior: can create an offer if balance allows, regardless
|
||||
// ot authorization
|
||||
env(token::createOffer(A1, nftID, USD(10)), token::owner(A2));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testAcceptBuyOffer_UnauthorizedBuyer(FeatureBitset features)
|
||||
{
|
||||
testcase("Seller tries to accept buy offer from unauth buyer");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account G1{"G1"};
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
auto const USD{G1["USD"]};
|
||||
|
||||
env.fund(XRP(10000), G1, A1, A2);
|
||||
env(fset(G1, asfRequireAuth));
|
||||
env.close();
|
||||
|
||||
auto const limit = USD(10000);
|
||||
|
||||
auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1));
|
||||
|
||||
// First we authorize buyer and seller so that he can create buy offer
|
||||
env(trust(A1, limit));
|
||||
env(trust(G1, limit, A1, tfSetfAuth));
|
||||
env(pay(G1, A1, USD(10)));
|
||||
env(trust(A2, limit));
|
||||
env(trust(G1, limit, A2, tfSetfAuth));
|
||||
env(pay(G1, A2, USD(10)));
|
||||
env.close();
|
||||
|
||||
auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
|
||||
env(token::createOffer(A1, nftID, USD(10)), token::owner(A2));
|
||||
env.close();
|
||||
|
||||
env(pay(A1, G1, USD(10)));
|
||||
env(trust(A1, USD(0)));
|
||||
env(trust(G1, A1["USD"](0)));
|
||||
env.close();
|
||||
|
||||
// Replace an existing authorized trustline with artificial unauthorized
|
||||
// trustline with balance. Don't close ledger before running the actual
|
||||
// tests against this trustline. After ledger is closed, the trustline
|
||||
// will not exist.
|
||||
auto const unauthTrustline = [&](OpenView& view,
|
||||
beast::Journal) -> bool {
|
||||
auto const sleA1 =
|
||||
std::make_shared<SLE>(keylet::line(A1, G1, G1["USD"].currency));
|
||||
sleA1->setFieldAmount(sfBalance, A1["USD"](-1000));
|
||||
view.rawInsert(sleA1);
|
||||
return true;
|
||||
};
|
||||
env.app().openLedger().modify(unauthTrustline);
|
||||
if (features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
// test: check that offer can't be accepted even with balance
|
||||
env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_AUTH));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSellOffer_UnauthorizedSeller(FeatureBitset features)
|
||||
{
|
||||
testcase(
|
||||
"Authorized buyer tries to accept sell offer from unauthorized "
|
||||
"seller");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account G1{"G1"};
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
auto const USD{G1["USD"]};
|
||||
|
||||
env.fund(XRP(10000), G1, A1, A2);
|
||||
env(fset(G1, asfRequireAuth));
|
||||
env.close();
|
||||
|
||||
auto const limit = USD(10000);
|
||||
|
||||
env(trust(A1, limit));
|
||||
env(trust(G1, limit, A1, tfSetfAuth));
|
||||
env(pay(G1, A1, USD(1000)));
|
||||
|
||||
auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1));
|
||||
if (features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
// test: can't create sell offer if there is no trustline but auth
|
||||
// required
|
||||
env(token::createOffer(A2, nftID, USD(10)),
|
||||
txflags(tfSellNFToken),
|
||||
ter(tecNO_LINE));
|
||||
|
||||
env(trust(A2, limit));
|
||||
// test: can't create sell offer if not authorized to hold token
|
||||
env(token::createOffer(A2, nftID, USD(10)),
|
||||
txflags(tfSellNFToken),
|
||||
ter(tecNO_AUTH));
|
||||
|
||||
// Authorizing trustline to make an offer creation possible
|
||||
env(trust(G1, USD(0), A2, tfSetfAuth));
|
||||
env.close();
|
||||
auto const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key;
|
||||
env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken));
|
||||
env.close();
|
||||
//
|
||||
|
||||
// Reseting trustline to delete it. This allows to check if
|
||||
// already existing offers handled correctly
|
||||
env(trust(A2, USD(0)));
|
||||
env.close();
|
||||
|
||||
// test: G1 requires authorization of A1, no trust line exists
|
||||
env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_LINE));
|
||||
env.close();
|
||||
|
||||
// trust line created, but not authorized
|
||||
env(trust(A2, limit));
|
||||
env.close();
|
||||
|
||||
// test: G1 requires authorization of A1
|
||||
env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_AUTH));
|
||||
env.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key;
|
||||
|
||||
// Old behavior: sell offer can be created without authorization
|
||||
env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken));
|
||||
env.close();
|
||||
|
||||
// Old behavior: it is possible to sell NFT and receive IOUs
|
||||
// without the authorization
|
||||
env(token::acceptSellOffer(A1, sellIdx));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(A2, USD) == USD(10));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSellOffer_UnauthorizedBuyer(FeatureBitset features)
|
||||
{
|
||||
testcase("Unauthorized buyer tries to accept sell offer");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account G1{"G1"};
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
auto const USD{G1["USD"]};
|
||||
|
||||
env.fund(XRP(10000), G1, A1, A2);
|
||||
env(fset(G1, asfRequireAuth));
|
||||
env.close();
|
||||
|
||||
auto const limit = USD(10000);
|
||||
|
||||
env(trust(A2, limit));
|
||||
env(trust(G1, limit, A2, tfSetfAuth));
|
||||
|
||||
auto const [_, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
|
||||
|
||||
// test: check that buyer can't accept an offer if they're not
|
||||
// authorized.
|
||||
env(token::acceptSellOffer(A1, sellIdx), ter(tecINSUFFICIENT_FUNDS));
|
||||
env.close();
|
||||
|
||||
// Creating an artificial unauth trustline
|
||||
auto const unauthTrustline = [&](OpenView& view,
|
||||
beast::Journal) -> bool {
|
||||
auto const sleA1 =
|
||||
std::make_shared<SLE>(keylet::line(A1, G1, G1["USD"].currency));
|
||||
sleA1->setFieldAmount(sfBalance, A1["USD"](-1000));
|
||||
view.rawInsert(sleA1);
|
||||
return true;
|
||||
};
|
||||
env.app().openLedger().modify(unauthTrustline);
|
||||
if (features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_AUTH));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testBrokeredAcceptOffer_UnauthorizedBroker(FeatureBitset features)
|
||||
{
|
||||
testcase("Unauthorized broker bridges authorized buyer and seller.");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account G1{"G1"};
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
Account broker{"broker"};
|
||||
auto const USD{G1["USD"]};
|
||||
|
||||
env.fund(XRP(10000), G1, A1, A2, broker);
|
||||
env(fset(G1, asfRequireAuth));
|
||||
env.close();
|
||||
|
||||
auto const limit = USD(10000);
|
||||
|
||||
env(trust(A1, limit));
|
||||
env(trust(G1, limit, A1, tfSetfAuth));
|
||||
env(pay(G1, A1, USD(1000)));
|
||||
env(trust(A2, limit));
|
||||
env(trust(G1, limit, A2, tfSetfAuth));
|
||||
env(pay(G1, A2, USD(1000)));
|
||||
env.close();
|
||||
|
||||
auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
|
||||
auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
|
||||
env(token::createOffer(A1, nftID, USD(11)), token::owner(A2));
|
||||
env.close();
|
||||
|
||||
if (features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
// test: G1 requires authorization of broker, no trust line exists
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx),
|
||||
token::brokerFee(USD(1)),
|
||||
ter(tecNO_LINE));
|
||||
env.close();
|
||||
|
||||
// trust line created, but not authorized
|
||||
env(trust(broker, limit));
|
||||
env.close();
|
||||
|
||||
// test: G1 requires authorization of broker
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx),
|
||||
token::brokerFee(USD(1)),
|
||||
ter(tecNO_AUTH));
|
||||
env.close();
|
||||
|
||||
// test: can still be brokered without broker fee.
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx));
|
||||
env.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Old behavior: broker can receive IOUs without the authorization
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx),
|
||||
token::brokerFee(USD(1)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(broker, USD) == USD(1));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testBrokeredAcceptOffer_UnauthorizedBuyer(FeatureBitset features)
|
||||
{
|
||||
testcase(
|
||||
"Authorized broker tries to bridge offers from unauthorized "
|
||||
"buyer.");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account G1{"G1"};
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
Account broker{"broker"};
|
||||
auto const USD{G1["USD"]};
|
||||
|
||||
env.fund(XRP(10000), G1, A1, A2, broker);
|
||||
env(fset(G1, asfRequireAuth));
|
||||
env.close();
|
||||
|
||||
auto const limit = USD(10000);
|
||||
|
||||
env(trust(A1, limit));
|
||||
env(trust(G1, USD(0), A1, tfSetfAuth));
|
||||
env(pay(G1, A1, USD(1000)));
|
||||
env(trust(A2, limit));
|
||||
env(trust(G1, USD(0), A2, tfSetfAuth));
|
||||
env(pay(G1, A2, USD(1000)));
|
||||
env(trust(broker, limit));
|
||||
env(trust(G1, USD(0), broker, tfSetfAuth));
|
||||
env(pay(G1, broker, USD(1000)));
|
||||
env.close();
|
||||
|
||||
auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
|
||||
auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
|
||||
env(token::createOffer(A1, nftID, USD(11)), token::owner(A2));
|
||||
env.close();
|
||||
|
||||
// Resetting buyer's trust line to delete it
|
||||
env(pay(A1, G1, USD(1000)));
|
||||
env(trust(A1, USD(0)));
|
||||
env.close();
|
||||
|
||||
auto const unauthTrustline = [&](OpenView& view,
|
||||
beast::Journal) -> bool {
|
||||
auto const sleA1 =
|
||||
std::make_shared<SLE>(keylet::line(A1, G1, G1["USD"].currency));
|
||||
sleA1->setFieldAmount(sfBalance, A1["USD"](-1000));
|
||||
view.rawInsert(sleA1);
|
||||
return true;
|
||||
};
|
||||
env.app().openLedger().modify(unauthTrustline);
|
||||
|
||||
if (features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
// test: G1 requires authorization of A2
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx),
|
||||
token::brokerFee(USD(1)),
|
||||
ter(tecNO_AUTH));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testBrokeredAcceptOffer_UnauthorizedSeller(FeatureBitset features)
|
||||
{
|
||||
testcase(
|
||||
"Authorized broker tries to bridge offers from unauthorized "
|
||||
"seller.");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account G1{"G1"};
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
Account broker{"broker"};
|
||||
auto const USD{G1["USD"]};
|
||||
|
||||
env.fund(XRP(10000), G1, A1, A2, broker);
|
||||
env(fset(G1, asfRequireAuth));
|
||||
env.close();
|
||||
|
||||
auto const limit = USD(10000);
|
||||
|
||||
env(trust(A1, limit));
|
||||
env(trust(G1, limit, A1, tfSetfAuth));
|
||||
env(pay(G1, A1, USD(1000)));
|
||||
env(trust(broker, limit));
|
||||
env(trust(G1, limit, broker, tfSetfAuth));
|
||||
env(pay(G1, broker, USD(1000)));
|
||||
env.close();
|
||||
|
||||
// Authorizing trustline to make an offer creation possible
|
||||
env(trust(G1, USD(0), A2, tfSetfAuth));
|
||||
env.close();
|
||||
|
||||
auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10));
|
||||
auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key;
|
||||
env(token::createOffer(A1, nftID, USD(11)), token::owner(A2));
|
||||
env.close();
|
||||
|
||||
// Reseting trustline to delete it. This allows to check if
|
||||
// already existing offers handled correctly
|
||||
env(trust(A2, USD(0)));
|
||||
env.close();
|
||||
|
||||
if (features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
// test: G1 requires authorization of broker, no trust line exists
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx),
|
||||
token::brokerFee(USD(1)),
|
||||
ter(tecNO_LINE));
|
||||
env.close();
|
||||
|
||||
// trust line created, but not authorized
|
||||
env(trust(A2, limit));
|
||||
env.close();
|
||||
|
||||
// test: G1 requires authorization of A2
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx),
|
||||
token::brokerFee(USD(1)),
|
||||
ter(tecNO_AUTH));
|
||||
env.close();
|
||||
|
||||
// test: cannot be brokered even without broker fee.
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx), ter(tecNO_AUTH));
|
||||
env.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Old behavior: broker can receive IOUs without the authorization
|
||||
env(token::brokerOffers(broker, buyIdx, sellIdx),
|
||||
token::brokerFee(USD(1)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(A2, USD) == USD(10));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testTransferFee_UnauthorizedMinter(FeatureBitset features)
|
||||
{
|
||||
testcase("Unauthorized minter receives transfer fee.");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account G1{"G1"};
|
||||
Account minter{"minter"};
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
auto const USD{G1["USD"]};
|
||||
|
||||
env.fund(XRP(10000), G1, minter, A1, A2);
|
||||
env(fset(G1, asfRequireAuth));
|
||||
env.close();
|
||||
|
||||
auto const limit = USD(10000);
|
||||
|
||||
env(trust(A1, limit));
|
||||
env(trust(G1, limit, A1, tfSetfAuth));
|
||||
env(pay(G1, A1, USD(1000)));
|
||||
env(trust(A2, limit));
|
||||
env(trust(G1, limit, A2, tfSetfAuth));
|
||||
env(pay(G1, A2, USD(1000)));
|
||||
|
||||
env(trust(minter, limit));
|
||||
env.close();
|
||||
|
||||
// We authorized A1 and A2, but not the minter.
|
||||
// Now mint NFT
|
||||
auto const [nftID, minterSellIdx] =
|
||||
mintAndOfferNFT(env, minter, drops(1), 1);
|
||||
env(token::acceptSellOffer(A1, minterSellIdx));
|
||||
|
||||
uint256 const sellIdx = keylet::nftoffer(A1, env.seq(A1)).key;
|
||||
env(token::createOffer(A1, nftID, USD(100)), txflags(tfSellNFToken));
|
||||
|
||||
if (features[fixEnforceNFTokenTrustlineV2])
|
||||
{
|
||||
// test: G1 requires authorization
|
||||
env(token::acceptSellOffer(A2, sellIdx), ter(tecNO_AUTH));
|
||||
env.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Old behavior: can sell for USD. Minter can receive tokens
|
||||
env(token::acceptSellOffer(A2, sellIdx));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(minter, USD) == USD(0.001));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
static FeatureBitset const all{supported_amendments()};
|
||||
|
||||
static std::array const features = {
|
||||
all - fixEnforceNFTokenTrustlineV2, all};
|
||||
|
||||
for (auto const feature : features)
|
||||
{
|
||||
testBuyOffer_UnauthorizedSeller(feature);
|
||||
testCreateBuyOffer_UnauthorizedBuyer(feature);
|
||||
testAcceptBuyOffer_UnauthorizedBuyer(feature);
|
||||
testSellOffer_UnauthorizedSeller(feature);
|
||||
testSellOffer_UnauthorizedBuyer(feature);
|
||||
testBrokeredAcceptOffer_UnauthorizedBroker(feature);
|
||||
testBrokeredAcceptOffer_UnauthorizedBuyer(feature);
|
||||
testBrokeredAcceptOffer_UnauthorizedSeller(feature);
|
||||
testTransferFee_UnauthorizedMinter(feature);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenAuth, tx, ripple, 2);
|
||||
|
||||
} // namespace ripple
|
||||
@@ -160,6 +160,27 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if ((*so)[sfAmount] > (*bo)[sfAmount] - *brokerFee)
|
||||
return tecINSUFFICIENT_PAYMENT;
|
||||
|
||||
// Check if broker is allowed to receive the fee with these IOUs.
|
||||
if (!brokerFee->native() &&
|
||||
ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2))
|
||||
{
|
||||
auto res = nft::checkTrustlineAuthorized(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
ctx.j,
|
||||
brokerFee->asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
|
||||
res = nft::checkTrustlineDeepFrozen(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
ctx.j,
|
||||
brokerFee->asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +229,38 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
|
||||
fhZERO_IF_FROZEN,
|
||||
ctx.j) < needed)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// Check that the account accepting the buy offer (he's selling the NFT)
|
||||
// is allowed to receive IOUs. Also check that this offer's creator is
|
||||
// authorized. But we need to exclude the case when the transaction is
|
||||
// created by the broker.
|
||||
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2) &&
|
||||
!needed.native())
|
||||
{
|
||||
auto res = nft::checkTrustlineAuthorized(
|
||||
ctx.view, bo->at(sfOwner), ctx.j, needed.asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
|
||||
if (!so)
|
||||
{
|
||||
res = nft::checkTrustlineAuthorized(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
ctx.j,
|
||||
needed.asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
|
||||
res = nft::checkTrustlineDeepFrozen(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
ctx.j,
|
||||
needed.asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (so)
|
||||
@@ -270,42 +323,74 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
|
||||
// Make sure that we are allowed to hold what the taker will pay us.
|
||||
// This is a similar approach taken by usual offers.
|
||||
if (!needed.native())
|
||||
{
|
||||
auto const result = checkAcceptAsset(
|
||||
ctx.view,
|
||||
ctx.flags,
|
||||
(*so)[sfOwner],
|
||||
ctx.j,
|
||||
needed.asset().get<Issue>());
|
||||
if (result != tesSUCCESS)
|
||||
return result;
|
||||
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2))
|
||||
{
|
||||
auto res = nft::checkTrustlineAuthorized(
|
||||
ctx.view,
|
||||
(*so)[sfOwner],
|
||||
ctx.j,
|
||||
needed.asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
|
||||
if (!bo)
|
||||
{
|
||||
res = nft::checkTrustlineAuthorized(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
ctx.j,
|
||||
needed.asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
auto const res = nft::checkTrustlineDeepFrozen(
|
||||
ctx.view, (*so)[sfOwner], ctx.j, needed.asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix a bug where the transfer of an NFToken with a transfer fee could
|
||||
// give the NFToken issuer an undesired trust line.
|
||||
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline))
|
||||
// Additional checks are required in case a minter set a transfer fee for
|
||||
// this nftoken
|
||||
auto const& offer = bo ? bo : so;
|
||||
if (!offer)
|
||||
// Purely defensive, should be caught in preflight.
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const& tokenID = offer->at(sfNFTokenID);
|
||||
auto const& amount = offer->at(sfAmount);
|
||||
auto const nftMinter = nft::getIssuer(tokenID);
|
||||
|
||||
if (nft::getTransferFee(tokenID) != 0 && !amount.native())
|
||||
{
|
||||
std::shared_ptr<SLE const> const& offer = bo ? bo : so;
|
||||
if (!offer)
|
||||
// Should be caught in preflight.
|
||||
return tecINTERNAL;
|
||||
|
||||
uint256 const& tokenID = offer->at(sfNFTokenID);
|
||||
STAmount const& amount = offer->at(sfAmount);
|
||||
if (nft::getTransferFee(tokenID) != 0 &&
|
||||
// Fix a bug where the transfer of an NFToken with a transfer fee could
|
||||
// give the NFToken issuer an undesired trust line.
|
||||
// Issuer doesn't need a trust line to accept their own currency.
|
||||
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline) &&
|
||||
(nft::getFlags(tokenID) & nft::flagCreateTrustLines) == 0 &&
|
||||
!amount.native())
|
||||
nftMinter != amount.getIssuer() &&
|
||||
!ctx.view.read(keylet::line(nftMinter, amount.issue())))
|
||||
return tecNO_LINE;
|
||||
|
||||
// Check that the issuer is allowed to receive IOUs.
|
||||
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2))
|
||||
{
|
||||
auto const issuer = nft::getIssuer(tokenID);
|
||||
// Issuer doesn't need a trust line to accept their own currency.
|
||||
if (issuer != amount.getIssuer() &&
|
||||
!ctx.view.read(keylet::line(issuer, amount.issue())))
|
||||
return tecNO_LINE;
|
||||
auto res = nft::checkTrustlineAuthorized(
|
||||
ctx.view, nftMinter, ctx.j, amount.asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
|
||||
res = nft::checkTrustlineDeepFrozen(
|
||||
ctx.view, nftMinter, ctx.j, amount.asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -524,62 +609,4 @@ NFTokenAcceptOffer::doApply()
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
TER
|
||||
NFTokenAcceptOffer::checkAcceptAsset(
|
||||
ReadView const& view,
|
||||
ApplyFlags const flags,
|
||||
AccountID const id,
|
||||
beast::Journal const j,
|
||||
Issue const& issue)
|
||||
{
|
||||
// Only valid for custom currencies
|
||||
|
||||
if (!view.rules().enabled(featureDeepFreeze))
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
!isXRP(issue.currency),
|
||||
"NFTokenAcceptOffer::checkAcceptAsset : valid to check.");
|
||||
auto const issuerAccount = view.read(keylet::account(issue.account));
|
||||
|
||||
if (!issuerAccount)
|
||||
{
|
||||
JLOG(j.debug())
|
||||
<< "delay: can't receive IOUs from non-existent issuer: "
|
||||
<< to_string(issue.account);
|
||||
|
||||
return tecNO_ISSUER;
|
||||
}
|
||||
|
||||
// An account can not create a trustline to itself, so no line can exist
|
||||
// to be frozen. Additionally, an issuer can always accept its own
|
||||
// issuance.
|
||||
if (issue.account == id)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
auto const trustLine =
|
||||
view.read(keylet::line(id, issue.account, issue.currency));
|
||||
|
||||
if (!trustLine)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// There's no difference which side enacted deep freeze, accepting
|
||||
// tokens shouldn't be possible.
|
||||
bool const deepFrozen =
|
||||
(*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze);
|
||||
|
||||
if (deepFrozen)
|
||||
{
|
||||
return tecFROZEN;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -44,14 +44,6 @@ private:
|
||||
AccountID const& seller,
|
||||
uint256 const& nfTokenID);
|
||||
|
||||
static TER
|
||||
checkAcceptAsset(
|
||||
ReadView const& view,
|
||||
ApplyFlags const flags,
|
||||
AccountID const id,
|
||||
beast::Journal const j,
|
||||
Issue const& issue);
|
||||
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
|
||||
@@ -1004,6 +1004,18 @@ tokenOfferCreatePreclaim(
|
||||
}
|
||||
}
|
||||
|
||||
if (view.rules().enabled(fixEnforceNFTokenTrustlineV2) && !amount.native())
|
||||
{
|
||||
// If this is a sell offer, check that the account is allowed to
|
||||
// receive IOUs. If this is a buy offer, we have to check that trustline
|
||||
// is authorized, even though we previosly checked it's balance via
|
||||
// accountHolds. This is due to a possibility of existence of
|
||||
// unauthorized trustlines with balance
|
||||
auto const res = nft::checkTrustlineAuthorized(
|
||||
view, acctID, j, amount.asset().get<Issue>());
|
||||
if (res != tesSUCCESS)
|
||||
return res;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -1081,5 +1093,115 @@ tokenOfferCreateApply(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
checkTrustlineAuthorized(
|
||||
ReadView const& view,
|
||||
AccountID const id,
|
||||
beast::Journal const j,
|
||||
Issue const& issue)
|
||||
{
|
||||
// Only valid for custom currencies
|
||||
XRPL_ASSERT(
|
||||
!isXRP(issue.currency),
|
||||
"ripple::nft::checkTrustlineAuthorized : valid to check.");
|
||||
|
||||
if (view.rules().enabled(fixEnforceNFTokenTrustlineV2))
|
||||
{
|
||||
auto const issuerAccount = view.read(keylet::account(issue.account));
|
||||
if (!issuerAccount)
|
||||
{
|
||||
JLOG(j.debug()) << "ripple::nft::checkTrustlineAuthorized: can't "
|
||||
"receive IOUs from non-existent issuer: "
|
||||
<< to_string(issue.account);
|
||||
|
||||
return tecNO_ISSUER;
|
||||
}
|
||||
|
||||
// An account can not create a trustline to itself, so no line can
|
||||
// exist to be authorized. Additionally, an issuer can always accept
|
||||
// its own issuance.
|
||||
if (issue.account == id)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (issuerAccount->isFlag(lsfRequireAuth))
|
||||
{
|
||||
auto const trustLine =
|
||||
view.read(keylet::line(id, issue.account, issue.currency));
|
||||
|
||||
if (!trustLine)
|
||||
{
|
||||
return tecNO_LINE;
|
||||
}
|
||||
|
||||
// Entries have a canonical representation, determined by a
|
||||
// lexicographical "greater than" comparison employing strict
|
||||
// weak ordering. Determine which entry we need to access.
|
||||
if (!trustLine->isFlag(
|
||||
id > issue.account ? lsfLowAuth : lsfHighAuth))
|
||||
{
|
||||
return tecNO_AUTH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
checkTrustlineDeepFrozen(
|
||||
ReadView const& view,
|
||||
AccountID const id,
|
||||
beast::Journal const j,
|
||||
Issue const& issue)
|
||||
{
|
||||
// Only valid for custom currencies
|
||||
XRPL_ASSERT(
|
||||
!isXRP(issue.currency),
|
||||
"ripple::nft::checkTrustlineDeepFrozen : valid to check.");
|
||||
|
||||
if (view.rules().enabled(featureDeepFreeze))
|
||||
{
|
||||
auto const issuerAccount = view.read(keylet::account(issue.account));
|
||||
if (!issuerAccount)
|
||||
{
|
||||
JLOG(j.debug()) << "ripple::nft::checkTrustlineDeepFrozen: can't "
|
||||
"receive IOUs from non-existent issuer: "
|
||||
<< to_string(issue.account);
|
||||
|
||||
return tecNO_ISSUER;
|
||||
}
|
||||
|
||||
// An account can not create a trustline to itself, so no line can
|
||||
// exist to be frozen. Additionally, an issuer can always accept its
|
||||
// own issuance.
|
||||
if (issue.account == id)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
auto const trustLine =
|
||||
view.read(keylet::line(id, issue.account, issue.currency));
|
||||
|
||||
if (!trustLine)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// There's no difference which side enacted deep freeze, accepting
|
||||
// tokens shouldn't be possible.
|
||||
bool const deepFrozen =
|
||||
(*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze);
|
||||
|
||||
if (deepFrozen)
|
||||
{
|
||||
return tecFROZEN;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace nft
|
||||
} // namespace ripple
|
||||
|
||||
@@ -152,6 +152,20 @@ tokenOfferCreateApply(
|
||||
beast::Journal j,
|
||||
std::uint32_t txFlags = lsfSellNFToken);
|
||||
|
||||
TER
|
||||
checkTrustlineAuthorized(
|
||||
ReadView const& view,
|
||||
AccountID const id,
|
||||
beast::Journal const j,
|
||||
Issue const& issue);
|
||||
|
||||
TER
|
||||
checkTrustlineDeepFrozen(
|
||||
ReadView const& view,
|
||||
AccountID const id,
|
||||
beast::Journal const j,
|
||||
Issue const& issue);
|
||||
|
||||
} // namespace nft
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
Reference in New Issue
Block a user