mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
109
include/xrpl/ledger/helpers/TokenIssuanceHelpers.h
Normal file
109
include/xrpl/ledger/helpers/TokenIssuanceHelpers.h
Normal 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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -378,6 +378,7 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecOUT_OF_GAS = 201,
|
||||
tecBYTECODE_REJECTED = 202,
|
||||
tecINVALID_PARAMETERS = 203,
|
||||
tecSUPPLY_EXCEEDED = 204,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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)) \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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},
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
|
||||
48
include/xrpl/tx/invariants/TokenIssuanceInvariant.h
Normal file
48
include/xrpl/tx/invariants/TokenIssuanceInvariant.h
Normal 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
|
||||
@@ -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))
|
||||
|
||||
47
include/xrpl/tx/transactors/token/TokenConvert.h
Normal file
47
include/xrpl/tx/transactors/token/TokenConvert.h
Normal 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
|
||||
52
include/xrpl/tx/transactors/token/TokenIssuanceCreate.h
Normal file
52
include/xrpl/tx/transactors/token/TokenIssuanceCreate.h
Normal 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
|
||||
44
include/xrpl/tx/transactors/token/TokenIssuanceDestroy.h
Normal file
44
include/xrpl/tx/transactors/token/TokenIssuanceDestroy.h
Normal 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
|
||||
52
include/xrpl/tx/transactors/token/TokenIssuanceSet.h
Normal file
52
include/xrpl/tx/transactors/token/TokenIssuanceSet.h
Normal 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
|
||||
@@ -308,6 +308,28 @@ transferRate(ReadView const& view, AccountID const& issuer)
|
||||
return kParityRate;
|
||||
}
|
||||
|
||||
Rate
|
||||
transferRate(ReadView const& view, AccountID const& issuer, Currency const& currency)
|
||||
{
|
||||
// A per-currency TransferFee on the TokenIssuance overrides the
|
||||
// account-wide TransferRate. Fee is in the XLS-33 format
|
||||
// (0..50000 = 0%..50%).
|
||||
if (view.rules().enabled(featureTokenIssuance))
|
||||
{
|
||||
if (auto const sle = view.read(keylet::tokenIssuance(issuer, currency));
|
||||
sle && sle->isFieldPresent(sfTransferFee))
|
||||
return Rate{1'000'000'000u + 10'000u * sle->getFieldU16(sfTransferFee)};
|
||||
}
|
||||
|
||||
return transferRate(view, issuer);
|
||||
}
|
||||
|
||||
Rate
|
||||
transferRate(ReadView const& view, Issue const& issue)
|
||||
{
|
||||
return transferRate(view, issue.account, issue.currency);
|
||||
}
|
||||
|
||||
void
|
||||
increaseOwnerCount(
|
||||
ApplyView& view,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/AmountConversions.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -136,6 +137,8 @@ isFrozen(
|
||||
auto sle = view.read(keylet::account(issuer));
|
||||
if (sle && sle->isFlag(lsfGlobalFreeze))
|
||||
return true;
|
||||
if (isTokenLocked(view, Issue{currency, issuer}))
|
||||
return true;
|
||||
if (issuer != account)
|
||||
{
|
||||
// Check if the issuer froze the line
|
||||
@@ -414,6 +417,11 @@ issueIOU(
|
||||
|
||||
JLOG(j.trace()) << "issueIOU: " << to_string(account) << ": " << amount.getFullText();
|
||||
|
||||
if (auto const ter =
|
||||
adjustTokenIssuance(view, issue.account, account, amount, EnforceSupplyCap::Yes, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
bool const bSenderHigh = issue.account > account;
|
||||
|
||||
auto const index = keylet::trustLine(issue.account, account, issue.currency);
|
||||
@@ -509,6 +517,11 @@ redeemIOU(
|
||||
|
||||
JLOG(j.trace()) << "redeemIOU: " << to_string(account) << ": " << amount.getFullText();
|
||||
|
||||
if (auto const ter =
|
||||
adjustTokenIssuance(view, account, issue.account, amount, EnforceSupplyCap::Yes, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
bool const bSenderHigh = account > issue.account;
|
||||
|
||||
if (auto state = view.peek(keylet::trustLine(account, issue.account, issue.currency)))
|
||||
|
||||
@@ -226,6 +226,7 @@ isLedgerEntryOwner(ReadView const& view, SLE const& sle, AccountID const& accoun
|
||||
case ltDEPOSIT_PREAUTH:
|
||||
return sle.getAccountID(sfAccount) == account;
|
||||
case ltMPTOKEN_ISSUANCE:
|
||||
case ltTOKEN_ISSUANCE:
|
||||
return sle.getAccountID(sfIssuer) == account;
|
||||
case ltSIGNER_LIST: {
|
||||
auto const signerList = view.read(keylet::signerList(account));
|
||||
@@ -275,6 +276,7 @@ isLedgerEntrySupportedBySponsorship(SLE const& sle)
|
||||
case ltDELEGATE:
|
||||
case ltDEPOSIT_PREAUTH:
|
||||
case ltMPTOKEN_ISSUANCE:
|
||||
case ltTOKEN_ISSUANCE:
|
||||
case ltSIGNER_LIST:
|
||||
case ltCREDENTIAL:
|
||||
case ltRIPPLE_STATE:
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
@@ -47,7 +48,9 @@ bool
|
||||
isGlobalFrozen(ReadView const& view, Asset const& asset)
|
||||
{
|
||||
return asset.visit(
|
||||
[&](Issue const& issue) { return isGlobalFrozen(view, issue.getIssuer()); },
|
||||
[&](Issue const& issue) {
|
||||
return isGlobalFrozen(view, issue.getIssuer()) || isTokenLocked(view, issue);
|
||||
},
|
||||
[&](MPTIssue const& issue) { return isGlobalFrozen(view, issue); });
|
||||
}
|
||||
|
||||
@@ -430,7 +433,10 @@ accountHolds(
|
||||
{
|
||||
return amount;
|
||||
}
|
||||
auto const available = availableMPTAmount(*issuance);
|
||||
auto available = availableMPTAmount(*issuance);
|
||||
// A bound issuance shares its cap with the IOU side.
|
||||
if (auto const headroom = mptBoundHeadroom(view, issuance))
|
||||
available = std::min(available, *headroom);
|
||||
if (!mptokensV2)
|
||||
return STAmount{mptIssue, available};
|
||||
return view.balanceHookMPT(issuer, mptIssue, available);
|
||||
@@ -504,6 +510,11 @@ accountFunds(
|
||||
{
|
||||
XRPL_ASSERT(saDefault.holds<Issue>(), "xrpl::accountFunds: saDefault holds Issue");
|
||||
|
||||
// Note: a capped issuer's funds are NOT clamped to the supply headroom
|
||||
// here. Funds readings must stay stable across a transaction (deferred
|
||||
// credits) or partially-crossed issuer offers read as never-funded and
|
||||
// are removed; the supply cap is enforced at the credit choke point
|
||||
// instead, which fails an over-cap crossing outright.
|
||||
if (!saDefault.native() && saDefault.getIssuer() == id)
|
||||
return saDefault;
|
||||
|
||||
@@ -538,7 +549,7 @@ Rate
|
||||
transferRate(ReadView const& view, Asset const& asset)
|
||||
{
|
||||
return asset.visit(
|
||||
[&](Issue const& issue) { return transferRate(view, issue.getIssuer()); },
|
||||
[&](Issue const& issue) { return transferRate(view, issue); },
|
||||
[&](MPTIssue const& issue) { return transferRate(view, issue.getMptID()); });
|
||||
}
|
||||
|
||||
@@ -688,7 +699,8 @@ directSendNoFeeIOU(
|
||||
STAmount const& saAmount,
|
||||
bool bCheckIssuer,
|
||||
SLE::ref sponsorSle,
|
||||
beast::Journal j)
|
||||
beast::Journal j,
|
||||
EnforceSupplyCap enforceSupplyCap = EnforceSupplyCap::Yes)
|
||||
{
|
||||
AccountID const& issuer = saAmount.getIssuer();
|
||||
Currency const& currency = saAmount.get<Issue>().currency;
|
||||
@@ -712,6 +724,12 @@ directSendNoFeeIOU(
|
||||
!isXRP(uReceiverID) && uReceiverID != noAccount(),
|
||||
"xrpl::directSendNoFeeIOU : receiver is not XRP");
|
||||
|
||||
// Supply accounting: every trust-line balance move funnels through here.
|
||||
if (auto const ter =
|
||||
adjustTokenIssuance(view, uSenderID, uReceiverID, saAmount, enforceSupplyCap, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the line exists, modify it accordingly.
|
||||
if (auto const sleRippleState = view.peek(index))
|
||||
{
|
||||
@@ -866,13 +884,29 @@ directSendNoLimitIOU(
|
||||
|
||||
// Calculate the amount to transfer accounting
|
||||
// for any transfer fees if the fee is not waived:
|
||||
saActual = (waiveFee == WaiveTransferFee::Yes) ? saAmount
|
||||
: multiply(saAmount, transferRate(view, issuer));
|
||||
saActual = (waiveFee == WaiveTransferFee::Yes)
|
||||
? saAmount
|
||||
: multiply(saAmount, transferRate(view, saAmount.get<Issue>()));
|
||||
|
||||
JLOG(j.debug()) << "directSendNoLimitIOU> " << to_string(uSenderID) << " - > "
|
||||
<< to_string(uReceiverID) << " : deliver=" << saAmount.getFullText()
|
||||
<< " cost=" << saActual.getFullText();
|
||||
|
||||
if (view.rules().enabled(featureTokenIssuance))
|
||||
{
|
||||
// Redeem from the sender before issuing to the receiver so a capped
|
||||
// currency never transiently exceeds its supply cap in transit.
|
||||
TER terResult = directSendNoFeeIOU(view, uSenderID, issuer, saActual, true, sponsorSle, j);
|
||||
|
||||
if (tesSUCCESS == terResult)
|
||||
{
|
||||
terResult =
|
||||
directSendNoFeeIOU(view, issuer, uReceiverID, saAmount, true, sponsorSle, j);
|
||||
}
|
||||
|
||||
return terResult;
|
||||
}
|
||||
|
||||
TER terResult = directSendNoFeeIOU(view, issuer, uReceiverID, saAmount, true, sponsorSle, j);
|
||||
|
||||
if (tesSUCCESS == terResult)
|
||||
@@ -938,7 +972,7 @@ directSendNoLimitMultiIOU(
|
||||
// for any transfer fees if the fee is not waived:
|
||||
STAmount const actualSend = (waiveFee == WaiveTransferFee::Yes)
|
||||
? amount
|
||||
: multiply(amount, transferRate(view, issuer));
|
||||
: multiply(amount, transferRate(view, amount.get<Issue>()));
|
||||
actual += actualSend;
|
||||
takeFromSender += actualSend;
|
||||
|
||||
@@ -1233,6 +1267,9 @@ directSendNoFeeMPT(
|
||||
if (isMPTOverflow(amt, outstanding, maxAmount, AllowMPTOverflow::Yes))
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
// A bound issuance shares its cap with the IOU side.
|
||||
if (auto const headroom = mptBoundHeadroom(view, sleIssuance); headroom && amt > *headroom)
|
||||
return tecSUPPLY_EXCEEDED;
|
||||
(*sleIssuance)[sfOutstandingAmount] += amt;
|
||||
view.update(sleIssuance);
|
||||
}
|
||||
@@ -1525,11 +1562,13 @@ directSendNoFee(
|
||||
AccountID const& uReceiverID,
|
||||
STAmount const& saAmount,
|
||||
bool bCheckIssuer,
|
||||
beast::Journal j)
|
||||
beast::Journal j,
|
||||
EnforceSupplyCap enforceSupplyCap)
|
||||
{
|
||||
return saAmount.asset().visit(
|
||||
[&](Issue const&) {
|
||||
return directSendNoFeeIOU(view, uSenderID, uReceiverID, saAmount, bCheckIssuer, {}, j);
|
||||
return directSendNoFeeIOU(
|
||||
view, uSenderID, uReceiverID, saAmount, bCheckIssuer, {}, j, enforceSupplyCap);
|
||||
},
|
||||
[&](MPTIssue const&) {
|
||||
XRPL_ASSERT(!bCheckIssuer, "xrpl::directSendNoFee : not checking issuer");
|
||||
|
||||
221
src/libxrpl/ledger/helpers/TokenIssuanceHelpers.cpp
Normal file
221
src/libxrpl/ledger/helpers/TokenIssuanceHelpers.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
Number
|
||||
tokenScaledCeil(Number const& value, std::uint8_t scale)
|
||||
{
|
||||
Number const shifted(value.mantissa(), value.exponent() + scale);
|
||||
Number const trunc = shifted.truncate();
|
||||
if (trunc < shifted)
|
||||
return trunc + Number(1);
|
||||
return trunc;
|
||||
}
|
||||
|
||||
std::optional<std::int64_t>
|
||||
tokenBaseUnits(Number const& value, std::uint8_t scale)
|
||||
{
|
||||
if (value < Number{})
|
||||
return std::nullopt;
|
||||
Number const shifted(value.mantissa(), value.exponent() + scale);
|
||||
// 10^18 is exactly representable; anything above it is far beyond any
|
||||
// legal MaximumAmount.
|
||||
if (shifted > Number(1'000'000'000'000'000'000LL))
|
||||
return std::nullopt;
|
||||
NumberRoundModeGuard const guard(Number::RoundingMode::TowardsZero);
|
||||
return static_cast<std::int64_t>(shifted);
|
||||
}
|
||||
|
||||
static bool
|
||||
supplyWouldExceed(ReadView const& view, SLE::const_ref sleIssuance, Number const& issued)
|
||||
{
|
||||
auto const maximum = sleIssuance->at(~sfMaximumAmount);
|
||||
if (!maximum)
|
||||
return false;
|
||||
|
||||
std::uint64_t mptOutstanding = 0;
|
||||
if (auto const mptId = sleIssuance->at(~sfMPTokenIssuanceID))
|
||||
{
|
||||
if (auto const sleMpt = view.read(keylet::mptokenIssuance(*mptId)))
|
||||
mptOutstanding = sleMpt->at(sfOutstandingAmount);
|
||||
}
|
||||
if (mptOutstanding > *maximum)
|
||||
return true;
|
||||
|
||||
Number const iouUnits = tokenScaledCeil(issued, sleIssuance->at(sfTokenScale));
|
||||
return iouUnits > Number(static_cast<std::int64_t>(*maximum - mptOutstanding));
|
||||
}
|
||||
|
||||
bool
|
||||
tokenSupplyExceeded(ReadView const& view, SLE::const_ref sleIssuance)
|
||||
{
|
||||
return supplyWouldExceed(view, sleIssuance, sleIssuance->at(sfIssuedAmount));
|
||||
}
|
||||
|
||||
TER
|
||||
adjustTokenIssuance(
|
||||
ApplyView& view,
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
STAmount const& amount,
|
||||
EnforceSupplyCap enforceCap,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (!view.rules().enabled(featureTokenIssuance))
|
||||
return tesSUCCESS;
|
||||
|
||||
if (!amount.holds<Issue>() || amount.native() || amount == beast::kZero)
|
||||
return tesSUCCESS;
|
||||
|
||||
if (amount.negative())
|
||||
return adjustTokenIssuance(view, receiver, sender, -amount, enforceCap, j);
|
||||
|
||||
// Only the amount's issuer has an obligation in this move. Currency
|
||||
// codes are not unique across issuers, so the counterparty's issuance
|
||||
// of the same code must never be touched.
|
||||
AccountID const& issuer = amount.getIssuer();
|
||||
bool const increasing = issuer == sender;
|
||||
if (!increasing && issuer != receiver)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const sle = view.peek(keylet::tokenIssuance(issuer, amount.get<Issue>().currency));
|
||||
if (!sle)
|
||||
return tesSUCCESS;
|
||||
|
||||
Number const delta = amount;
|
||||
Number const issued = sle->at(sfIssuedAmount);
|
||||
Number const next = increasing ? issued + delta : issued - delta;
|
||||
|
||||
if (increasing && enforceCap == EnforceSupplyCap::Yes && supplyWouldExceed(view, sle, next))
|
||||
{
|
||||
JLOG(j.trace()) << "adjustTokenIssuance: supply cap exceeded for " << issuer;
|
||||
return tecSUPPLY_EXCEEDED;
|
||||
}
|
||||
|
||||
sle->at(sfIssuedAmount) = next;
|
||||
associateAsset(*sle, amount.asset());
|
||||
view.update(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
std::optional<STAmount>
|
||||
tokenIssuanceHeadroom(ReadView const& view, Issue const& issue)
|
||||
{
|
||||
if (!view.rules().enabled(featureTokenIssuance))
|
||||
return std::nullopt;
|
||||
|
||||
auto const sle = view.read(keylet::tokenIssuance(issue.account, issue.currency));
|
||||
if (!sle)
|
||||
return std::nullopt;
|
||||
|
||||
auto const maximum = sle->at(~sfMaximumAmount);
|
||||
if (!maximum)
|
||||
return std::nullopt;
|
||||
|
||||
std::uint8_t const scale = sle->at(sfTokenScale);
|
||||
|
||||
std::uint64_t mptOutstanding = 0;
|
||||
if (auto const mptId = sle->at(~sfMPTokenIssuanceID))
|
||||
{
|
||||
if (auto const sleMpt = view.read(keylet::mptokenIssuance(*mptId)))
|
||||
mptOutstanding = sleMpt->at(sfOutstandingAmount);
|
||||
}
|
||||
if (mptOutstanding > *maximum)
|
||||
return STAmount{issue};
|
||||
|
||||
Number const capUnits(static_cast<std::int64_t>(*maximum - mptOutstanding));
|
||||
Number const iouUnits = tokenScaledCeil(sle->at(sfIssuedAmount), scale);
|
||||
if (iouUnits >= capUnits)
|
||||
return STAmount{issue};
|
||||
|
||||
Number const freeUnits = capUnits - iouUnits;
|
||||
return STAmount{issue, Number(freeUnits.mantissa(), freeUnits.exponent() - scale)};
|
||||
}
|
||||
|
||||
bool
|
||||
isTokenLocked(ReadView const& view, Issue const& issue)
|
||||
{
|
||||
if (!view.rules().enabled(featureTokenIssuance))
|
||||
return false;
|
||||
|
||||
auto const sle = view.read(keylet::tokenIssuance(issue.account, issue.currency));
|
||||
return sle && sle->isFlag(lsfTokenLocked);
|
||||
}
|
||||
|
||||
std::optional<std::int64_t>
|
||||
mptBoundHeadroom(ReadView const& view, SLE::const_ref sleMptIssuance)
|
||||
{
|
||||
if (!view.rules().enabled(featureTokenIssuance))
|
||||
return std::nullopt;
|
||||
|
||||
auto const issuanceKey = sleMptIssuance->at(~sfTokenIssuanceID);
|
||||
if (!issuanceKey)
|
||||
return std::nullopt;
|
||||
|
||||
auto const sleTI = view.read(keylet::tokenIssuance(*issuanceKey));
|
||||
if (!sleTI)
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
auto const maximum = sleTI->at(~sfMaximumAmount);
|
||||
if (!maximum)
|
||||
return std::nullopt;
|
||||
|
||||
std::uint64_t const outstanding = sleMptIssuance->at(sfOutstandingAmount);
|
||||
if (outstanding >= *maximum)
|
||||
return 0;
|
||||
|
||||
Number const room(static_cast<std::int64_t>(*maximum - outstanding));
|
||||
Number const iouUnits = tokenScaledCeil(sleTI->at(sfIssuedAmount), sleTI->at(sfTokenScale));
|
||||
if (iouUnits >= room)
|
||||
return 0;
|
||||
|
||||
return tokenBaseUnits(room - iouUnits, 0).value_or(0);
|
||||
}
|
||||
|
||||
TER
|
||||
validateTokenBinding(
|
||||
ReadView const& view,
|
||||
MPTID const& mptId,
|
||||
AccountID const& account,
|
||||
std::optional<std::uint64_t> const& maximumAmount,
|
||||
std::uint8_t tokenScale)
|
||||
{
|
||||
auto const sleMpt = view.read(keylet::mptokenIssuance(mptId));
|
||||
if (!sleMpt)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (sleMpt->at(sfIssuer) != account)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// The binding is one-to-one: an MPT issuance already bound to a
|
||||
// TokenIssuance cannot be bound again.
|
||||
if (sleMpt->isFieldPresent(sfTokenIssuanceID))
|
||||
return tecDUPLICATE;
|
||||
|
||||
// One cap, two views of it: equal maxima in the same base units.
|
||||
if (sleMpt->at(~sfMaximumAmount) != maximumAmount)
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
if (sleMpt->at(sfAssetScale) != tokenScale)
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -105,6 +105,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
Loan = 'L',
|
||||
PasskeyList = 'k',
|
||||
Sponsorship = '>',
|
||||
TokenIssuance = 'F',
|
||||
|
||||
ContractSource = 'Z',
|
||||
Contract = 'c',
|
||||
@@ -564,6 +565,12 @@ mptokenIssuance(MPTID const& issuanceID) noexcept
|
||||
return {ltMPTOKEN_ISSUANCE, indexHash(LedgerNameSpace::MPTokenIssuance, issuanceID)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
tokenIssuance(AccountID const& issuer, Currency const& currency) noexcept
|
||||
{
|
||||
return {ltTOKEN_ISSUANCE, indexHash(LedgerNameSpace::TokenIssuance, issuer, currency)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept
|
||||
{
|
||||
|
||||
@@ -111,6 +111,7 @@ transResults()
|
||||
MAKE_ERROR(tecOUT_OF_GAS, "The WASM code ran out of gas during execution."),
|
||||
MAKE_ERROR(tecBYTECODE_REJECTED, "The custom WASM code that was run rejected your transaction."),
|
||||
MAKE_ERROR(tecINVALID_PARAMETERS, "Contract parameters do not match the expected ABI."),
|
||||
MAKE_ERROR(tecSUPPLY_EXCEEDED, "The transaction would exceed the token's maximum supply."),
|
||||
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
|
||||
129
src/libxrpl/tx/invariants/TokenIssuanceInvariant.cpp
Normal file
129
src/libxrpl/tx/invariants/TokenIssuanceInvariant.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include <xrpl/tx/invariants/TokenIssuanceInvariant.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/STCurrency.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidTokenIssuance::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
auto const type = before ? before->getType() : after ? after->getType() : ltANY;
|
||||
if (type != ltTOKEN_ISSUANCE)
|
||||
return;
|
||||
|
||||
if (!before && after)
|
||||
{
|
||||
++created_;
|
||||
after_.push_back(after);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDelete)
|
||||
{
|
||||
++deleted_;
|
||||
deletedIssuances_.push_back(before);
|
||||
return;
|
||||
}
|
||||
|
||||
if (before && after)
|
||||
{
|
||||
if (before->at(~sfMaximumAmount) != after->at(~sfMaximumAmount) ||
|
||||
before->at(sfTokenScale) != after->at(sfTokenScale) ||
|
||||
before->at(sfIssuer) != after->at(sfIssuer) ||
|
||||
before->at(sfCurrency) != after->at(sfCurrency) ||
|
||||
(before->isFieldPresent(sfMPTokenIssuanceID) &&
|
||||
before->at(~sfMPTokenIssuanceID) != after->at(~sfMPTokenIssuanceID)))
|
||||
immutableChanged_ = true;
|
||||
|
||||
after_.push_back(after);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ValidTokenIssuance::finalize(
|
||||
STTx const& tx,
|
||||
TER const result,
|
||||
XRPAmount const,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
// No TokenIssuance can exist before the amendment, so any activity at
|
||||
// all pre-amendment is a hard failure.
|
||||
if (!view.rules().enabled(featureTokenIssuance) &&
|
||||
(created_ != 0 || deleted_ != 0 || !after_.empty() || !deletedIssuances_.empty()))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TokenIssuance changed without amendment";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The destructive checks run regardless of the transaction result: a
|
||||
// failed transaction must not have created, deleted, or mutated
|
||||
// anything (and carries no privileges).
|
||||
if (created_ != 0 && !hasPrivilege(tx, Privilege::CreateTokenIssuance))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TokenIssuance created without privilege";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deleted_ != 0 && !hasPrivilege(tx, Privilege::DestroyTokenIssuance))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TokenIssuance deleted without privilege";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (immutableChanged_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TokenIssuance immutable field changed";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto const& sle : deletedIssuances_)
|
||||
{
|
||||
if (Number const issued = sle->at(sfIssuedAmount); issued != Number{})
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TokenIssuance deleted with "
|
||||
"outstanding IssuedAmount";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& sle : after_)
|
||||
{
|
||||
if (tokenSupplyExceeded(view, sle))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TokenIssuance supply cap exceeded";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTesSuccess(result))
|
||||
return true;
|
||||
|
||||
// The privileged transactions must do exactly what they claim.
|
||||
if (hasPrivilege(tx, Privilege::CreateTokenIssuance) && (created_ != 1 || deleted_ != 0))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TokenIssuanceCreate succeeded "
|
||||
"without creating exactly one issuance";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasPrivilege(tx, Privilege::DestroyTokenIssuance) && (deleted_ != 1 || created_ != 0))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TokenIssuanceDestroy succeeded "
|
||||
"without deleting exactly one issuance";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
@@ -1482,7 +1483,7 @@ BookStep<TIn, TOut, TDerived>::rate(
|
||||
[&](Issue const& issue) -> Rate {
|
||||
if (isXRP(issue.account) || issue.account == dstAccount)
|
||||
return kParityRate;
|
||||
return transferRate(view, issue.account);
|
||||
return transferRate(view, issue);
|
||||
},
|
||||
[&](MPTIssue const& mptIssue) -> Rate {
|
||||
// For MPT, parity applies only when this asset is the final strand
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/AmountConversions.h>
|
||||
#include <xrpl/protocol/IOUAmount.h>
|
||||
@@ -481,11 +482,26 @@ DirectStepI<TDerived>::maxPaymentFlow(ReadView const& sb) const
|
||||
auto const srcOwed = toAmount<IOUAmount>(
|
||||
accountHolds(sb, src_, currency_, dst_, FreezeHandling::IgnoreFreeze, j_));
|
||||
|
||||
// A capped issuer at the head of a strand expands supply, so the
|
||||
// headroom bounds the flow. Mid-strand the issuer only ripples
|
||||
// (redeem-then-reissue, sum-neutral), which the cap never blocks.
|
||||
std::optional<STAmount> headroom;
|
||||
if (prevStep_ == nullptr)
|
||||
headroom = tokenIssuanceHeadroom(sb, Issue{currency_, src_});
|
||||
|
||||
if (srcOwed.signum() > 0)
|
||||
return {srcOwed, DebtDirection::Redeems};
|
||||
{
|
||||
auto flow = srcOwed;
|
||||
if (headroom)
|
||||
flow = std::min(flow, toAmount<IOUAmount>(*headroom));
|
||||
return {flow, DebtDirection::Redeems};
|
||||
}
|
||||
|
||||
// srcOwed is negative or zero
|
||||
return {creditLimit2(sb, dst_, src_, currency_) + srcOwed, DebtDirection::Issues};
|
||||
auto flow = creditLimit2(sb, dst_, src_, currency_) + srcOwed;
|
||||
if (headroom)
|
||||
flow = std::min(flow, toAmount<IOUAmount>(*headroom));
|
||||
return {flow, DebtDirection::Issues};
|
||||
}
|
||||
|
||||
template <class TDerived>
|
||||
@@ -543,7 +559,8 @@ DirectStepI<TDerived>::revImp(
|
||||
dst_,
|
||||
toSTAmount(srcToDst, srcToDstIss),
|
||||
/*checkIssuer*/ true,
|
||||
j_);
|
||||
j_,
|
||||
EnforceSupplyCap::No);
|
||||
JLOG(j_.trace()) << "DirectStepI::rev: Non-limiting"
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
|
||||
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
|
||||
@@ -560,7 +577,8 @@ DirectStepI<TDerived>::revImp(
|
||||
dst_,
|
||||
toSTAmount(maxSrcToDst, srcToDstIss),
|
||||
/*checkIssuer*/ true,
|
||||
j_);
|
||||
j_,
|
||||
EnforceSupplyCap::No);
|
||||
JLOG(j_.trace()) << "DirectStepI::rev: Limiting"
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
|
||||
<< " srcToDst: " << to_string(maxSrcToDst) << " out: " << to_string(out);
|
||||
@@ -656,7 +674,8 @@ DirectStepI<TDerived>::fwdImp(
|
||||
dst_,
|
||||
toSTAmount(cache_->srcToDst, srcToDstIss),
|
||||
/*checkIssuer*/ true,
|
||||
j_);
|
||||
j_,
|
||||
EnforceSupplyCap::No);
|
||||
JLOG(j_.trace()) << "DirectStepI::fwd: Non-limiting"
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
|
||||
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
|
||||
@@ -673,7 +692,8 @@ DirectStepI<TDerived>::fwdImp(
|
||||
dst_,
|
||||
toSTAmount(cache_->srcToDst, srcToDstIss),
|
||||
/*checkIssuer*/ true,
|
||||
j_);
|
||||
j_,
|
||||
EnforceSupplyCap::No);
|
||||
JLOG(j_.trace()) << "DirectStepI::rev: Limiting"
|
||||
<< " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(actualIn)
|
||||
<< " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
|
||||
@@ -763,7 +783,7 @@ DirectStepI<TDerived>::qualitiesSrcIssues(ReadView const& sb, DebtDirection prev
|
||||
"issue");
|
||||
|
||||
std::uint32_t const srcQOut =
|
||||
redeems(prevStepDebtDirection) ? transferRate(sb, src_).value : QUALITY_ONE;
|
||||
redeems(prevStepDebtDirection) ? transferRate(sb, src_, currency_).value : QUALITY_ONE;
|
||||
auto dstQIn = static_cast<TDerived const*>(this)->quality(sb, QualityDirection::In);
|
||||
|
||||
if (isLast_ && dstQIn > QUALITY_ONE)
|
||||
|
||||
@@ -36,6 +36,11 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx)
|
||||
if ((*sleMPT)[~sfLockedAmount].value_or(0) != 0)
|
||||
return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE
|
||||
|
||||
// An issuance bound to a TokenIssuance must outlive the binding;
|
||||
// destroy the TokenIssuance first.
|
||||
if (sleMPT->isFieldPresent(sfTokenIssuanceID))
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
222
src/libxrpl/tx/transactors/token/TokenConvert.cpp
Normal file
222
src/libxrpl/tx/transactors/token/TokenConvert.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include <xrpl/tx/transactors/token/TokenConvert.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STCurrency.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
struct ConvertInfo
|
||||
{
|
||||
bool iouToMpt = false;
|
||||
Issue issue;
|
||||
MPTIssue mptIssue;
|
||||
std::int64_t units = 0; // MPT base units minted or burned
|
||||
STAmount iouAmount; // exact IOU amount debited or credited
|
||||
};
|
||||
|
||||
std::expected<ConvertInfo, TER>
|
||||
checkConvert(ReadView const& view, STTx const& tx, beast::Journal j)
|
||||
{
|
||||
AccountID const account = tx[sfAccount];
|
||||
STAmount const amount{tx[sfAmount]};
|
||||
|
||||
SLE::const_pointer sleIssuance;
|
||||
ConvertInfo info;
|
||||
info.iouToMpt = amount.holds<Issue>();
|
||||
|
||||
if (info.iouToMpt)
|
||||
{
|
||||
info.issue = amount.get<Issue>();
|
||||
sleIssuance = view.read(keylet::tokenIssuance(info.issue.account, info.issue.currency));
|
||||
if (!sleIssuance)
|
||||
return std::unexpected(tecOBJECT_NOT_FOUND);
|
||||
|
||||
auto const mptId = sleIssuance->at(~sfMPTokenIssuanceID);
|
||||
if (!mptId)
|
||||
return std::unexpected(tecNO_PERMISSION);
|
||||
info.mptIssue = MPTIssue{*mptId};
|
||||
}
|
||||
else
|
||||
{
|
||||
info.mptIssue = amount.get<MPTIssue>();
|
||||
auto const sleMpt = view.read(keylet::mptokenIssuance(info.mptIssue.getMptID()));
|
||||
if (!sleMpt)
|
||||
return std::unexpected(tecOBJECT_NOT_FOUND);
|
||||
|
||||
auto const issuanceKey = sleMpt->at(~sfTokenIssuanceID);
|
||||
if (!issuanceKey)
|
||||
return std::unexpected(tecNO_PERMISSION);
|
||||
|
||||
sleIssuance = view.read(keylet::tokenIssuance(*issuanceKey));
|
||||
if (!sleIssuance)
|
||||
return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
|
||||
info.issue = Issue{sleIssuance->at(sfCurrency), sleIssuance->at(sfIssuer)};
|
||||
}
|
||||
|
||||
if (sleIssuance->isFlag(lsfTokenLocked))
|
||||
return std::unexpected(tecLOCKED);
|
||||
|
||||
std::uint8_t const scale = sleIssuance->at(sfTokenScale);
|
||||
|
||||
// Both representations are touched in both directions, so neither side
|
||||
// may be frozen or locked.
|
||||
if (isFrozen(view, account, info.issue) || isDeepFrozen(view, account, info.issue))
|
||||
return std::unexpected(tecFROZEN);
|
||||
|
||||
if (isFrozen(view, account, info.mptIssue))
|
||||
return std::unexpected(tecLOCKED);
|
||||
|
||||
// The holder needs an existing, authorized MPToken; conversion never
|
||||
// creates one (that would silently take a reserve).
|
||||
if (!view.exists(keylet::mptoken(info.mptIssue.getMptID(), account)))
|
||||
return std::unexpected(tecNO_ENTRY);
|
||||
if (auto const ter = requireAuth(view, info.mptIssue, account, AuthType::StrongAuth);
|
||||
!isTesSuccess(ter))
|
||||
return std::unexpected(ter);
|
||||
|
||||
if (info.iouToMpt)
|
||||
{
|
||||
// Over-range (an uncapped binding with a high scale) or sub-unit
|
||||
// amounts cannot be represented in base units.
|
||||
auto const units = tokenBaseUnits(amount, scale);
|
||||
if (!units || *units == 0)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
info.units = *units;
|
||||
info.iouAmount = STAmount{info.issue, Number(info.units, -scale)};
|
||||
|
||||
auto const spendable =
|
||||
accountHolds(view, account, info.issue, FreezeHandling::ZeroIfFrozen, j);
|
||||
if (spendable < info.iouAmount)
|
||||
return std::unexpected(tecINSUFFICIENT_FUNDS);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.units = amount.mpt().value();
|
||||
info.iouAmount = STAmount{info.issue, Number(info.units, -scale)};
|
||||
|
||||
auto const spendable = accountHolds(
|
||||
view,
|
||||
account,
|
||||
info.mptIssue,
|
||||
FreezeHandling::ZeroIfFrozen,
|
||||
AuthHandling::ZeroIfUnauthorized,
|
||||
j);
|
||||
if (spendable < amount)
|
||||
return std::unexpected(tecINSUFFICIENT_FUNDS);
|
||||
|
||||
// The trust line must already exist with limit headroom; conversion
|
||||
// never creates one and never pushes a holder past their limit.
|
||||
auto const lineKey = keylet::trustLine(account, info.issue.account, info.issue.currency);
|
||||
auto const sleLine = view.read(lineKey);
|
||||
if (!sleLine)
|
||||
return std::unexpected(tecNO_LINE);
|
||||
|
||||
bool const accountLow = account < info.issue.account;
|
||||
STAmount const limit = sleLine->getFieldAmount(accountLow ? sfLowLimit : sfHighLimit);
|
||||
STAmount balance = sleLine->getFieldAmount(sfBalance);
|
||||
if (!accountLow)
|
||||
balance.negate();
|
||||
balance += info.iouAmount;
|
||||
if (limit < balance)
|
||||
return std::unexpected(tecLIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
TokenConvert::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
return ctx.rules.enabled(featureMPTokensV1);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
TokenConvert::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
STAmount const amount{ctx.tx[sfAmount]};
|
||||
|
||||
if (amount.native())
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (amount <= beast::kZero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
// The issuer cannot hold its own token in either representation.
|
||||
if (amount.getIssuer() == ctx.tx[sfAccount])
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TokenConvert::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const info = checkConvert(ctx.view, ctx.tx, ctx.j);
|
||||
if (!info)
|
||||
return info.error();
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TokenConvert::doApply()
|
||||
{
|
||||
auto const infoExp = checkConvert(view(), ctx_.tx, j_);
|
||||
if (!infoExp)
|
||||
return infoExp.error();
|
||||
auto const& info = *infoExp;
|
||||
|
||||
AccountID const& issuer = info.issue.account;
|
||||
STAmount const mptAmount{info.mptIssue, info.units};
|
||||
|
||||
if (info.iouToMpt)
|
||||
{
|
||||
// Redeem the IOU (exactly units * 10^-scale; sub-unit dust stays on
|
||||
// the trust line), then mint the bound MPT. Sum-neutral.
|
||||
if (auto const ter = directSendNoFee(view(), accountID_, issuer, info.iouAmount, true, j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Defensive cap check; a conversion is sum-neutral so this can only
|
||||
// trip if the two sides have drifted.
|
||||
auto const sleMpt = view().read(keylet::mptokenIssuance(info.mptIssue.getMptID()));
|
||||
if (!sleMpt)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
if (auto const maximum = sleMpt->at(~sfMaximumAmount))
|
||||
{
|
||||
auto const minted = static_cast<std::uint64_t>(info.units);
|
||||
if (minted > *maximum || sleMpt->at(sfOutstandingAmount) > *maximum - minted)
|
||||
return tecSUPPLY_EXCEEDED; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
return directSendNoFee(view(), issuer, accountID_, mptAmount, false, j_);
|
||||
}
|
||||
|
||||
// Burn the MPT, then credit the IOU. This direction is exact.
|
||||
if (auto const ter = directSendNoFee(view(), accountID_, issuer, mptAmount, false, j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
return directSendNoFee(view(), issuer, accountID_, info.iouAmount, true, j_);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
150
src/libxrpl/tx/transactors/token/TokenIssuanceCreate.cpp
Normal file
150
src/libxrpl/tx/transactors/token/TokenIssuanceCreate.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
#include <xrpl/tx/transactors/token/TokenIssuanceCreate.h>
|
||||
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STCurrency.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool
|
||||
TokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
// Binding to an MPT issuance requires the MPT amendment.
|
||||
return !ctx.tx.isFieldPresent(sfMPTokenIssuanceID) || ctx.rules.enabled(featureMPTokensV1);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
TokenIssuanceCreate::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfTokenIssuanceCreateMask;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
TokenIssuanceCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
Currency const currency = ctx.tx[sfCurrency];
|
||||
if (isXRP(currency) || currency == badCurrency())
|
||||
return temBAD_CURRENCY;
|
||||
|
||||
if (auto const fee = ctx.tx[~sfTransferFee]; fee && *fee > kMaxTransferFee)
|
||||
return temBAD_TRANSFER_FEE;
|
||||
|
||||
if (auto const metadata = ctx.tx[~sfMPTokenMetadata];
|
||||
metadata && (metadata->empty() || metadata->length() > kMaxMpTokenMetadataLength))
|
||||
return temMALFORMED;
|
||||
|
||||
if (auto const maximum = ctx.tx[~sfMaximumAmount];
|
||||
maximum && (*maximum == 0 || *maximum > kMaxTokenIssuanceAmount))
|
||||
return temMALFORMED;
|
||||
|
||||
if (auto const scale = ctx.tx[~sfTokenScale]; scale && *scale > kMaxTokenIssuanceScale)
|
||||
return temMALFORMED;
|
||||
|
||||
// The base unit must be defined for anything that counts in base units.
|
||||
if ((ctx.tx.isFieldPresent(sfMaximumAmount) || ctx.tx.isFieldPresent(sfMPTokenIssuanceID)) &&
|
||||
!ctx.tx.isFieldPresent(sfTokenScale))
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TokenIssuanceCreate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
AccountID const account = ctx.tx[sfAccount];
|
||||
Currency const currency = ctx.tx[sfCurrency];
|
||||
|
||||
if (ctx.view.exists(keylet::tokenIssuance(account, currency)))
|
||||
return tecDUPLICATE;
|
||||
|
||||
if (auto const mptId = ctx.tx[~sfMPTokenIssuanceID])
|
||||
return validateTokenBinding(
|
||||
ctx.view, *mptId, account, ctx.tx[~sfMaximumAmount], ctx.tx[~sfTokenScale].value_or(0));
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TokenIssuanceCreate::doApply()
|
||||
{
|
||||
auto ctx = ctx_.getApplyViewContext();
|
||||
auto const& tx = ctx_.tx;
|
||||
Currency const currency = tx[sfCurrency];
|
||||
|
||||
auto const acct = ctx.view.peek(keylet::account(accountID_));
|
||||
if (!acct)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sponsorExp = getEffectiveTxReserveSponsor(ctx, acct);
|
||||
if (!sponsorExp)
|
||||
return sponsorExp.error(); // LCOV_EXCL_LINE
|
||||
auto const sponsorSle = *sponsorExp;
|
||||
|
||||
if (auto const ret =
|
||||
checkReserve(ctx, acct, preFeeBalance_, sponsorSle, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
auto const issuanceKeylet = keylet::tokenIssuance(accountID_, currency);
|
||||
|
||||
auto const ownerNode = ctx.view.dirInsert(
|
||||
keylet::ownerDir(accountID_), issuanceKeylet, describeOwnerDir(accountID_));
|
||||
if (!ownerNode)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
|
||||
auto issuance = std::make_shared<SLE>(issuanceKeylet);
|
||||
std::uint32_t flags = 0;
|
||||
if (tx.isFlag(tfTokenCannotLock))
|
||||
flags |= lsfTokenCannotLock;
|
||||
(*issuance)[sfFlags] = flags;
|
||||
(*issuance)[sfIssuer] = accountID_;
|
||||
issuance->setFieldCurrency(sfCurrency, STCurrency{sfCurrency, currency});
|
||||
(*issuance)[sfOwnerNode] = *ownerNode;
|
||||
|
||||
if (auto const maximum = tx[~sfMaximumAmount])
|
||||
(*issuance)[sfMaximumAmount] = *maximum;
|
||||
|
||||
if (auto const scale = tx[~sfTokenScale])
|
||||
issuance->at(sfTokenScale) = *scale;
|
||||
|
||||
if (auto const fee = tx[~sfTransferFee])
|
||||
issuance->at(sfTransferFee) = *fee;
|
||||
|
||||
if (auto const metadata = tx[~sfMPTokenMetadata])
|
||||
(*issuance)[sfMPTokenMetadata] = *metadata;
|
||||
|
||||
if (auto const mptId = tx[~sfMPTokenIssuanceID])
|
||||
{
|
||||
(*issuance)[sfMPTokenIssuanceID] = *mptId;
|
||||
|
||||
auto const sleMpt = ctx.view.peek(keylet::mptokenIssuance(*mptId));
|
||||
if (!sleMpt || sleMpt->isFieldPresent(sfTokenIssuanceID))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
(*sleMpt)[sfTokenIssuanceID] = issuanceKeylet.key;
|
||||
ctx.view.update(sleMpt);
|
||||
}
|
||||
|
||||
addSponsorToLedgerEntry(issuance, sponsorSle);
|
||||
|
||||
ctx.view.insert(issuance);
|
||||
|
||||
increaseOwnerCount(ctx.view, acct, sponsorSle, 1, j_);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
79
src/libxrpl/tx/transactors/token/TokenIssuanceDestroy.cpp
Normal file
79
src/libxrpl/tx/transactors/token/TokenIssuanceDestroy.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include <xrpl/tx/transactors/token/TokenIssuanceDestroy.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STCurrency.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
TokenIssuanceDestroy::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TokenIssuanceDestroy::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
AccountID const account = ctx.tx[sfAccount];
|
||||
Currency const currency = ctx.tx[sfCurrency];
|
||||
|
||||
auto const sle = ctx.view.read(keylet::tokenIssuance(account, currency));
|
||||
if (!sle)
|
||||
return tecNO_ENTRY;
|
||||
|
||||
if (sle->isFlag(lsfTokenWrapped))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (Number const issued = sle->at(sfIssuedAmount); issued != Number{})
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
if (auto const mptId = sle->at(~sfMPTokenIssuanceID))
|
||||
{
|
||||
// A missing bound issuance leaves nothing outstanding on that side.
|
||||
if (auto const sleMpt = ctx.view.read(keylet::mptokenIssuance(*mptId)); sleMpt &&
|
||||
(sleMpt->at(sfOutstandingAmount) != 0 || sleMpt->at(~sfLockedAmount).value_or(0) != 0))
|
||||
return tecHAS_OBLIGATIONS;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TokenIssuanceDestroy::doApply()
|
||||
{
|
||||
Currency const currency = ctx_.tx[sfCurrency];
|
||||
|
||||
auto const sle = view().peek(keylet::tokenIssuance(accountID_, currency));
|
||||
if (!sle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Unbind the MPT issuance; with both sides at zero this is harmless and
|
||||
// lets the MPT issuance outlive the TokenIssuance.
|
||||
if (auto const mptId = sle->at(~sfMPTokenIssuanceID))
|
||||
{
|
||||
if (auto const sleMpt = view().peek(keylet::mptokenIssuance(*mptId)))
|
||||
{
|
||||
sleMpt->makeFieldAbsent(sfTokenIssuanceID);
|
||||
view().update(sleMpt);
|
||||
}
|
||||
}
|
||||
|
||||
if (!view().dirRemove(keylet::ownerDir(accountID_), (*sle)[sfOwnerNode], sle->key(), false))
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
decreaseOwnerCountForObject(view(), accountID_, sle, 1, j_);
|
||||
view().erase(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
125
src/libxrpl/tx/transactors/token/TokenIssuanceSet.cpp
Normal file
125
src/libxrpl/tx/transactors/token/TokenIssuanceSet.cpp
Normal file
@@ -0,0 +1,125 @@
|
||||
#include <xrpl/tx/transactors/token/TokenIssuanceSet.h>
|
||||
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STCurrency.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool
|
||||
TokenIssuanceSet::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
return !ctx.tx.isFieldPresent(sfMPTokenIssuanceID) || ctx.rules.enabled(featureMPTokensV1);
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
TokenIssuanceSet::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
return tfTokenIssuanceSetMask;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
TokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
auto const flags = ctx.tx.getFlags();
|
||||
|
||||
if ((flags & tfTokenLock) && (flags & tfTokenUnlock))
|
||||
return temINVALID_FLAG;
|
||||
|
||||
// Renouncing the lock while requesting it is contradictory.
|
||||
if ((flags & tfTokenCannotLock) && (flags & tfTokenLock))
|
||||
return temINVALID_FLAG;
|
||||
|
||||
if (auto const fee = ctx.tx[~sfTransferFee]; fee && *fee > kMaxTransferFee)
|
||||
return temBAD_TRANSFER_FEE;
|
||||
|
||||
if (auto const metadata = ctx.tx[~sfMPTokenMetadata];
|
||||
metadata && (metadata->empty() || metadata->length() > kMaxMpTokenMetadataLength))
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
AccountID const account = ctx.tx[sfAccount];
|
||||
Currency const currency = ctx.tx[sfCurrency];
|
||||
|
||||
auto const sle = ctx.view.read(keylet::tokenIssuance(account, currency));
|
||||
if (!sle)
|
||||
return tecNO_ENTRY;
|
||||
|
||||
if (sle->at(sfIssuer) != account)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Nobody speaks for a dead issuer: wrapped issuances are not configurable.
|
||||
if (sle->isFlag(lsfTokenWrapped))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (ctx.tx.isFlag(tfTokenLock) && sle->isFlag(lsfTokenCannotLock))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (auto const mptId = ctx.tx[~sfMPTokenIssuanceID])
|
||||
{
|
||||
// The binding is write-once.
|
||||
if (sle->isFieldPresent(sfMPTokenIssuanceID))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return validateTokenBinding(
|
||||
ctx.view, *mptId, account, sle->at(~sfMaximumAmount), sle->at(sfTokenScale));
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
TokenIssuanceSet::doApply()
|
||||
{
|
||||
auto const& tx = ctx_.tx;
|
||||
Currency const currency = tx[sfCurrency];
|
||||
|
||||
auto const sle = view().peek(keylet::tokenIssuance(accountID_, currency));
|
||||
if (!sle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
std::uint32_t flags = sle->getFlags();
|
||||
if (tx.isFlag(tfTokenLock))
|
||||
flags |= lsfTokenLocked;
|
||||
else if (tx.isFlag(tfTokenUnlock))
|
||||
flags &= ~lsfTokenLocked;
|
||||
if (tx.isFlag(tfTokenCannotLock))
|
||||
flags |= lsfTokenCannotLock;
|
||||
(*sle)[sfFlags] = flags;
|
||||
|
||||
if (auto const fee = tx[~sfTransferFee])
|
||||
sle->at(sfTransferFee) = *fee;
|
||||
|
||||
if (auto const metadata = tx[~sfMPTokenMetadata])
|
||||
(*sle)[sfMPTokenMetadata] = *metadata;
|
||||
|
||||
if (auto const mptId = tx[~sfMPTokenIssuanceID])
|
||||
{
|
||||
(*sle)[sfMPTokenIssuanceID] = *mptId;
|
||||
|
||||
auto const sleMpt = view().peek(keylet::mptokenIssuance(*mptId));
|
||||
if (!sleMpt || sleMpt->isFieldPresent(sfTokenIssuanceID))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
(*sleMpt)[sfTokenIssuanceID] = sle->key();
|
||||
view().update(sleMpt);
|
||||
}
|
||||
|
||||
view().update(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
826
src/test/app/TokenIssuance_test.cpp
Normal file
826
src/test/app/TokenIssuance_test.cpp
Normal file
@@ -0,0 +1,826 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h> // IWYU pragma: keep
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/offer.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/txflags.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenIssuanceHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class TokenIssuance_test : public beast::unit_test::Suite
|
||||
{
|
||||
FeatureBitset const all_{test::jtx::testableAmendments()};
|
||||
|
||||
static json::Value
|
||||
tiCreate(test::jtx::Account const& account, std::string const& currency)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::TokenIssuanceCreate;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[sfCurrency.jsonName] = currency;
|
||||
return jv;
|
||||
}
|
||||
|
||||
static json::Value
|
||||
tiSet(test::jtx::Account const& account, std::string const& currency)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::TokenIssuanceSet;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[sfCurrency.jsonName] = currency;
|
||||
return jv;
|
||||
}
|
||||
|
||||
static json::Value
|
||||
tiDestroy(test::jtx::Account const& account, std::string const& currency)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::TokenIssuanceDestroy;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[sfCurrency.jsonName] = currency;
|
||||
return jv;
|
||||
}
|
||||
|
||||
static json::Value
|
||||
tiConvert(test::jtx::Account const& account, STAmount const& amount)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::TokenConvert;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
|
||||
return jv;
|
||||
}
|
||||
|
||||
static std::shared_ptr<SLE const>
|
||||
issuanceSle(test::jtx::Env& env, test::jtx::Account const& issuer, test::jtx::IOU const& iou)
|
||||
{
|
||||
return env.le(keylet::tokenIssuance(issuer.id(), iou.currency));
|
||||
}
|
||||
|
||||
bool
|
||||
expectIssued(
|
||||
test::jtx::Env& env,
|
||||
test::jtx::Account const& issuer,
|
||||
test::jtx::IOU const& iou,
|
||||
Number const& expected)
|
||||
{
|
||||
auto const sle = issuanceSle(env, issuer, iou);
|
||||
if (!BEAST_EXPECT(sle))
|
||||
return false;
|
||||
Number const issued = (*sle)[sfIssuedAmount];
|
||||
return BEAST_EXPECTS(issued == expected, to_string(issued) + " != " + to_string(expected));
|
||||
}
|
||||
|
||||
void
|
||||
testDisabled(FeatureBitset features)
|
||||
{
|
||||
testcase("disabled");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features - featureTokenIssuance);
|
||||
Account const gw{"gw"};
|
||||
env.fund(XRP(10'000), gw);
|
||||
env.close();
|
||||
|
||||
env(tiCreate(gw, "USD"), Ter(temDISABLED));
|
||||
env(tiSet(gw, "USD"), Ter(temDISABLED));
|
||||
env(tiDestroy(gw, "USD"), Ter(temDISABLED));
|
||||
env(tiConvert(gw, gw["USD"](10)), Ter(temDISABLED));
|
||||
}
|
||||
|
||||
void
|
||||
testCreatePreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("create preflight");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gw"};
|
||||
env.fund(XRP(10'000), gw);
|
||||
env.close();
|
||||
|
||||
// XRP is not a valid currency for an issuance
|
||||
env(tiCreate(gw, "XRP"), Ter(temBAD_CURRENCY));
|
||||
|
||||
// Bad transfer fee
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfTransferFee.jsonName] = 50'001;
|
||||
env(jv, Ter(temBAD_TRANSFER_FEE));
|
||||
}
|
||||
|
||||
// Zero maximum
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "0";
|
||||
jv[sfTokenScale.jsonName] = 0;
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// Maximum above the representable bound
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "1000000000000001";
|
||||
jv[sfTokenScale.jsonName] = 0;
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// Maximum without a scale
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "1000";
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// Scale out of range
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "1000";
|
||||
jv[sfTokenScale.jsonName] = 19;
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// Invalid flags
|
||||
env(tiCreate(gw, "USD"), Txflags(0x00000008), Ter(temINVALID_FLAG));
|
||||
|
||||
// Empty metadata
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMPTokenMetadata.jsonName] = "";
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCreate(FeatureBitset features)
|
||||
{
|
||||
testcase("create");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gw"};
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(10'000), gw);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.ownerCount(gw) == 0);
|
||||
|
||||
// Uncapped, metadata-only issuance
|
||||
env(tiCreate(gw, "USD"));
|
||||
env.close();
|
||||
|
||||
{
|
||||
auto const sle = issuanceSle(env, gw, USD);
|
||||
if (!BEAST_EXPECT(sle))
|
||||
return;
|
||||
BEAST_EXPECT((*sle)[sfIssuer] == gw.id());
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfMaximumAmount));
|
||||
Number const issued = (*sle)[sfIssuedAmount];
|
||||
BEAST_EXPECT(issued == Number{});
|
||||
}
|
||||
BEAST_EXPECT(env.ownerCount(gw) == 1);
|
||||
|
||||
// Duplicate fails
|
||||
env(tiCreate(gw, "USD"), Ter(tecDUPLICATE));
|
||||
|
||||
// Capped issuance for another currency, with CannotLock renounced
|
||||
{
|
||||
auto jv = tiCreate(gw, "EUR");
|
||||
jv[sfMaximumAmount.jsonName] = "1000";
|
||||
jv[sfTokenScale.jsonName] = 0;
|
||||
env(jv, Txflags(tfTokenCannotLock));
|
||||
env.close();
|
||||
|
||||
auto const sle = issuanceSle(env, gw, gw["EUR"]);
|
||||
if (!BEAST_EXPECT(sle))
|
||||
return;
|
||||
BEAST_EXPECT((*sle)[sfMaximumAmount] == 1000);
|
||||
BEAST_EXPECT((*sle)[sfTokenScale] == 0);
|
||||
BEAST_EXPECT(sle->isFlag(lsfTokenCannotLock));
|
||||
}
|
||||
BEAST_EXPECT(env.ownerCount(gw) == 2);
|
||||
}
|
||||
|
||||
void
|
||||
testSetAndLock(FeatureBitset features)
|
||||
{
|
||||
testcase("set and lock");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(10'000), gw, alice, bob);
|
||||
env.close();
|
||||
env.trust(USD(1'000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Set on a missing issuance
|
||||
env(tiSet(gw, "USD"), Ter(tecNO_ENTRY));
|
||||
|
||||
env(tiCreate(gw, "USD"));
|
||||
env.close();
|
||||
|
||||
env(pay(gw, alice, USD(100)));
|
||||
env.close();
|
||||
|
||||
// Contradictory flags
|
||||
env(tiSet(gw, "USD"), Txflags(tfTokenLock | tfTokenUnlock), Ter(temINVALID_FLAG));
|
||||
env(tiSet(gw, "USD"), Txflags(tfTokenLock | tfTokenCannotLock), Ter(temINVALID_FLAG));
|
||||
|
||||
// Lock the currency: holder-to-holder transfers stop, redemption
|
||||
// to the issuer still works (GlobalFreeze semantics, scoped).
|
||||
env(tiSet(gw, "USD"), Txflags(tfTokenLock));
|
||||
env.close();
|
||||
{
|
||||
auto const sle = issuanceSle(env, gw, USD);
|
||||
if (!BEAST_EXPECT(sle))
|
||||
return;
|
||||
BEAST_EXPECT(sle->isFlag(lsfTokenLocked));
|
||||
}
|
||||
|
||||
env(pay(alice, bob, USD(10)), Ter(tecPATH_DRY));
|
||||
env.close();
|
||||
env(pay(alice, gw, USD(10)));
|
||||
env.close();
|
||||
env.require(Balance(alice, USD(90)));
|
||||
|
||||
// Unlock restores transfers
|
||||
env(tiSet(gw, "USD"), Txflags(tfTokenUnlock));
|
||||
env.close();
|
||||
env(pay(alice, bob, USD(10)));
|
||||
env.close();
|
||||
env.require(Balance(bob, USD(10)));
|
||||
|
||||
// Metadata and fee updates
|
||||
{
|
||||
auto jv = tiSet(gw, "USD");
|
||||
jv[sfTransferFee.jsonName] = 100;
|
||||
jv[sfMPTokenMetadata.jsonName] = strHex(std::string{"meta"});
|
||||
env(jv);
|
||||
env.close();
|
||||
auto const sle = issuanceSle(env, gw, USD);
|
||||
if (!BEAST_EXPECT(sle))
|
||||
return;
|
||||
BEAST_EXPECT((*sle)[sfTransferFee] == 100);
|
||||
BEAST_EXPECT(sle->isFieldPresent(sfMPTokenMetadata));
|
||||
}
|
||||
|
||||
// Renounce locking, then locking fails
|
||||
env(tiSet(gw, "USD"), Txflags(tfTokenCannotLock));
|
||||
env.close();
|
||||
env(tiSet(gw, "USD"), Txflags(tfTokenLock), Ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
void
|
||||
testDestroy(FeatureBitset features)
|
||||
{
|
||||
testcase("destroy");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(10'000), gw, alice);
|
||||
env.close();
|
||||
env.trust(USD(1'000), alice);
|
||||
env.close();
|
||||
|
||||
env(tiDestroy(gw, "USD"), Ter(tecNO_ENTRY));
|
||||
|
||||
env(tiCreate(gw, "USD"));
|
||||
env.close();
|
||||
|
||||
env(pay(gw, alice, USD(100)));
|
||||
env.close();
|
||||
|
||||
if (!expectIssued(env, gw, USD, Number(100)))
|
||||
return;
|
||||
|
||||
// Outstanding tokens block destruction. Close before the redemption
|
||||
// so the failed destroy is not replayed after it in canonical order.
|
||||
env(tiDestroy(gw, "USD"), Ter(tecHAS_OBLIGATIONS));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, gw, USD(100)));
|
||||
env.close();
|
||||
|
||||
if (!expectIssued(env, gw, USD, Number(0)))
|
||||
return;
|
||||
|
||||
env(tiDestroy(gw, "USD"));
|
||||
env.close();
|
||||
BEAST_EXPECT(!issuanceSle(env, gw, USD));
|
||||
BEAST_EXPECT(env.ownerCount(gw) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testSupplyCap(FeatureBitset features)
|
||||
{
|
||||
testcase("supply cap");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(10'000), gw, alice, bob, carol);
|
||||
env.close();
|
||||
env.trust(USD(10'000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "1000";
|
||||
jv[sfTokenScale.jsonName] = 0;
|
||||
env(jv);
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Issue up to 600
|
||||
env(pay(gw, alice, USD(600)));
|
||||
env.close();
|
||||
if (!expectIssued(env, gw, USD, Number(600)))
|
||||
return;
|
||||
|
||||
// 500 more would breach the cap
|
||||
env(pay(gw, alice, USD(500)), Ter(tecPATH_PARTIAL));
|
||||
env.close();
|
||||
expectIssued(env, gw, USD, Number(600));
|
||||
|
||||
// Redemption frees headroom
|
||||
env(pay(alice, gw, USD(100)));
|
||||
env.close();
|
||||
if (!expectIssued(env, gw, USD, Number(500)))
|
||||
return;
|
||||
|
||||
// Fill the cap exactly
|
||||
env(pay(gw, alice, USD(500)));
|
||||
env.close();
|
||||
if (!expectIssued(env, gw, USD, Number(1000)))
|
||||
return;
|
||||
|
||||
env(pay(gw, bob, USD(1)), Ter(tecPATH_PARTIAL));
|
||||
env.close();
|
||||
|
||||
// Holder-to-holder transfers are unaffected by the cap
|
||||
env(pay(alice, bob, USD(250)));
|
||||
expectIssued(env, gw, USD, Number(1000));
|
||||
env.close();
|
||||
env.require(Balance(bob, USD(250)));
|
||||
env.require(Balance(alice, USD(750)));
|
||||
if (!expectIssued(env, gw, USD, Number(1000)))
|
||||
return;
|
||||
|
||||
// An issuer's offer within the remaining headroom crosses in full
|
||||
env(pay(alice, gw, USD(400)));
|
||||
env.close();
|
||||
if (!expectIssued(env, gw, USD, Number(600)))
|
||||
return;
|
||||
|
||||
env(offer(gw, XRP(300), USD(300)));
|
||||
env.close();
|
||||
env(offer(bob, USD(300), XRP(300)));
|
||||
env.close();
|
||||
env.require(Balance(bob, USD(550)));
|
||||
if (!expectIssued(env, gw, USD, Number(900)))
|
||||
return;
|
||||
|
||||
// An issuer's offer beyond the headroom can never push issuance past
|
||||
// the cap: the supply invariant rejects the crossing outright.
|
||||
env(offer(gw, XRP(600), USD(600)));
|
||||
env.close();
|
||||
env(offer(carol, USD(600), XRP(600)), Ter(tecINVARIANT_FAILED));
|
||||
env.close();
|
||||
env.require(Balance(carol, USD(0)));
|
||||
if (!expectIssued(env, gw, USD, Number(900)))
|
||||
return;
|
||||
}
|
||||
|
||||
void
|
||||
testBinding(FeatureBitset features)
|
||||
{
|
||||
testcase("binding");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
|
||||
MPTTester mpt(env, gw, {.holders = {alice}});
|
||||
mpt.create({.maxAmt = 100'000, .assetScale = 2});
|
||||
auto const badId = makeMptID(env.seq(gw) + 100, gw.id());
|
||||
|
||||
// Binding a nonexistent MPT issuance
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "100000";
|
||||
jv[sfTokenScale.jsonName] = 2;
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(badId);
|
||||
env(jv, Ter(tecOBJECT_NOT_FOUND));
|
||||
}
|
||||
|
||||
// Cap mismatch
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "99999";
|
||||
jv[sfTokenScale.jsonName] = 2;
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(mpt.issuanceID());
|
||||
env(jv, Ter(tecWRONG_ASSET));
|
||||
}
|
||||
|
||||
// Scale mismatch
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "100000";
|
||||
jv[sfTokenScale.jsonName] = 3;
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(mpt.issuanceID());
|
||||
env(jv, Ter(tecWRONG_ASSET));
|
||||
}
|
||||
|
||||
// Not the MPT issuer
|
||||
{
|
||||
auto jv = tiCreate(alice, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "100000";
|
||||
jv[sfTokenScale.jsonName] = 2;
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(mpt.issuanceID());
|
||||
env(jv, Ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
// Valid binding
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "100000";
|
||||
jv[sfTokenScale.jsonName] = 2;
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(mpt.issuanceID());
|
||||
env(jv);
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
auto const sle = issuanceSle(env, gw, gw["USD"]);
|
||||
if (!BEAST_EXPECT(sle))
|
||||
return;
|
||||
BEAST_EXPECT((*sle)[~sfMPTokenIssuanceID] == mpt.issuanceID());
|
||||
|
||||
// The MPT issuance carries the back-pointer
|
||||
auto const sleMpt = env.le(keylet::mptokenIssuance(mpt.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleMpt))
|
||||
return;
|
||||
BEAST_EXPECT((*sleMpt)[~sfTokenIssuanceID] == sle->key());
|
||||
}
|
||||
|
||||
// A second issuance cannot bind the same MPT
|
||||
{
|
||||
auto jv = tiCreate(gw, "EUR");
|
||||
jv[sfMaximumAmount.jsonName] = "100000";
|
||||
jv[sfTokenScale.jsonName] = 2;
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(mpt.issuanceID());
|
||||
env(jv, Ter(tecDUPLICATE));
|
||||
}
|
||||
|
||||
// The binding is write-once
|
||||
{
|
||||
auto jv = tiSet(gw, "USD");
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(mpt.issuanceID());
|
||||
env(jv, Ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
// Destroy unbinds: back-pointer is cleared
|
||||
env(tiDestroy(gw, "USD"));
|
||||
env.close();
|
||||
{
|
||||
auto const sleMpt = env.le(keylet::mptokenIssuance(mpt.issuanceID()));
|
||||
if (!BEAST_EXPECT(sleMpt))
|
||||
return;
|
||||
BEAST_EXPECT(!sleMpt->isFieldPresent(sfTokenIssuanceID));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testConvert(FeatureBitset features)
|
||||
{
|
||||
testcase("convert");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
MPTTester mpt(env, gw, {.holders = {alice, bob, carol}});
|
||||
mpt.create({.maxAmt = 100'000, .assetScale = 2});
|
||||
MPTIssue const mptIssue{mpt.issuanceID()};
|
||||
|
||||
env.trust(USD(10'000), alice, carol);
|
||||
env.close();
|
||||
|
||||
// No TokenIssuance yet
|
||||
env(tiConvert(alice, USD(10)), Ter(tecOBJECT_NOT_FOUND));
|
||||
|
||||
// Unbound issuance: conversion unavailable
|
||||
env(tiCreate(gw, "EUR"));
|
||||
env.close();
|
||||
env(tiConvert(alice, gw["EUR"](10)), Ter(tecNO_PERMISSION));
|
||||
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "100000";
|
||||
jv[sfTokenScale.jsonName] = 2;
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(mpt.issuanceID());
|
||||
env(jv);
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Issue after the object exists so IssuedAmount tracks the float
|
||||
env(pay(gw, alice, USD(100.505)));
|
||||
env.close();
|
||||
|
||||
// The issuer cannot convert its own token
|
||||
env(tiConvert(gw, USD(10)), Ter(temMALFORMED));
|
||||
|
||||
// No MPToken for the holder yet
|
||||
env(tiConvert(alice, USD(10)), Ter(tecNO_ENTRY));
|
||||
|
||||
mpt.authorize({.account = alice});
|
||||
mpt.authorize({.account = bob});
|
||||
env.close();
|
||||
|
||||
// Sub-base-unit amount floors to zero units
|
||||
env(tiConvert(alice, USD(0.001)), Ter(tecPRECISION_LOSS));
|
||||
|
||||
// Insufficient balance
|
||||
env(tiConvert(alice, USD(200)), Ter(tecINSUFFICIENT_FUNDS));
|
||||
|
||||
// IOU -> MPT: debit exactly 10.50, mint 1050 units, dust stays
|
||||
env(tiConvert(alice, USD(10.505)));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(alice, USD(90.005)));
|
||||
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 1'050));
|
||||
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(1'050));
|
||||
if (!expectIssued(env, gw, USD, Number(90'005, -3)))
|
||||
return;
|
||||
|
||||
// MPT -> IOU: exact
|
||||
env(tiConvert(alice, STAmount{mptIssue, 1'000}));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(alice, USD(100.005)));
|
||||
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 50));
|
||||
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(50));
|
||||
if (!expectIssued(env, gw, USD, Number(100'005, -3)))
|
||||
return;
|
||||
|
||||
// More MPT than held
|
||||
env(tiConvert(alice, STAmount{mptIssue, 51}), Ter(tecINSUFFICIENT_FUNDS));
|
||||
|
||||
// bob holds MPT (paid directly) but has no trust line
|
||||
mpt.pay(gw, bob, 500);
|
||||
env.close();
|
||||
env(tiConvert(bob, STAmount{mptIssue, 500}), Ter(tecNO_LINE));
|
||||
|
||||
// A too-low limit blocks the credit
|
||||
env.trust(USD(1), bob);
|
||||
env.close();
|
||||
env(tiConvert(bob, STAmount{mptIssue, 500}), Ter(tecLIMIT_EXCEEDED));
|
||||
|
||||
env.trust(USD(1'000), bob);
|
||||
env.close();
|
||||
env(tiConvert(bob, STAmount{mptIssue, 500}));
|
||||
env.close();
|
||||
env.require(Balance(bob, USD(5)));
|
||||
|
||||
// carol has a line and IOU but no MPToken
|
||||
env(pay(gw, carol, USD(10)));
|
||||
env.close();
|
||||
env(tiConvert(carol, USD(10)), Ter(tecNO_ENTRY));
|
||||
|
||||
// Locking the currency blocks conversion. Close between the failed
|
||||
// convert and the unlock so canonical-order replay cannot flip it.
|
||||
env(tiSet(gw, "USD"), Txflags(tfTokenLock));
|
||||
env.close();
|
||||
env(tiConvert(alice, USD(10)), Ter(tecLOCKED));
|
||||
env.close();
|
||||
env(tiSet(gw, "USD"), Txflags(tfTokenUnlock));
|
||||
env.close();
|
||||
|
||||
// A frozen trust line blocks conversion
|
||||
env(trust(gw, USD(0), alice, tfSetFreeze));
|
||||
env.close();
|
||||
env(tiConvert(alice, USD(10)), Ter(tecFROZEN));
|
||||
env.close();
|
||||
env(trust(gw, USD(0), alice, tfClearFreeze));
|
||||
env.close();
|
||||
|
||||
// Conversions are sum-neutral: they work even at a full cap.
|
||||
// Fill the cap: issued 105.01 (alice) + 0.05 (alice MPT->wait)
|
||||
// Just verify a round trip leaves the counters unchanged.
|
||||
auto const sleBefore = issuanceSle(env, gw, USD);
|
||||
if (!BEAST_EXPECT(sleBefore))
|
||||
return;
|
||||
Number const issuedBefore = (*sleBefore)[sfIssuedAmount];
|
||||
|
||||
env(tiConvert(alice, USD(50)));
|
||||
env.close();
|
||||
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 5'050));
|
||||
env(tiConvert(alice, STAmount{mptIssue, 5'000}));
|
||||
env.close();
|
||||
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 50));
|
||||
|
||||
auto const sleAfter = issuanceSle(env, gw, USD);
|
||||
if (!BEAST_EXPECT(sleAfter))
|
||||
return;
|
||||
Number const issuedAfter = (*sleAfter)[sfIssuedAmount];
|
||||
BEAST_EXPECT(issuedBefore == issuedAfter);
|
||||
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 50));
|
||||
}
|
||||
|
||||
void
|
||||
testSharedCurrencyCode(FeatureBitset features)
|
||||
{
|
||||
testcase("shared currency code");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Two issuers of the same currency code: moving one issuer's token
|
||||
// must never touch the other issuer's counter.
|
||||
Env env(*this, features);
|
||||
Account const gwA{"gwA"};
|
||||
Account const gwB{"gwB"};
|
||||
Account const alice{"alice"};
|
||||
auto const aUSD = gwA["USD"];
|
||||
auto const bUSD = gwB["USD"];
|
||||
env.fund(XRP(10'000), gwA, gwB, alice);
|
||||
env.close();
|
||||
env.trust(aUSD(10'000), gwB, alice);
|
||||
env.trust(bUSD(10'000), alice);
|
||||
env.close();
|
||||
|
||||
env(tiCreate(gwA, "USD"));
|
||||
{
|
||||
auto jv = tiCreate(gwB, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "1000";
|
||||
jv[sfTokenScale.jsonName] = 0;
|
||||
env(jv);
|
||||
}
|
||||
env.close();
|
||||
|
||||
// gwB receiving gwA's USD affects only gwA's counter
|
||||
env(pay(gwA, gwB, aUSD(100)));
|
||||
env.close();
|
||||
if (!expectIssued(env, gwA, aUSD, Number(100)))
|
||||
return;
|
||||
if (!expectIssued(env, gwB, bUSD, Number(0)))
|
||||
return;
|
||||
|
||||
// gwB issuing its own USD affects only gwB's counter
|
||||
env(pay(gwB, alice, bUSD(500)));
|
||||
env.close();
|
||||
if (!expectIssued(env, gwA, aUSD, Number(100)))
|
||||
return;
|
||||
if (!expectIssued(env, gwB, bUSD, Number(500)))
|
||||
return;
|
||||
|
||||
// gwB's cap is intact: 501 more would breach it
|
||||
env(pay(gwB, alice, bUSD(501)), Ter(tecPATH_PARTIAL));
|
||||
env.close();
|
||||
env(pay(gwB, alice, bUSD(500)));
|
||||
env.close();
|
||||
if (!expectIssued(env, gwB, bUSD, Number(1000)))
|
||||
return;
|
||||
}
|
||||
|
||||
void
|
||||
testBoundMptDestroy(FeatureBitset features)
|
||||
{
|
||||
testcase("bound MPT destroy");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gw"};
|
||||
|
||||
MPTTester mpt(env, gw, MPTInit{});
|
||||
mpt.create({.maxAmt = 100'000, .assetScale = 2});
|
||||
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "100000";
|
||||
jv[sfTokenScale.jsonName] = 2;
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(mpt.issuanceID());
|
||||
env(jv);
|
||||
env.close();
|
||||
}
|
||||
|
||||
// A bound MPT issuance cannot be destroyed out from under the
|
||||
// TokenIssuance.
|
||||
mpt.destroy({.err = tecHAS_OBLIGATIONS});
|
||||
env.close();
|
||||
|
||||
// Destroying the TokenIssuance unbinds; then the MPT can go.
|
||||
env(tiDestroy(gw, "USD"));
|
||||
env.close();
|
||||
mpt.destroy({});
|
||||
}
|
||||
|
||||
void
|
||||
testWithoutMptAmendment(FeatureBitset features)
|
||||
{
|
||||
testcase("without MPT amendment");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features - featureMPTokensV1);
|
||||
Account const gw{"gw"};
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(10'000), gw);
|
||||
env.close();
|
||||
|
||||
// Plain issuances work without the MPT amendment
|
||||
{
|
||||
auto jv = tiCreate(gw, "USD");
|
||||
jv[sfMaximumAmount.jsonName] = "1000";
|
||||
jv[sfTokenScale.jsonName] = 0;
|
||||
env(jv);
|
||||
env.close();
|
||||
}
|
||||
BEAST_EXPECT(issuanceSle(env, gw, USD) != nullptr);
|
||||
|
||||
// Binding and conversion require it
|
||||
{
|
||||
auto jv = tiCreate(gw, "EUR");
|
||||
jv[sfMaximumAmount.jsonName] = "1000";
|
||||
jv[sfTokenScale.jsonName] = 0;
|
||||
jv[sfMPTokenIssuanceID.jsonName] = to_string(makeMptID(env.seq(gw), gw.id()));
|
||||
env(jv, Ter(temDISABLED));
|
||||
}
|
||||
env(tiConvert(gw, USD(10)), Ter(temDISABLED));
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerEntryRPC(FeatureBitset features)
|
||||
{
|
||||
testcase("ledger_entry");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gw"};
|
||||
env.fund(XRP(10'000), gw);
|
||||
env.close();
|
||||
|
||||
env(tiCreate(gw, "USD"));
|
||||
env.close();
|
||||
|
||||
json::Value params;
|
||||
params[jss::token_issuance][jss::issuer] = gw.human();
|
||||
params[jss::token_issuance][jss::currency] = "USD";
|
||||
auto const jrr = env.rpc("json", "ledger_entry", to_string(params))[jss::result];
|
||||
BEAST_EXPECTS(jrr[jss::node][sfIssuer.jsonName] == gw.human(), to_string(jrr));
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDisabled(all_);
|
||||
testCreatePreflight(all_);
|
||||
testCreate(all_);
|
||||
testSetAndLock(all_);
|
||||
testDestroy(all_);
|
||||
testSupplyCap(all_);
|
||||
testBinding(all_);
|
||||
testConvert(all_);
|
||||
testSharedCurrencyCode(all_);
|
||||
testBoundMptDestroy(all_);
|
||||
testWithoutMptAmendment(all_);
|
||||
testLedgerEntryRPC(all_);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(TokenIssuance, app, xrpl);
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -5199,7 +5199,7 @@ NetworkOPsImp::getBookPage(
|
||||
MetaView lesActive(lpLedger, tapNONE, true);
|
||||
OrderBookIterator obIterator(lesActive, book);
|
||||
|
||||
auto const rate = transferRate(lesActive, book.out.account);
|
||||
auto const rate = transferRate(lesActive, Issue{book.out.currency, book.out.account});
|
||||
|
||||
bool const bGlobalFreeze =
|
||||
lesActive.isGlobalFrozen(book.out.account) || lesActive.isGlobalFrozen(book.in.account);
|
||||
|
||||
@@ -314,6 +314,7 @@ doAccountObjects(rpc::JsonContext& context)
|
||||
{.name = jss::permissioned_domain, .type = ltPERMISSIONED_DOMAIN},
|
||||
{.name = jss::vault, .type = ltVAULT},
|
||||
{.name = jss::sponsorship, .type = ltSPONSORSHIP},
|
||||
{.name = jss::token_issuance, .type = ltTOKEN_ISSUANCE},
|
||||
};
|
||||
|
||||
typeFilter.emplace();
|
||||
|
||||
@@ -564,6 +564,41 @@ parseMPTokenIssuance(
|
||||
return keylet::mptokenIssuance(*mptIssuanceID).key;
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseTokenIssuance(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
return parseObjectID(params, fieldName);
|
||||
}
|
||||
|
||||
if (auto const value = ledger_entry_helpers::hasRequired(params, {jss::issuer, jss::currency});
|
||||
!value)
|
||||
{
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
|
||||
auto const issuer = ledger_entry_helpers::parse<AccountID>(params[jss::issuer]);
|
||||
if (!issuer)
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedAddress", jss::issuer, "AccountID");
|
||||
}
|
||||
|
||||
Currency currency;
|
||||
if (!params[jss::currency].isString() || params[jss::currency] == "" ||
|
||||
!toCurrency(currency, params[jss::currency].asString()))
|
||||
{
|
||||
return ledger_entry_helpers::invalidFieldError(
|
||||
"malformedCurrency", jss::currency, "Currency");
|
||||
}
|
||||
|
||||
return keylet::tokenIssuance(*issuer, currency).key;
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseNFTokenOffer(
|
||||
json::Value const& params,
|
||||
|
||||
Reference in New Issue
Block a user