Fix granular permission for Attackathon (#6831)

Co-authored-by: yinyiqian1 <yqian@ripple.com>
This commit is contained in:
Kenny Lei
2026-04-09 12:08:18 -07:00
committed by GitHub
parent dfa1da47fa
commit 87e951470d
17 changed files with 542 additions and 228 deletions

View File

@@ -26,10 +26,7 @@ checkTxPermission(std::shared_ptr<SLE const> const& delegate, STTx const& tx);
* @param granularPermissions Granted granular permissions tied to the
* transaction type.
*/
void
loadGranularPermission(
std::shared_ptr<SLE const> const& delegate,
TxType const& type,
std::unordered_set<GranularPermissionType>& granularPermissions);
std::unordered_set<GranularPermissionType>
getGranularPermission(std::shared_ptr<SLE const> const& delegate, TxType const& type);
} // namespace xrpl

View File

@@ -7,8 +7,12 @@
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
namespace xrpl {
class STTx;
/**
* We have both transaction type permissions and granular type permissions.
* Since we will reuse the TransactionFormats to parse the Transaction
@@ -17,15 +21,15 @@ namespace xrpl {
* greater than the maximum value of uint16.
*/
enum GranularPermissionType : std::uint32_t {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#define PERMISSION(type, txType, value) type = value,
#define GRANULAR_PERMISSION(name, txType, value, allowedFlags, allowedFields) name = value,
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
};
enum Delegation { delegable, notDelegable };
@@ -36,15 +40,16 @@ private:
Permission();
std::unordered_map<std::uint16_t, uint256> txFeatureMap_;
std::unordered_map<std::uint16_t, Delegation> delegableTx_;
std::unordered_map<std::string, GranularPermissionType> granularPermissionMap_;
std::unordered_map<GranularPermissionType, std::string> granularNameMap_;
std::unordered_map<GranularPermissionType, TxType> granularTxTypeMap_;
std::unordered_map<GranularPermissionType, std::uint32_t> granularPermittedFlags_;
std::unordered_map<GranularPermissionType, SOTemplate> granularTemplates_;
std::unordered_set<TxType> granularTxTypes_;
public:
static Permission const&
getInstance();
@@ -71,6 +76,9 @@ public:
bool
isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const;
bool
hasGranularPermissions(TxType txType) const;
// for tx level permission, permission value is equal to tx type plus one
static uint32_t
txToPermissionType(TxType const& type);
@@ -78,6 +86,23 @@ public:
// tx type value is permission value minus one
static TxType
permissionToTxType(uint32_t const& value);
/**
* @brief Verifies a delegated transaction against its granular permission template.
*
* @note WARNING: Do not move this check before standard transaction-level
* format checks, which is in preclaim. This function assumes the transaction's
* base structural integrity (fees, sequence, signatures) has already been
* validated.
*
* @param tx The transaction to verify.
* @param heldPermissions The granular permissions that the sender hold.
* @return true if the transaction fields and flags comply with the granular template.
*/
[[nodiscard]] bool
checkGranularSandbox(
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldPermissions) const;
};
} // namespace xrpl

View File

