Merge remote-tracking branch 'Transia-RnD-rippled/dangell7/token-issuance' into develop

# Conflicts:
#	include/xrpl/protocol/Protocol.h
#	include/xrpl/protocol/TER.h
#	include/xrpl/protocol/detail/ledger_entries.macro
#	src/libxrpl/protocol/TER.cpp
This commit is contained in:
Denis Angell
2026-09-13 19:26:58 -04:00
39 changed files with 2412 additions and 40 deletions

View File

@@ -338,6 +338,17 @@ adjustLoanBrokerOwnerCount(
[[nodiscard]] Rate
transferRate(ReadView const& view, AccountID const& issuer);
/**
* Returns the transfer fee charged for a specific currency of the issuer.
* A per-currency TransferFee on the currency's TokenIssuance overrides the
* account-wide TransferRate.
*/
[[nodiscard]] Rate
transferRate(ReadView const& view, AccountID const& issuer, Currency const& currency);
[[nodiscard]] Rate
transferRate(ReadView const& view, Issue const& issue);
/**
* Generate a pseudo-account address from a pseudo owner key.
* @param pseudoOwnerKey The key to generate the address from

View File

@@ -4,6 +4,7 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Issue.h>
@@ -391,7 +392,8 @@ directSendNoFee(
AccountID const& uReceiverID,
STAmount const& saAmount,
bool bCheckIssuer,
beast::Journal j);
beast::Journal j,
EnforceSupplyCap enforceSupplyCap = EnforceSupplyCap::Yes);
/**
* Calls static accountSendIOU if saAmount represents Issue.

View File

@@ -0,0 +1,109 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
#include <optional>
namespace xrpl {
/**
* ceil(value * 10^scale), exact.
*
* The shift is a pure exponent adjustment; the +1 for a fractional
* positive value is exact because a fractional Number is < 10^16.
* Truncation toward zero already is the ceiling for negative values.
*/
[[nodiscard]] Number
tokenScaledCeil(Number const& value, std::uint8_t scale);
/**
* floor(value * 10^scale) as integer base units; nullopt if it does not
* fit in a non-negative int64.
*/
[[nodiscard]] std::optional<std::int64_t>
tokenBaseUnits(Number const& value, std::uint8_t scale);
/**
* True if the issuance violates
* ceil(IssuedAmount * 10^TokenScale) + MPT OutstandingAmount > MaximumAmount.
* Always false for an uncapped issuance.
*/
[[nodiscard]] bool
tokenSupplyExceeded(ReadView const& view, SLE::const_ref sleIssuance);
/**
* Controls whether an IOU credit hard-fails when a capped TokenIssuance
* would exceed its MaximumAmount. Flow-engine steps pass No: their send
* results are advisory (return values ignored, reverse-pass execution can
* legitimately overshoot transiently) and the supply-cap invariant checker
* gates the final state instead.
*/
enum class EnforceSupplyCap : bool { No = false, Yes = true };
/**
* Maintain IssuedAmount on the issuer's TokenIssuance for a trust-line
* balance move of `amount` from `sender` to `receiver`. Only the amount's
* issuer is adjusted: balance moving away from the issuer increases its
* net issuance, balance returning decreases it.
*
* No-op when the amendment is disabled, neither party is the issuer, or no
* TokenIssuance exists. Returns tecSUPPLY_EXCEEDED when the cap is enforced
* and a capped issuer would exceed MaximumAmount.
*/
[[nodiscard]] TER
adjustTokenIssuance(
ApplyView& view,
AccountID const& sender,
AccountID const& receiver,
STAmount const& amount,
EnforceSupplyCap enforceCap,
beast::Journal j);
/**
* Remaining IOU the issuer can issue under the supply cap, floored to the
* representable amount; nullopt when there is no TokenIssuance or no cap.
*/
[[nodiscard]] std::optional<STAmount>
tokenIssuanceHeadroom(ReadView const& view, Issue const& issue);
/**
* True when the issue's TokenIssuance carries the per-currency lock
* (lsfTokenLocked). The per-currency analog of lsfGlobalFreeze.
*/
[[nodiscard]] bool
isTokenLocked(ReadView const& view, Issue const& issue);
/**
* For an MPT issuance bound to a capped TokenIssuance: the additional MPT
* base units that can be minted under the shared cap
* (MaximumAmount - OutstandingAmount - ceil(IssuedAmount * 10^TokenScale)).
* nullopt when unbound or uncapped.
*/
[[nodiscard]] std::optional<std::int64_t>
mptBoundHeadroom(ReadView const& view, SLE::const_ref sleMptIssuance);
/**
* Validate binding an MPTokenIssuance to a TokenIssuance: the MPT issuance
* exists, is issued by `account`, is not already bound, and its
* MaximumAmount/AssetScale equal the TokenIssuance's MaximumAmount/
* TokenScale.
*/
[[nodiscard]] TER
validateTokenBinding(
ReadView const& view,
MPTID const& mptId,
AccountID const& account,
std::optional<std::uint64_t> const& maximumAmount,
std::uint8_t tokenScale);
} // namespace xrpl

