mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-27 22:45:52 +00:00
Implement MPT domain checks
This commit is contained in:
@@ -186,6 +186,7 @@ enum LedgerSpecificFlags {
|
||||
|
||||
// ltMPTOKEN
|
||||
lsfMPTAuthorized = 0x00000002,
|
||||
lsfMPTDomainCheck = 0x00000004,
|
||||
|
||||
// ltCREDENTIAL
|
||||
lsfAccepted = 0x00010000,
|
||||
|
||||
@@ -405,6 +405,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfMPTokenMetadata, soeOPTIONAL},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A ledger object which tracks MPToken
|
||||
@@ -479,8 +480,8 @@ LEDGER_ENTRY(ltVAULT, 0x0083, Vault, vault, ({
|
||||
{sfAssetMaximum, soeDEFAULT},
|
||||
{sfLossUnrealized, soeDEFAULT},
|
||||
{sfMPTokenIssuanceID, soeREQUIRED}, // sfShare
|
||||
{sfDomainID, soeOPTIONAL}, // PermissionedDomainID
|
||||
// no ShareTotal ever (use MPTIssuance.sfOutstandingAmount)
|
||||
// no PermissionedDomainID (use MPTIssuance.sfDomainID)
|
||||
// no WithdrawalPolicy yet
|
||||
}))
|
||||
|
||||
|
||||
@@ -204,7 +204,9 @@ valid(ReadView const& view, uint256 domainID, AccountID const& subject)
|
||||
auto const sleCredential =
|
||||
view.read(keylet::credential(subject, issuer, type));
|
||||
|
||||
if (sleCredential && sleCredential->getFlags() & lsfAccepted)
|
||||
// Do not check for expired credentials here, we need ApplyView& for
|
||||
// that, to allow us to delete them (see verifyDomain below)
|
||||
if (sleCredential && (sleCredential->getFlags() & lsfAccepted))
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -300,16 +302,11 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j)
|
||||
|
||||
TER
|
||||
verifyDomain(
|
||||
ApplyContext& ctx,
|
||||
AccountID const& src,
|
||||
AccountID const& dst,
|
||||
std::shared_ptr<SLE> const& object)
|
||||
ApplyView& view,
|
||||
AccountID const& account,
|
||||
uint256 domainID,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (!object->isFieldPresent(sfDomainID))
|
||||
return tesSUCCESS;
|
||||
auto const domainID = object->getFieldH256(sfDomainID);
|
||||
|
||||
auto& view = ctx.view();
|
||||
auto const slePD = view.read(keylet::permissionedDomain(domainID));
|
||||
if (!slePD || !slePD->isFieldPresent(sfAcceptedCredentials))
|
||||
return tefINTERNAL;
|
||||
@@ -324,18 +321,13 @@ verifyDomain(
|
||||
|
||||
auto const issuer = h.getAccountID(sfIssuer);
|
||||
auto const type = makeSlice(h.getFieldVL(sfCredentialType));
|
||||
auto const keyletCredential = keylet::credential(dst, issuer, type);
|
||||
auto const keyletCredential = keylet::credential(account, issuer, type);
|
||||
if (view.exists(keyletCredential))
|
||||
credentials.push_back(keyletCredential.key);
|
||||
}
|
||||
|
||||
// Result intentionally ignored.
|
||||
[[maybe_unused]] bool _ =
|
||||
credentials::removeExpired(view, credentials, ctx.journal);
|
||||
|
||||
// Only do this check after we have removed expired credentials.
|
||||
if (src == dst)
|
||||
return tesSUCCESS;
|
||||
[[maybe_unused]] bool _ = credentials::removeExpired(view, credentials, j);
|
||||
|
||||
for (auto const& h : credentials)
|
||||
{
|
||||
|
||||
@@ -84,10 +84,10 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j);
|
||||
// object
|
||||
TER
|
||||
verifyDomain(
|
||||
ApplyContext& ctx,
|
||||
AccountID const& src,
|
||||
AccountID const& dst,
|
||||
std::shared_ptr<SLE> const& object);
|
||||
ApplyView& view,
|
||||
AccountID const& account,
|
||||
uint256 domainID,
|
||||
beast::Journal j);
|
||||
|
||||
// Check expired credentials and for existing DepositPreauth ledger object
|
||||
TER
|
||||
|
||||
@@ -212,7 +212,7 @@ CashCheck::preclaim(PreclaimContext const& ctx)
|
||||
if (!sleTrustLine)
|
||||
{
|
||||
// We can only create a trust line if the issuer does not
|
||||
// have requireAuth set.
|
||||
// have lsfRequireAuth set.
|
||||
return tecNO_AUTH;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace ripple {
|
||||
|
||||
struct MPTAuthorizeArgs
|
||||
{
|
||||
XRPAmount const& priorBalance{};
|
||||
XRPAmount const& priorBalance;
|
||||
uint192 const& mptIssuanceID;
|
||||
AccountID const& accountID;
|
||||
std::uint32_t flags{};
|
||||
|
||||
@@ -109,6 +109,9 @@ MPTokenIssuanceCreate::create(
|
||||
if (args.metadata)
|
||||
(*mptIssuance)[sfMPTokenMetadata] = *args.metadata;
|
||||
|
||||
if (args.domainId)
|
||||
(*mptIssuance)[sfDomainID] = *args.domainId;
|
||||
|
||||
view.insert(mptIssuance);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ struct MPTCreateArgs
|
||||
std::optional<std::uint8_t> assetScale{};
|
||||
std::optional<std::uint16_t> transferFee{};
|
||||
std::optional<Slice> const& metadata{};
|
||||
std::optional<uint256> domainId{};
|
||||
};
|
||||
|
||||
class MPTokenIssuanceCreate : public Transactor
|
||||
|
||||
@@ -146,6 +146,7 @@ VaultCreate::doApply()
|
||||
.sequence = 1,
|
||||
.flags = mptFlags,
|
||||
.metadata = tx[~sfMPTokenMetadata],
|
||||
.domainId = tx[~sfDomainID],
|
||||
});
|
||||
if (!maybeShare)
|
||||
return maybeShare.error();
|
||||
@@ -156,8 +157,6 @@ VaultCreate::doApply()
|
||||
vault->at(sfOwner) = ownerId;
|
||||
vault->at(sfAccount) = pseudoId;
|
||||
vault->at(sfAsset) = tx[sfAsset];
|
||||
if (tx.isFieldPresent(sfDomainID))
|
||||
vault->setFieldH256(sfDomainID, tx.getFieldH256(sfDomainID));
|
||||
// Leave default values for AssetTotal and AssetAvailable, both zero.
|
||||
if (auto value = tx[~sfAssetMaximum])
|
||||
vault->at(sfAssetMaximum) = *value;
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
|
||||
#include <xrpld/ledger/View.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -52,21 +54,20 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
if (!vault)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// Only the VaultDeposit transaction is subject to this permission check.
|
||||
if (vault->getFlags() == tfVaultPrivate &&
|
||||
ctx.tx[sfAccount] != vault->at(sfOwner))
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
if (vault->getFlags() == tfVaultPrivate && account != vault->at(sfOwner))
|
||||
{
|
||||
// Similar to credential::valid call inside Payment::prelaim (different
|
||||
// overload), if we do not see authorised credentials in preclaim, we do
|
||||
// not progress to doApply. This means that any expired credentials are
|
||||
// only deleted *if* we pass this check here in preclaim.
|
||||
if (auto const domain = vault->at(~sfVaultID))
|
||||
{
|
||||
if (auto const err =
|
||||
credentials::valid(ctx.view, *domain, ctx.tx[sfAccount]);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
auto const err = requireAuth(
|
||||
ctx.view, MPTIssue(vault->at(sfMPTokenIssuanceID)), account);
|
||||
return err;
|
||||
|
||||
// The above will perform authorization check based on DomainID stored
|
||||
// in MPTokenIssuance. Had this been a regular MPToken, it would also
|
||||
// allow use of authorization granted by the issuer explicitly, but
|
||||
// Vault does not have an MPT issuer (instead it uses pseudo-account).
|
||||
//
|
||||
// If we passed the above check then we also need to do similar check
|
||||
// inside doApply(), in order to check for expired credentials.
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -79,17 +80,6 @@ VaultDeposit::doApply()
|
||||
if (!vault)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
auto const dst = ctx_.tx[sfAccount];
|
||||
auto const src = vault->at(sfOwner);
|
||||
|
||||
if (vault->getFlags() & lsfVaultPrivate)
|
||||
{
|
||||
// TODO move DomainID from vault to MPTokenIssuance
|
||||
if (auto const err = verifyDomain(ctx_, src, dst, vault);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
auto const assets = ctx_.tx[sfAmount];
|
||||
Asset const& asset = vault->at(sfAsset);
|
||||
if (assets.asset() != asset)
|
||||
@@ -107,30 +97,17 @@ VaultDeposit::doApply()
|
||||
}
|
||||
|
||||
// Make sure the depositor can hold shares.
|
||||
auto share = (*vault)[sfMPTokenIssuanceID];
|
||||
auto maybeToken = findToken(view(), MPTIssue(share), account_);
|
||||
if (!maybeToken)
|
||||
{
|
||||
if (maybeToken.error() == tecNO_LINE)
|
||||
{
|
||||
if (auto ter = MPTokenAuthorize::authorize(
|
||||
view(),
|
||||
j_,
|
||||
{.priorBalance = mPriorBalance,
|
||||
.mptIssuanceID = share,
|
||||
.accountID = account_}))
|
||||
return ter;
|
||||
}
|
||||
else if (maybeToken.error() != tesSUCCESS)
|
||||
{
|
||||
return maybeToken.error();
|
||||
}
|
||||
}
|
||||
MPTIssue const mptIssue((*vault)[sfMPTokenIssuanceID]);
|
||||
if (auto const err =
|
||||
verifyAuth(ctx_.view(), mptIssue, account_, mPriorBalance, j_);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
// Compute exchange before transferring any amounts.
|
||||
auto const shares = assetsToSharesDeposit(view(), vault, assets);
|
||||
XRPL_ASSERT(
|
||||
shares.asset() != assets.asset(), "do not mix up assets and shares");
|
||||
shares.asset() != assets.asset(),
|
||||
"ripple::VaultDeposit::doApply : assets are not shares");
|
||||
|
||||
vault->at(sfAssetTotal) += assets;
|
||||
vault->at(sfAssetAvailable) += assets;
|
||||
|
||||
@@ -573,27 +573,43 @@ struct TokenDescriptor
|
||||
std::shared_ptr<SLE const> issuance;
|
||||
};
|
||||
|
||||
[[nodiscard]] Expected<TokenDescriptor, TER>
|
||||
findToken(
|
||||
ReadView const& view,
|
||||
MPTIssue const& mptIssue,
|
||||
AccountID const& account);
|
||||
|
||||
[[nodiscard]] TER
|
||||
requireAuth(ReadView const& view, Issue const& issue, AccountID const& account);
|
||||
|
||||
/** Check if the account lacks required authorization.
|
||||
*
|
||||
* Return tecNO_AUTH or tecNO_LINE if it does
|
||||
* and tesSUCCESS otherwise.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
requireAuth(ReadView const& view, Issue const& issue, AccountID const& account);
|
||||
|
||||
/** Check if the account lacks required authorization.
|
||||
*
|
||||
* Does not check for expired credentials, hence must be followed by
|
||||
* verifyAuth from doApply
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
requireAuth(
|
||||
ReadView const& view,
|
||||
MPTIssue const& mptIssue,
|
||||
AccountID const& account);
|
||||
|
||||
/** Check if the account lacks required authorization.
|
||||
*
|
||||
* Called from doApply - it will check for expired (and delete if found any)
|
||||
* credentials maching DomainID set in MPTIssuance. Must be called if
|
||||
* requireAuth(...MPTIssue...) check passed in preclaim. Will create MPToken
|
||||
* if needed, on the basis of non-expired credentals found.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
verifyAuth(
|
||||
ApplyView& view,
|
||||
MPTIssue const& mptIssue,
|
||||
AccountID const& account,
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal j);
|
||||
|
||||
/** Check if the destination account is allowed
|
||||
* to receive MPT. Return tecNO_AUTH if it doesn't
|
||||
* and tesSUCCESS otherwise.
|
||||
|
||||
@@ -19,15 +19,20 @@
|
||||
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
// TODO: Move the helper out of the `app` module.
|
||||
#include <xrpld/app/misc/CredentialHelpers.h>
|
||||
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
|
||||
#include <xrpld/ledger/View.h>
|
||||
#include <xrpl/basics/Expected.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/st.h>
|
||||
@@ -341,6 +346,8 @@ accountHolds(
|
||||
|
||||
auto const sleMpt =
|
||||
view.read(keylet::mptoken(mptIssue.getMptID(), account));
|
||||
|
||||
// TODO check authorization per DomainID stored in MPTokenIssuance
|
||||
if (!sleMpt)
|
||||
amount.clear(mptIssue);
|
||||
else if (
|
||||
@@ -1197,7 +1204,8 @@ removeEmptyHolding(
|
||||
return MPTokenAuthorize::authorize(
|
||||
view,
|
||||
journal,
|
||||
{.mptIssuanceID = mptID,
|
||||
{.priorBalance = {},
|
||||
.mptIssuanceID = mptID,
|
||||
.accountID = accountID,
|
||||
.flags = tfMPTUnauthorize});
|
||||
}
|
||||
@@ -1603,6 +1611,7 @@ rippleCreditMPT(
|
||||
}
|
||||
else
|
||||
return tecNO_AUTH;
|
||||
// TODO check authorization per DomainID stored in MPTokenIssuance
|
||||
}
|
||||
|
||||
if (uReceiverID == issuer)
|
||||
@@ -1627,6 +1636,7 @@ rippleCreditMPT(
|
||||
}
|
||||
else
|
||||
return tecNO_AUTH;
|
||||
// TODO check authorization per DomainID stored in MPTokenIssuanced
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -2031,29 +2041,42 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] Expected<TokenDescriptor, TER>
|
||||
findToken(
|
||||
[[nodiscard]] Expected<TokenDescriptor, TER> static findToken(
|
||||
ReadView const& view,
|
||||
MPTIssue const& mptIssue,
|
||||
AccountID const& account)
|
||||
{
|
||||
auto const mptID = keylet::mptIssuance(mptIssue.getMptID());
|
||||
auto const sleIssuance = view.read(mptID);
|
||||
|
||||
if (!sleIssuance)
|
||||
return Unexpected(tecOBJECT_NOT_FOUND);
|
||||
|
||||
auto const mptIssuer = sleIssuance->getAccountID(sfIssuer);
|
||||
// Issuer won't have mptoken, i.e. "the operation failed succcessfully"
|
||||
if (mptIssuer == account)
|
||||
return Unexpected(tesSUCCESS);
|
||||
if (mptIssuer == account) // Issuer won't have MPToken
|
||||
return {TokenDescriptor{.token = nullptr, .issuance = sleIssuance}};
|
||||
|
||||
auto const mptokenID = keylet::mptoken(mptID.key, account);
|
||||
auto const sleToken = view.read(mptokenID);
|
||||
|
||||
// if account has no MPToken, fail
|
||||
if (!sleToken)
|
||||
return Unexpected(tecNO_LINE);
|
||||
{
|
||||
auto const maybeDomainID = sleIssuance->at(~sfDomainID);
|
||||
if (!maybeDomainID)
|
||||
return Unexpected(tecNO_AUTH);
|
||||
|
||||
auto const slePD =
|
||||
view.read(keylet::permissionedDomain(*maybeDomainID));
|
||||
if (!slePD)
|
||||
return Unexpected(tecOBJECT_NOT_FOUND);
|
||||
|
||||
if (auto const err = credentials::valid(view, *maybeDomainID, account);
|
||||
!isTesSuccess(err))
|
||||
return Unexpected(err);
|
||||
|
||||
// No token but there are credentials matching DomainID, so a token
|
||||
// could be created & authorized if the required creds are not expired
|
||||
return {TokenDescriptor{.token = nullptr, .issuance = sleIssuance}};
|
||||
}
|
||||
|
||||
return {TokenDescriptor{.token = sleToken, .issuance = sleIssuance}};
|
||||
}
|
||||
@@ -2065,28 +2088,108 @@ requireAuth(
|
||||
AccountID const& account)
|
||||
{
|
||||
auto maybeToken = findToken(view, mptIssue, account);
|
||||
|
||||
// Whatever reason why we could not find
|
||||
if (!maybeToken)
|
||||
{
|
||||
// Convert tecNO_LINE to useful error
|
||||
if (maybeToken.error() == tecNO_LINE)
|
||||
return tecNO_AUTH;
|
||||
// Note, error() is tesSUCCESS if no authorization was needed
|
||||
return maybeToken.error();
|
||||
}
|
||||
else if (maybeToken->token == nullptr)
|
||||
{
|
||||
// Either account is issuer and no authorization is needed, or it has
|
||||
// credentials maching DomainID stored in MPT issuance.
|
||||
// Note: the credentials on which basis we return tesSUCCESS might have
|
||||
// expired; the user must call verifyAuth to check with ApplyView.
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
auto sleToken = maybeToken->token;
|
||||
auto sleIssuance = maybeToken->issuance;
|
||||
|
||||
// mptoken must be authorized if issuance enabled requireAuth
|
||||
if (sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth &&
|
||||
if ((sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth) &&
|
||||
!(sleToken->getFlags() & lsfMPTAuthorized))
|
||||
return tecNO_AUTH;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
verifyAuth(
|
||||
ApplyView& view,
|
||||
MPTIssue const& mptIssue,
|
||||
AccountID const& account,
|
||||
XRPAmount const& priorBalance, // for MPToken authorization
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const mptIssuanceID = mptIssue.getMptID();
|
||||
auto const sleIssuance = view.read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tefINTERNAL; // Should have called requireAuth earlier
|
||||
|
||||
if (account == sleIssuance->at(sfIssuer))
|
||||
return tesSUCCESS; // Won't create MPToken for the token issuer
|
||||
|
||||
auto const sleToken = view.read(keylet::mptoken(mptIssuanceID, account));
|
||||
bool const domainCheck =
|
||||
(sleToken && sleToken->getFlags() & lsfMPTDomainCheck);
|
||||
|
||||
bool authorizedByDomain = false;
|
||||
if (domainCheck || sleToken == nullptr)
|
||||
{
|
||||
// We check DomainID if:
|
||||
// 1. Token not found or
|
||||
// 2. Token found and has lsfMPTDomainCheck flag
|
||||
auto const maybeDomainID = sleIssuance->at(~sfDomainID);
|
||||
authorizedByDomain = maybeDomainID.has_value() &&
|
||||
verifyDomain(view, account, *maybeDomainID, j) == tesSUCCESS;
|
||||
}
|
||||
|
||||
if (authorizedByDomain && sleToken == nullptr) // && !domainCheck, implied
|
||||
{
|
||||
// Create MPToken with the lsfMPTDomainCheck flag set
|
||||
if (auto const err = MPTokenAuthorize::authorize(
|
||||
view,
|
||||
j,
|
||||
{
|
||||
.priorBalance = priorBalance,
|
||||
.mptIssuanceID = mptIssuanceID,
|
||||
.accountID = account,
|
||||
.flags = 0,
|
||||
});
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
auto sleMpt = view.peek(keylet::mptoken(mptIssuanceID, account));
|
||||
XRPL_ASSERT(sleMpt, "ripple::verifyAuth : found new MPToken");
|
||||
std::uint32_t const flags = sleMpt->getFieldU32(sfFlags);
|
||||
sleMpt->setFieldU32(sfFlags, flags | lsfMPTDomainCheck);
|
||||
view.update(sleMpt);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
else if (!authorizedByDomain && domainCheck) // && sleToken, implied
|
||||
{
|
||||
// We will only delete MPToken here if it has 0 balance
|
||||
[[maybe_unused]] auto _ = MPTokenAuthorize::authorize(
|
||||
view,
|
||||
j,
|
||||
{
|
||||
.priorBalance = priorBalance,
|
||||
.mptIssuanceID = mptIssuanceID,
|
||||
.accountID = account,
|
||||
.flags = tfMPTUnauthorize,
|
||||
});
|
||||
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else if (authorizedByDomain || sleToken)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
TER
|
||||
canTransfer(
|
||||
ReadView const& view,
|
||||
|
||||
Reference in New Issue
Block a user