@@ -1,49 +1,80 @@
#if !defined(PERMISSION)
#error "undefined macro: PERMISSION"
#if !defined(GRANULAR_PERMISSION)
#error "undefined macro: GRANULAR_PERMISSION"
#endif
/**
* PERMISSION(name, type, txType, value)
* GRANULAR_PERMISSION(name, txType, value, allowedFlags, allowedFields)
*
* This macro defines a permission:
* name: the name of the permission.
* type: the GranularPermissionType enum.
* txType: the corresponding TxType for this permission.
* value: the uint32 numeric value for the enum type.
* Defines a granular permission:
* name: the granular permission name.
* txType: the corresponding TxType for this permission.
* value: the uint32 numeric value for the enum type.
* allowedFlags: transaction flags permitted under this permission.
* allowedFields: transaction fields permitted under this permission.
*/
/** This permission grants the delegated account the ability to authorize a trustline. */
PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537)
/** Grants the ability to authorize a trustline. */
GRANULAR_PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537, tfUniversal | tfSetfAuth,
({{sfLimitAmount, soeREQUIRED}}))
/** This permission grants the delegated account the ability to freeze a trustline. */
PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538)
/** Grants the ability to freeze a trustline. */
GRANULAR_PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538, tfUniversal | tfSetFreeze,
({{sfLimitAmount, soeREQUIRED}}))
/** This permission grants the delegated account the ability to unfreeze a trustline. */
PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539)
/** Grants the ability to unfreeze a trustline. */
GRANULAR_PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539, tfUniversal | tfClearFreeze,
({{sfLimitAmount, soeREQUIRED}}))
/** This permission grants the delegated account the ability to set Domain. */
PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540)
/** Grants the ability to set Domain. */
GRANULAR_PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540, tfUniversal,
({{sfDomain, soeOPTIONAL}}))
/** This permission grants the delegated account the ability to set EmailHashSet. */
PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541)
/** Grants the ability to set EmailHash. */
GRANULAR_PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541, tfUniversal,
({{sfEmailHash, soeOPTIONAL}}))
/** This permission grants the delegated account the ability to set MessageKey. */
PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542)
/** Grants the ability to set MessageKey. */
GRANULAR_PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542, tfUniversal,
({{sfMessageKey, soeOPTIONAL}}))
/** This permission grants the delegated account the ability to set TransferRate. */
PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543)
/** Grants the ability to set TransferRate. */
GRANULAR_PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543, tfUniversal,
({{sfTransferRate, soeOPTIONAL}}))
/** This permission grants the delegated account the ability to set TickSize. */
PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544)
/** Grants the ability to set TickSize. */
GRANULAR_PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544, tfUniversal,
({{sfTickSize, soeOPTIONAL}}))
/** This permission grants the delegated account the ability to mint payment, which means sending a payment for a currency where the sending account is the issuer. */
PERMISSION(PaymentMint, ttPAYMENT, 65545)
/** Grants the ability to mint payment (sending account is the issuer). Cross-currency payments are disallowed. */
GRANULAR_PERMISSION(PaymentMint, ttPAYMENT, 65545,
tfUniversal | tfPartialPayment | tfLimitQuality | tfNoRippleDirect,
({{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED},
{sfSendMax, soeOPTIONAL},
{sfInvoiceID, soeOPTIONAL},
{sfDestinationTag, soeOPTIONAL},
{sfDeliverMin, soeOPTIONAL},
{sfCredentialIDs, soeOPTIONAL},
{sfDomainID, soeOPTIONAL}}))
/** This permission grants the delegated account the ability to burn payment, which means sending a payment for a currency where the destination account is the issuer */
PERMISSION(PaymentBurn, ttPAYMENT, 65546)
/** Grants the ability to burn payment (destination account is the issuer). Cross-currency payments are disallowed. */
GRANULAR_PERMISSION(PaymentBurn, ttPAYMENT, 65546,
tfUniversal | tfPartialPayment | tfLimitQuality | tfNoRippleDirect,
({{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED},
{sfSendMax, soeOPTIONAL},
{sfInvoiceID, soeOPTIONAL},
{sfDestinationTag, soeOPTIONAL},
{sfDeliverMin, soeOPTIONAL},
{sfCredentialIDs, soeOPTIONAL},
{sfDomainID, soeOPTIONAL}}))
/** This permission grants the delegated account the ability to lock MPToken. */
PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547)
/** Grants the ability to lock an MPToken. */
GRANULAR_PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547, tfUniversal | tfMPTLock,
({{sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeOPTIONAL}}))
/** This permission grants the delegated account the ability to unlock MPToken. */
PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548)
/** Grants the ability to unlock an MPToken. */
GRANULAR_PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548, tfUniversal | tfMPTUnlock,
({{sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeOPTIONAL}}))

View File