View File

@@ -348,6 +348,15 @@ mptokenIssuance(uint256 const& issuanceKey)
return {ltMPTOKEN_ISSUANCE, issuanceKey};
}
Keylet
tokenIssuance(AccountID const& issuer, Currency const& currency) noexcept;
inline Keylet
tokenIssuance(uint256 const& key)
{
return {ltTOKEN_ISSUANCE, key};
}
Keylet
mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept;

View File

@@ -199,7 +199,13 @@ enum LedgerEntryType : std::uint16_t {
\
LEDGER_OBJECT(Sponsorship, \
LSF_FLAG(lsfSponsorshipRequireSignForFee, 0x00010000) \
LSF_FLAG(lsfSponsorshipRequireSignForReserve, 0x00020000))
LSF_FLAG(lsfSponsorshipRequireSignForReserve, 0x00020000)) \
\
LEDGER_OBJECT(TokenIssuance, \
LSF_FLAG(lsfTokenLocked, 0x00000001) /* True, per-currency global freeze */ \
LSF_FLAG(lsfTokenCannotLock, 0x00000002) /* True, issuer renounced the per-currency lock */ \
LSF_FLAG(lsfTokenWrapped, 0x00000004) /* True, owned by a pseudo-account (blackholed issuer) */ \
LSF_FLAG(lsfTokenVerifiedSupply, 0x00000008)) /* True, IssuedAmount reflects verified legacy supply */
// clang-format on

View File

@@ -391,6 +391,20 @@ constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB
*/
constexpr std::size_t kWasmTransferLimit = 1 << 20; // 1MB
/**
* Maximum MaximumAmount of a TokenIssuance, in base units (10^15). Bounded
* to what Number/STAmount arithmetic represents exactly, so the supply-cap
* comparison can never be affected by mantissa rounding.
*/
constexpr std::uint64_t kMaxTokenIssuanceAmount = 1'000'000'000'000'000ull;
static_assert(kMaxTokenIssuanceAmount <= kMaxMpTokenAmount);
/**
* Maximum TokenScale of a TokenIssuance: 10^scale must stay in exact int64
* range. 10^19 > 2^63-1 > 10^18
*/
constexpr std::uint8_t kMaxTokenIssuanceScale = 18;
/**
* A ledger index.
*/

View File

@@ -378,6 +378,7 @@ enum TECcodes : TERUnderlyingType {
tecOUT_OF_GAS = 201,
tecBYTECODE_REJECTED = 202,
tecINVALID_PARAMETERS = 203,
tecSUPPLY_EXCEEDED = 204,
};
//------------------------------------------------------------------------------

View File

@@ -162,6 +162,16 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
TF_FLAG(tfMPTSetCanHoldConfidentialBalance, 0x00000100), \
MASK_ADJ(0)) \
\
TRANSACTION(TokenIssuanceCreate, \
TF_FLAG(tfTokenCannotLock, 0x00000001), \
MASK_ADJ(0)) \
\
TRANSACTION(TokenIssuanceSet, \
TF_FLAG2(tfTokenCannotLock, 0x00000001) \
TF_FLAG(tfTokenLock, 0x00000002) \
TF_FLAG(tfTokenUnlock, 0x00000004), \
MASK_ADJ(0)) \
\
TRANSACTION(NFTokenCreateOffer, \
TF_FLAG(tfSellNFToken, 0x00000001), \
MASK_ADJ(0)) \

View File

@@ -22,25 +22,27 @@ enum class Emittance { Emitable, NotEmitable };
* TxSettings::privileges) and enforced in InvariantCheck.cpp.
*/
enum class Privilege : std::uint16_t {
NoPriv = 0x0000, // The transaction can not do any of the enumerated operations
CreateAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object.
CreatePseudoAcct = 0x0002, // The transaction can create a pseudo account,
// which implies createAcct
MustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object
MayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT
// object, but does not have to
OverrideFreeze = 0x0010, // The transaction can override some freeze rules
ChangeNftCounts = 0x0020, // The transaction can mint or burn an NFT
CreateMptIssuance = 0x0040, // The transaction can create a new MPT issuance
DestroyMptIssuance = 0x0080, // The transaction can destroy an MPT issuance
MustAuthorizeMpt = 0x0100, // The transaction MUST create or delete an MPT
// object (except by issuer)
MayAuthorizeMpt = 0x0200, // The transaction MAY create or delete an MPT
// object (except by issuer)
MayDeleteMpt = 0x0400, // The transaction MAY delete an MPT object. May not create.
MustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault
MayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault
MayCreateMpt = 0x2000, // The transaction MAY create an MPT object, except for issuer.
NoPriv = 0x0000, // The transaction can not do any of the enumerated operations
CreateAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object.
CreatePseudoAcct = 0x0002, // The transaction can create a pseudo account,
// which implies createAcct
MustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object
MayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT
// object, but does not have to
OverrideFreeze = 0x0010, // The transaction can override some freeze rules
ChangeNftCounts = 0x0020, // The transaction can mint or burn an NFT
CreateMptIssuance = 0x0040, // The transaction can create a new MPT issuance
DestroyMptIssuance = 0x0080, // The transaction can destroy an MPT issuance
MustAuthorizeMpt = 0x0100, // The transaction MUST create or delete an MPT
// object (except by issuer)
MayAuthorizeMpt = 0x0200, // The transaction MAY create or delete an MPT
// object (except by issuer)
MayDeleteMpt = 0x0400, // The transaction MAY delete an MPT object. May not create.
MustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault
MayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault
MayCreateMpt = 0x2000, // The transaction MAY create an MPT object, except for issuer.
CreateTokenIssuance = 0x4000, // The transaction can create a new token issuance
DestroyTokenIssuance = 0x8000, // The transaction can destroy a token issuance
};
// The inner static_cast is not redundant: the underlying type is narrower than

View File