@@ -206,8 +206,47 @@ public:
return tesSUCCESS;
}
/**
* This function can be overridden to introduce additional semantic constraints beyond the
* granular template validation for granular permissions. It is called by the base
* checkPermission method only after the transaction has successfully passed
* checkGranularSandbox.
*/
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions)
{
return tesSUCCESS;
}
/**
* Checks whether the transaction is authorized to be executed by the delegated account.
* This function enforces the strict permission check hierarchy. It is explicitly
* designed NOT to be overridden. Derived transactors must instead implement
* checkGranularSemantics to add custom validation logic for granular permissions.
*
* The evaluation proceeds as follows:
* - If transaction-level permission is granted, the function immediately returns tesSUCCESS.
* - If transaction-level permission is not granted, the function checks whether the transaction
* matches the granular permission template defined in permissions.macro. If it does, it then
* calls checkGranularSemantics to perform any additional, fine-grained validation.
*
*/
template <class T>
static NotTEC
checkPermission(ReadView const& view, STTx const& tx)
{
std::unordered_set<GranularPermissionType> heldGranularPermissions;
if (NotTEC const result = checkPermissionImpl(view, tx, heldGranularPermissions);
!isTesSuccess(result) || heldGranularPermissions.empty())
{
return result;
}
return T::checkGranularSemantics(view, tx, heldGranularPermissions);
}
/////////////////////////////////////////////////////
// Interface used by AccountDelete
@@ -294,6 +333,12 @@ protected:
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
private:
static NotTEC
checkPermissionImpl(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType>& heldGranularPermissions);
std::pair<TER, XRPAmount>
reset(XRPAmount fee);

View File

@@ -23,9 +23,6 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -32,7 +32,10 @@ public:
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -22,9 +22,6 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -21,7 +21,10 @@ public:
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -1,6 +1,8 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace xrpl {
@@ -32,41 +34,70 @@ Permission::Permission()
};
granularPermissionMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#define PERMISSION(type, txType, value) {#type, type},
#define GRANULAR_PERMISSION(type, txType, value, flags, fields) {#type, type},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
};
granularNameMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#define PERMISSION(type, txType, value) {type, #type},
#define GRANULAR_PERMISSION(type, txType, value, flags, fields) {type, #type},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
};
granularTxTypeMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#define PERMISSION(type, txType, value) {type, txType},
#define GRANULAR_PERMISSION(type, txType, value, flags, fields) {type, txType},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
};
{
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#define GRANULAR_PERMISSION(type, txType, value, flags, fields) \
granularPermittedFlags_[type] = (flags);
#include <xrpl/protocol/detail/permissions.macro>
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
}
{
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#define GRANULAR_PERMISSION(type, txType, value, flags, fields) \
granularTemplates_.emplace( \
std::piecewise_construct, \
std::forward_as_tuple(type), \
std::forward_as_tuple(std::vector<SOElement> fields, TxFormats::getCommonFields()));
#include <xrpl/protocol/detail/permissions.macro>
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
}
XRPL_ASSERT(
txFeatureMap_.size() == delegableTx_.size(),
"xrpl::Permission : txFeatureMap_ and delegableTx_ must have same "
@@ -79,6 +110,9 @@ Permission::Permission()
"xrpl::Permission::granularPermissionMap_ : granular permission "
"value must not exceed the maximum uint16_t value.");
}
for (auto const& [gp, txType] : granularTxTypeMap_)
granularTxTypes_.insert(txType);
}
Permission const&
@@ -133,6 +167,12 @@ Permission::getGranularTxType(GranularPermissionType const& gpType) const
return std::nullopt;
}
bool
Permission::hasGranularPermissions(TxType txType) const
{
return granularTxTypes_.contains(txType);
}
std::optional<std::reference_wrapper<uint256 const>>
Permission::getTxFeature(TxType txType) const
{
@@ -192,4 +232,48 @@ Permission::permissionToTxType(uint32_t const& value)
return static_cast<TxType>(value - 1);
}
bool
Permission::checkGranularSandbox(
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldPermissions) const
{
auto const txFlags = tx.getFlags();
// Build union of flags and templates across all held permissions.
std::uint32_t unionFlags = 0;
for (auto const& gp : heldPermissions)
{
auto const it = granularPermittedFlags_.find(gp);
if (it != granularPermittedFlags_.end())
unionFlags |= it->second;
}
// Check if flags are permitted
if ((txFlags & ~unionFlags) != 0)
return false;
// Check if fields are permitted. Every present field must appear in at least one held
// permission's template. The common fields are included in the constructor.
for (auto const& field : tx)
{
if (field.getSType() == STI_NOTPRESENT)
continue;
bool fieldAllowed = false;
for (auto const& gp : heldPermissions)
{
auto const it = granularTemplates_.find(gp);
if (it != granularTemplates_.end() && it->second.getIndex(field.getFName()) != -1)
{
fieldAllowed = true;
break;
}
}
if (!fieldAllowed)
return false;
}
return true;
}
} // namespace xrpl

View File

@@ -256,19 +256,33 @@ Transactor::preflightSigValidated(PreflightContext const& ctx)
}
NotTEC
Transactor::checkPermission(ReadView const& view, STTx const& tx)
Transactor::checkPermissionImpl(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType>& heldGranularPermissions)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
auto const sle = view.read(keylet::delegate(tx[sfAccount], *delegate));
if (!sle)
return terNO_DELEGATE_PERMISSION;
return checkTxPermission(sle, tx);
if (isTesSuccess(checkTxPermission(sle, tx)))
return tesSUCCESS;
if (!Permission::getInstance().hasGranularPermissions(tx.getTxnType()))
return terNO_DELEGATE_PERMISSION;
heldGranularPermissions = getGranularPermission(sle, tx.getTxnType());
if (heldGranularPermissions.empty())
return terNO_DELEGATE_PERMISSION;
if (!Permission::getInstance().checkGranularSandbox(tx, heldGranularPermissions))
return terNO_DELEGATE_PERMISSION;
return tesSUCCESS;
}
XRPAmount

View File

@@ -174,7 +174,7 @@ invoke_preclaim(PreclaimContext const& ctx)
if (NotTEC const result = T::checkPriorTxAndLastLedger(ctx))
return result;
if (NotTEC const result = T::checkPermission(ctx.view, ctx.tx))
if (NotTEC const result = Transactor::checkPermission<T>(ctx.view, ctx.tx))
return result;
if (NotTEC const result = T::checkSign(ctx))

View File

@@ -155,55 +155,6 @@ AccountSet::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
NotTEC
AccountSet::checkPermission(ReadView const& view, STTx const& tx)
{
// AccountSet is prohibited to be granted on a transaction level,
// but some granular permissions are allowed.
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return terNO_DELEGATE_PERMISSION;
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttACCOUNT_SET, granularPermissions);
auto const uSetFlag = tx.getFieldU32(sfSetFlag);
auto const uClearFlag = tx.getFieldU32(sfClearFlag);
auto const uTxFlags = tx.getFlags();
// We don't support any flag based granular permission under
// AccountSet transaction. If any delegated account is trying to
// update the flag on behalf of another account, it is not
// authorized.
if (uSetFlag != 0 || uClearFlag != 0 || ((uTxFlags & tfUniversalMask) != 0u))
return terNO_DELEGATE_PERMISSION;
if (tx.isFieldPresent(sfEmailHash) && !granularPermissions.contains(AccountEmailHashSet))
return terNO_DELEGATE_PERMISSION;
if (tx.isFieldPresent(sfWalletLocator) || tx.isFieldPresent(sfNFTokenMinter))
return terNO_DELEGATE_PERMISSION;
if (tx.isFieldPresent(sfMessageKey) && !granularPermissions.contains(AccountMessageKeySet))
return terNO_DELEGATE_PERMISSION;
if (tx.isFieldPresent(sfDomain) && !granularPermissions.contains(AccountDomainSet))
return terNO_DELEGATE_PERMISSION;
if (tx.isFieldPresent(sfTransferRate) && !granularPermissions.contains(AccountTransferRateSet))
return terNO_DELEGATE_PERMISSION;
if (tx.isFieldPresent(sfTickSize) && !granularPermissions.contains(AccountTickSizeSet))
return terNO_DELEGATE_PERMISSION;
return tesSUCCESS;
}
TER
AccountSet::preclaim(PreclaimContext const& ctx)
{

View File

@@ -21,14 +21,12 @@ checkTxPermission(std::shared_ptr<SLE const> const& delegate, STTx const& tx)
return terNO_DELEGATE_PERMISSION;
}
void
loadGranularPermission(
std::shared_ptr<SLE const> const& delegate,
TxType const& txType,
std::unordered_set<GranularPermissionType>& granularPermissions)
std::unordered_set<GranularPermissionType>
getGranularPermission(std::shared_ptr<SLE const> const& delegate, TxType const& txType)
{
std::unordered_set<GranularPermissionType> granularPermissions;
if (!delegate)
return;
return granularPermissions; // LCOV_EXCL_LINE
auto const permissionArray = delegate->getFieldArray(sfPermissions);
for (auto const& permission : permissionArray)
@@ -39,6 +37,8 @@ loadGranularPermission(
if (type && *type == txType)
granularPermissions.insert(granularValue);
}
return granularPermissions;
}
} // namespace xrpl