@@ -153,3 +153,4 @@ XRPL_FEATURE(TokenPaychan, Supported::Yes, VoteBehavior::DefaultN
XRPL_FEATURE(Quantum, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Passkey, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(MPTStructuredData, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(TokenIssuance, Supported::No, VoteBehavior::DefaultNo)

View File

@@ -412,6 +412,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
{sfLockedAmount, SoeOptional},
{sfMPTokenMetadata, SoeOptional},
{sfMPTokenSchema, SoeOptional},
{sfTokenIssuanceID, SoeOptional},
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
{sfDomainID, SoeOptional},
@@ -702,5 +703,22 @@ LEDGER_ENTRY(ltPASSKEY_LIST, 0x0091, PasskeyList, passkey_list, ({
{sfPasskeys, SoeRequired},
}))
/** A ledger object which describes an IOU token issuance.
\sa keylet::tokenIssuance
*/
LEDGER_ENTRY(ltTOKEN_ISSUANCE, 0x0092, TokenIssuance, token_issuance, ({
{sfIssuer, SoeRequired},
{sfCurrency, SoeRequired},
{sfMaximumAmount, SoeOptional},
{sfIssuedAmount, SoeDefault},
{sfTokenScale, SoeDefault},
{sfMPTokenIssuanceID, SoeOptional},
{sfTransferFee, SoeOptional},
{sfMPTokenMetadata, SoeOptional},
{sfOwnerNode, SoeRequired},
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE

View File

@@ -479,3 +479,7 @@ UNTYPED_SFIELD(sfPasskeySignature, OBJECT, 43, SField::kSmdDefault, SFi
UNTYPED_SFIELD(sfPasskey, OBJECT, 44)
UNTYPED_SFIELD(sfPasskeys, ARRAY, 36)
TYPED_SFIELD(sfMPTokenSchema, VL, 53)
TYPED_SFIELD(sfTokenScale, UINT8, 7)
TYPED_SFIELD(sfTokenIssuanceID, UINT256, 44)
TYPED_SFIELD(sfIssuedAmount, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfCurrency, CURRENCY, 3)

View File

@@ -1320,3 +1320,37 @@ TRANSACTION(ttPASSKEY_LIST_SET, 103, PasskeyListSet,
({
{sfPasskeys, SoeRequired},
}))
TRANSACTION(ttTOKEN_ISSUANCE_CREATE, 104, TokenIssuanceCreate,
({
.amendment = featureTokenIssuance,
.privileges = Privilege::CreateTokenIssuance,
}),
({
{sfCurrency, SoeRequired},
{sfMaximumAmount, SoeOptional},
{sfTokenScale, SoeOptional},
{sfMPTokenIssuanceID, SoeOptional},
{sfTransferFee, SoeOptional},
{sfMPTokenMetadata, SoeOptional},
}))
TRANSACTION(ttTOKEN_ISSUANCE_SET, 105, TokenIssuanceSet,
({.amendment = featureTokenIssuance}),
({
{sfCurrency, SoeRequired},
{sfMPTokenIssuanceID, SoeOptional},
{sfTransferFee, SoeOptional},
{sfMPTokenMetadata, SoeOptional},
}))
TRANSACTION(ttTOKEN_ISSUANCE_DESTROY, 106, TokenIssuanceDestroy,
({
.amendment = featureTokenIssuance,
.privileges = Privilege::DestroyTokenIssuance,
}),
({
{sfCurrency, SoeRequired},
}))
TRANSACTION(ttTOKEN_CONVERT, 107, TokenConvert,
({.amendment = featureTokenIssuance}),
({
{sfAmount, SoeRequired, SoeMptSupported},
}))

View File

@@ -16,6 +16,7 @@
#include <xrpl/tx/invariants/PermissionedDEXInvariant.h>
#include <xrpl/tx/invariants/PermissionedDomainInvariant.h>
#include <xrpl/tx/invariants/SponsorshipInvariant.h>
#include <xrpl/tx/invariants/TokenIssuanceInvariant.h>
#include <xrpl/tx/invariants/VaultInvariant.h>
#include <cstdint>
@@ -486,7 +487,8 @@ using InvariantChecks = std::tuple<
ValidMPTTransfer,
ObjectHasPseudoAccount,
SponsorshipOwnerCountsMatch,
SponsorshipAccountCountMatchesField>;
SponsorshipAccountCountMatchesField,
ValidTokenIssuance>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -0,0 +1,48 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <vector>
namespace xrpl {
/**
* Invariants for TokenIssuance objects:
*
* - created/deleted only by transactions carrying the matching privilege,
* and the privileged transactions create/delete exactly one
* - MaximumAmount, TokenScale, Issuer, and Currency never change; the
* MPT binding is write-once
* - a capped issuance never exceeds
* ceil(IssuedAmount * 10^TokenScale) + MPT OutstandingAmount
* <= MaximumAmount after application
* - deletion only with IssuedAmount == 0
*/
class ValidTokenIssuance
{
std::uint32_t created_ = 0;
std::uint32_t deleted_ = 0;
bool immutableChanged_ = false;
std::vector<SLE::const_pointer> after_;
std::vector<SLE::const_pointer> deletedIssuances_;
public:
void
visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after);
[[nodiscard]] bool
finalize(
STTx const& tx,
TER const result,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j);
};
} // namespace xrpl

View File

@@ -5,6 +5,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -33,6 +34,13 @@ checkFreeze(
}
}
// The per-currency lock behaves like a global freeze scoped to one
// currency of the issuer.
if (isTokenLocked(view, Issue{currency, dst}))
{
return terNO_LINE;
}
if (auto sle = view.read(keylet::trustLine(src, dst, currency)))
{
if (sle->isFlag((dst > src) ? lsfHighFreeze : lsfLowFreeze))

View File

@@ -0,0 +1,47 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class TokenConvert : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit TokenConvert(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) override
{
}
[[nodiscard]] bool
finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
{
return true;
}
};
} // namespace xrpl

View File

@@ -0,0 +1,52 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
namespace xrpl {
class TokenIssuanceCreate : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit TokenIssuanceCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) override
{
}
[[nodiscard]] bool
finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
{
return true;
}
};
} // namespace xrpl

View File

@@ -0,0 +1,44 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class TokenIssuanceDestroy : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit TokenIssuanceDestroy(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) override
{
}
[[nodiscard]] bool
finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
{
return true;
}
};
} // namespace xrpl

View File

@@ -0,0 +1,52 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
namespace xrpl {
class TokenIssuanceSet : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit TokenIssuanceSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) override
{
}
[[nodiscard]] bool
finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
{
return true;
}
};
} // namespace xrpl