View File

@@ -239,38 +239,24 @@ Payment::preflight(PreflightContext const& ctx)
}
NotTEC
Payment::checkPermission(ReadView const& view, STTx const& tx)
Payment::checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return terNO_DELEGATE_PERMISSION;
if (isTesSuccess(checkTxPermission(sle, tx)))
return tesSUCCESS;
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttPAYMENT, granularPermissions);
auto const& dstAmount = tx.getFieldAmount(sfAmount);
auto const& amountAsset = dstAmount.asset();
// Granular permissions are only valid for direct payments.
if ((tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset) ||
tx.isFieldPresent(sfPaths))
if (tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset)
return terNO_DELEGATE_PERMISSION;
// PaymentMint and PaymentBurn apply to both IOU and MPT direct payments.
if (granularPermissions.contains(PaymentMint) && !isXRP(amountAsset) &&
if (heldGranularPermissions.contains(PaymentMint) && !isXRP(amountAsset) &&
amountAsset.getIssuer() == tx[sfAccount])
return tesSUCCESS;
if (granularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) &&
if (heldGranularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) &&
amountAsset.getIssuer() == tx[sfDestination])
return tesSUCCESS;

View File

@@ -151,41 +151,6 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
NotTEC
MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return terNO_DELEGATE_PERMISSION;
if (isTesSuccess(checkTxPermission(sle, tx)))
return tesSUCCESS;
auto const txFlags = tx.getFlags();
// this is added in case more flags will be added for MPTokenIssuanceSet
// in the future. Currently unreachable.
if ((txFlags & tfMPTokenIssuanceSetMask) != 0u)
return terNO_DELEGATE_PERMISSION; // LCOV_EXCL_LINE
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions);
if (((txFlags & tfMPTLock) != 0u) && !granularPermissions.contains(MPTokenIssuanceLock))
return terNO_DELEGATE_PERMISSION;
if (((txFlags & tfMPTUnlock) != 0u) && !granularPermissions.contains(MPTokenIssuanceUnlock))
return terNO_DELEGATE_PERMISSION;
return tesSUCCESS;
}
TER
MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
{

View File

@@ -107,53 +107,21 @@ TrustSet::preflight(PreflightContext const& ctx)
}
NotTEC
TrustSet::checkPermission(ReadView const& view, STTx const& tx)
TrustSet::checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return terNO_DELEGATE_PERMISSION;
if (isTesSuccess(checkTxPermission(sle, tx)))
return tesSUCCESS;
std::uint32_t const txFlags = tx.getFlags();
// Currently we only support TrustlineAuthorize, TrustlineFreeze and
// TrustlineUnfreeze granular permission. Setting other flags returns
// error.
if ((txFlags & tfTrustSetPermissionMask) != 0u)
return terNO_DELEGATE_PERMISSION;
if (tx.isFieldPresent(sfQualityIn) || tx.isFieldPresent(sfQualityOut))
return terNO_DELEGATE_PERMISSION;
auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount);
auto const sleRippleState = view.read(
keylet::line(
tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.get<Issue>().currency));
// if the trustline does not exist, granular permissions are
// not allowed to create trustline
// granular permissions are not allowed to create a trustline
if (!sleRippleState)
return terNO_DELEGATE_PERMISSION;
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttTRUST_SET, granularPermissions);
if (((txFlags & tfSetfAuth) != 0u) && !granularPermissions.contains(TrustlineAuthorize))
return terNO_DELEGATE_PERMISSION;
if (((txFlags & tfSetFreeze) != 0u) && !granularPermissions.contains(TrustlineFreeze))
return terNO_DELEGATE_PERMISSION;
if (((txFlags & tfClearFreeze) != 0u) && !granularPermissions.contains(TrustlineUnfreeze))
return terNO_DELEGATE_PERMISSION;
// updating LimitAmount is not allowed only with granular permissions,
// updating LimitAmount is not allowed with granular permissions,
// unless there's a new granular permission for this in the future.
auto const curLimit = tx[sfAccount] > saLimitAmount.getIssuer()
? sleRippleState->getFieldAmount(sfHighLimit)

View File

@@ -870,6 +870,54 @@ class Delegate_test : public beast::unit_test::suite
}
}
// PaymentMint/PaymentBurn with sfSendMax of the same asset is allowed,
// same-asset SendMax is still a direct payment, not cross-currency.
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
Account const gw{"gw"};
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.trust(USD(200), alice);
env.close();
env(delegate::set(gw, bob, {"PaymentMint"}));
env.close();
// sfSendMax with same asset as sfAmount, still a direct payment
env(pay(gw, alice, USD(50)), sendmax(USD(50)), delegate::as(bob));
env.require(balance(alice, USD(50)));
env(delegate::set(alice, bob, {"PaymentBurn"}));
env.close();
env(pay(alice, gw, USD(30)), sendmax(USD(30)), delegate::as(bob));
env.require(balance(alice, USD(20)));
}
// Delegate account holds no granular permissions for the tx type:
// getGranularPermission returns empty set.
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
Account const gw{"gw"};
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.trust(USD(200), alice);
env.close();
// Bob holds only an AccountSet granular permission.
env(delegate::set(alice, bob, {"AccountDomainSet"}));
env.close();
// Payment has granular permissions defined in permissions.macro,
// but bob only holds AccountSet's granular permission,
// getGranularPermission returns empty.
env(pay(alice, gw, USD(50)), delegate::as(bob), ter(terNO_DELEGATE_PERMISSION));
}
// PaymentMint and PaymentBurn for MPT
{
std::string logs;
@@ -926,6 +974,40 @@ class Delegate_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(bob, MPT) == bobMPT + MPT(100));
}
}
// Verify granular permissions of different tx types in the same SLE are scoped
// correctly. AccountSet permissions don't apply to Payment and vice versa
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const gw{"gw"};
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.trust(USD(200), alice);
env.close();
// Alice granted bob with both AccountDomainSet and PaymentMint.
env(delegate::set(alice, bob, {"AccountDomainSet", "PaymentMint"}));
env.close();
// PaymentMint fails at granular semantic check because alice is not the issuer.
env(pay(alice, gw, USD(50)), delegate::as(bob), ter(terNO_DELEGATE_PERMISSION));
// AccountDomainSet applies correctly to AccountSet
std::string const domain = "example.com";
auto jt = noop(alice);
jt[sfDomain] = strHex(domain);
jt[sfDelegate] = bob.human();
env(jt);
BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain));
// gw gives bob PaymentMint and bob can mint on gw's behalf
env(delegate::set(gw, bob, {"PaymentMint"}));
env.close();
env(pay(gw, alice, USD(50)), delegate::as(bob));
env.require(balance(alice, USD(50)));
}
}
void
@@ -1108,6 +1190,34 @@ class Delegate_test : public beast::unit_test::suite
env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfFullyCanonicalSig),
delegate::as(bob));
}
{
Env env(*this);
Account const gw{"gw"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env(fset(gw, asfRequireAuth));
env.close();
env(trust(alice, gw["USD"](50)));
env.close();
env(delegate::set(gw, bob, {"TrustlineAuthorize"}));
env.close();
env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::as(bob));
env.close();
// sfQualityOut is a valid TrustSet field, but not permitted in granular template
Json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth);
txJson[sfQualityOut.jsonName] = 100;
env(txJson, delegate::as(bob), ter(terNO_DELEGATE_PERMISSION));
// tfSetNoRipple is a valid flag for TrustSet, but not permitted in granular template
env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfSetNoRipple),
delegate::as(bob),
ter(terNO_DELEGATE_PERMISSION));
}
}
void
@@ -1263,7 +1373,9 @@ class Delegate_test : public beast::unit_test::suite
env(jv2, ter(terNO_DELEGATE_PERMISSION));
}
// can not set AccountSet flags on behalf of other account
// can not set AccountSet flags on behalf of other account,
// in permissions.macro, the template for AccountSet does
// not allow any flag set or clear.
{
Env env(*this);
auto const alice = Account{"alice"};
@@ -1359,6 +1471,71 @@ class Delegate_test : public beast::unit_test::suite
env(jt);
BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain));
}
// setting invalid field not in permissions.macro template will be rejected.
{
Env env(*this);
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
// Alice gives Bob permission to set her Domain
env(delegate::set(alice, bob, {"AccountDomainSet"}));
env.close();
std::string const domain = "example.com";
auto txJson = noop(alice);
txJson[sfDomain] = strHex(domain);
txJson[sfDelegate] = bob.human();
// sfNFTokenMinter is a valid field in AccountSet tx, but
// it is not permitted for granular template
txJson[sfNFTokenMinter] = bob.human();
env(txJson, ter(terNO_DELEGATE_PERMISSION));
}
// Delegated AccountSet with no fields and no flags is allowed,
// because it is allowed in the non-delegated case as well.
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
env(delegate::set(alice, bob, {"AccountDomainSet"}));
env.close();
auto jt = noop(alice);
jt[sfDelegate] = bob.human();
env(jt);
}
// Revoking all permissions deletes the SLE and subsequent attempts are rejected.
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
env(delegate::set(alice, bob, {"AccountDomainSet"}));
env.close();
std::string const domain = "example.com";
auto jt = noop(alice);
jt[sfDomain] = strHex(domain);
jt[sfDelegate] = bob.human();
env(jt);
// empty DelegateSet deletes the SLE
env(delegate::set(alice, bob, {}));
env.close();
env(jt, ter(terNO_DELEGATE_PERMISSION));
}
}
void
@@ -1479,6 +1656,37 @@ class Delegate_test : public beast::unit_test::suite
env.close();
mpt.set({.account = alice, .flags = tfMPTLock | tfFullyCanonicalSig, .delegate = bob});
}
// field not permitted to exist in granular delegation
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(100000), alice, bob);
MPTTester mpt(env, alice, {.fund = false});
mpt.create({.flags = tfMPTCanLock});
env.close();
// alice gives granular permission to bob for MPTokenIssuanceLock
env(delegate::set(alice, bob, {"MPTokenIssuanceLock"}));
env.close();
// Field is not permitted, permitted fields for delegation is defined in
// permissions.macro.
mpt.set(
{.account = alice,
.mutableFlags = 2,
.delegate = bob,
.err = terNO_DELEGATE_PERMISSION});
// Notice: flags not defined in permissions.macro are not permitted for delegation.
// Since preflight will check invalid flag for the tx, it is not reachable.
// If any new flag is defined into the transaction in the future,
// but is not allowed for delegation, the transaction will be rejected with
// terNO_DELEGATE_PERMISSION. The set of permitted flags for delegation is defined in
// permissions.macro.
}
}
void
@@ -1807,6 +2015,46 @@ class Delegate_test : public beast::unit_test::suite
}
}
void
testGranularSandboxCheckOrder()
{
testcase("Make sure GranularSandbox is checked after transaction-level permission");
using namespace jtx;
Env env(*this);
Account const gw{"gw"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env(fset(gw, asfRequireAuth));
env.close();
env(trust(alice, gw["USD"](50)));
env.close();
env(delegate::set(gw, bob, {"TrustlineAuthorize"}));
env.close();
env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::as(bob));
env.close();
// sfQualityOut is a valid TrustSet field, but not permitted in granular template
Json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth);
txJson[sfQualityOut.jsonName] = 100;
env(txJson, delegate::as(bob), ter(terNO_DELEGATE_PERMISSION));
// Now Alice grants Bob with transaction level permission
env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustSet"}));
env.close();
// NOTE: This case is to ensure that if a delegate possesses a
// transaction-level permission (e.g., TrustSet), the granular sandbox must not incorrectly
// block the transaction. The function checkGranularSandbox MUST be called after the
// transaction-level permission check. This test case is to avoid future refactor mistakes,
// modifying the order will fail here.
env(txJson, delegate::as(bob));
}
void
testTxDelegableCount()
{
@@ -1866,9 +2114,8 @@ class Delegate_test : public beast::unit_test::suite
STTx const tx{ttPAYMENT, [](STObject&) {}};
BEAST_EXPECT(checkTxPermission(nullptr, tx) == terNO_DELEGATE_PERMISSION);
// loadGranularPermission nullptr check
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(nullptr, ttPAYMENT, granularPermissions);
// getGranularPermission nullptr check
auto const granularPermissions = getGranularPermission(nullptr, ttPAYMENT);
BEAST_EXPECT(granularPermissions.empty());
}
@@ -1896,6 +2143,7 @@ class Delegate_test : public beast::unit_test::suite
testMultiSignQuorumNotMet();
testPermissionValue(all);
testTxRequireFeatures(all);
testGranularSandboxCheckOrder();
testTxDelegableCount();
testDelegateUtilsNullptrCheck();
}