From 20071660aea40aed287d8a9d11cd91b44aad5a00 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Sun, 27 Sep 2026 13:36:00 +0300 Subject: [PATCH 1/5] Document ephemeral tokens, token lending and mint rate limits Add three sections to the MultiTokens pallet page for the upcoming runtime upgrade, plus matching Create Token parameters and terminology entries. Extrinsic names, parameters, errors, events and the loosening delay were verified against the canary matrixchain metadata (spec version 1040). Co-Authored-By: Claude Fable 5.1 --- .../01-multitoken-pallet.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md index 5c8c5ee..d8f635b 100644 --- a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md +++ b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md @@ -42,6 +42,9 @@ Token grouping is currently implemented at the blockchain level. While users can - **Approval -** Required for an operator to use an account - **Freeze/Thaw -** If a collection, token, or account is frozen, it cannot transfer tokens - **Descriptor -** Used to create something. For example, a CollectionDescriptor creates a Collection. +- **Ephemeral Token -** An NFT with a fixed expiration block, after which it is automatically destroyed. See [#Ephemeral Tokens](#ephemeral-tokens). +- **Loan -** A temporary transfer of an NFT to a borrower, automatically returned to the lender at an expiration block. See [#Token Lending](#token-lending). +- **Mint Rate Limit -** A cap on how many units may be minted within a rolling period, set per collection or per token. See [#Mint Rate Limit](#mint-rate-limit). ## Collections @@ -150,6 +153,9 @@ To create an NFT, set the cap: `supply`/`collapsing supply` to `1`. - ENJ Infusion: - Infusion: The amount of ENJ to infuse to each unit. (More info in the [#ENJ Infusion](#enj-infusion) section below) - Anyone Can Infuse: Whether anyone will be able to add infusion to this token, or only the collection owner. +- Ephemeral Expiration: The block number at which the token is automatically destroyed. Leave as `None` for a regular, permanent token. This setting is immutable and requires the token to be an NFT. (More info in the [#Ephemeral Tokens](#ephemeral-tokens) section below) +- Is Lendable: Whether holders of this token are allowed to lend it. Defaults to `true`, and can be changed later on using the `MutateToken` extrinsic. (More info in the [#Token Lending](#token-lending) section below) +- Mint Rate Limit: An optional `period` (in blocks) and `max` amount that caps how many units of this token can be minted within any rolling period. (More info in the [#Mint Rate Limit](#mint-rate-limit) section below) ![](/img/components/enjin-matrixchain/9.png) @@ -294,6 +300,93 @@ If `token_id` is `None`, it removes all attributes of the collection. If `token_ ![](/img/components/enjin-matrixchain/23.png) +## Ephemeral Tokens + +An ephemeral token is a short-lived NFT. When it is created, an expiration block is set, and once the chain reaches that block the token is automatically and irreversibly destroyed, no matter who holds it at the time. + +Ephemeral tokens are created with the regular `mint` extrinsic by setting the `ephemeral_expiration` field of `CreateToken` to a future block number. The following rules apply: + +- The ephemeral nature and the expiration block are **immutable**. A regular token cannot become ephemeral, an ephemeral token cannot become permanent, and its expiration cannot be extended. Make sure the token's metadata makes its short-lived nature clear to holders. +- The token must be an NFT, i.e. created with a supply cap of `Supply(1)` or `CollapsingSupply(1)`. Multi-unit tokens cannot be ephemeral. +- The expiration block must be in the future. A limited number of tokens may share the same expiration block, so if the call fails with `EphemeralScheduleFull`, choose a nearby block instead. + +Until the expiration block is reached, an ephemeral token behaves like any other NFT: it can be transferred, listed on the marketplace (subject to the marketplace and the token's `listing_forbidden` setting), lent, and burned. Burning it early simply destroys it ahead of schedule. + +Once the expiration block is reached, the token can no longer be transferred and is queued for destruction. Destruction runs automatically during idle block time, in chunks, so a large batch of tokens expiring on the same block may take a few blocks to clear. Anyone can speed this up by calling the permissionless `cleanup_expired_ephemeral` extrinsic, which is fee-free when it destroys at least one token. + +Destruction removes the token and its attributes from storage and emits an `EphemeralTokenDestroyed` event. If a token had also been lent, destruction takes precedence: the token is destroyed instead of being returned to the lender. + +## Token Lending + +Token lending lets a holder temporarily transfer an NFT to another account with a guarantee that it comes back. The lender sets an expiration block, the token moves to the borrower, and at the expiration block the chain automatically returns it to the lender. + +### Enabling Lending + +Each token has an `is_lendable` flag that controls whether it can be lent. It defaults to `true` for new and existing tokens, and only the collection owner can change it, either at creation time via `CreateToken` or later using the `mutate_token` extrinsic. Games that don't want their items to be lent should set it to `false`. + +### Lending a Token + +Any holder of a lendable NFT can lend it using the `lend` extrinsic, which takes the `collection_id`, `token_id`, the `borrower` account, and the `expiration` block at which the token is returned. The following rules apply: + +- Only NFTs with a supply cap of `Supply(1)` or `CollapsingSupply(1)` can be lent. Multi-unit tokens cannot be lent. +- The token must not already be lent, and the expiration block must be in the future. +- The token moves to the borrower through a regular transfer, so the token must not be frozen. The lender pays the borrower's . + +A successful call emits a `TokenLent` event. + +### While a Token is Lent + +The borrower holds the token but with restrictions in place to protect the lender: + +- A lent token **cannot be listed** on the marketplace. +- A lent token **cannot be burned**. +- A lent token **can only be transferred back to the lender**. This return transfer supersedes freeze and transferability restrictions, so the token can always find its way back. + +The borrower can end the loan early by transferring the token back to the lender, which clears the loan and emits a `TokenReturned` event. The lender cannot claw the token back before the expiration block. + +### Extending a Loan + +The lender can push the expiration further out using the `extend_loan` extrinsic with a `new_expiration` that is strictly later than the current one. A loan can only be extended before its current expiration block is reached, and it can never be shortened. Extending emits a `LoanExtended` event. + +### Automatic Return + +When the expiration block is reached, the token is automatically returned to the lender during idle block time and a `TokenReturned` event is emitted. As with ephemeral tokens, returns are processed in chunks, and anyone can call the permissionless, fee-free `process_expired_loans` extrinsic to speed the process up. + +If a token is both ephemeral and lent, and its ephemeral expiration comes first, it is destroyed rather than returned. See [#Ephemeral Tokens](#ephemeral-tokens) above. + +## Mint Rate Limit + +A token's supply cap governs *how many* units can ever exist. A mint rate limit is an independent, optional policy that governs *how fast* units can be minted. The two can be combined freely: a token may have either, both, or neither, and tokens without a rate limit mint exactly as before. + +Rate limits protect both the collection owner and their community. If the owner's key is compromised, an attacker cannot mint an unlimited number of tokens, and the delay on loosening a limit (described below) gives the owner time to react. Rate limits also act as a trust layer, giving holders a provable, on-chain guarantee of how quickly a token's supply can grow. + +### Scopes + +A rate limit can be set at two scopes, and a mint must satisfy both when both are set: + +- **Collection scope** limits the aggregate number of units minted across the collection when new tokens are created. Minting additional units of an existing token is not counted at this scope. +- **Token scope** limits the number of units minted for a single token, whether through `CreateToken` or `Mint`. + +### Parameters + +A rate limit is defined by two values: + +- `period`: the length of the window, in blocks (a block is produced roughly every 6 seconds, so 14,400 blocks is about 24 hours). +- `max`: the maximum number of units that may be minted within one period. + +Both values must be greater than zero. The limit is enforced over a rolling window: any mint, including a `batch_mint`, that would push the total minted within the trailing `period` blocks above `max` is rejected with `MintRateLimitExceeded`. There is no fixed reset point; allowance is restored as earlier mints age out of the window. Burning units does **not** restore allowance, as the limit measures minting velocity, not net supply. + +### Setting and Changing a Limit + +A token-scope limit can be set at creation time through the `mint_rate_limit` field of `CreateToken`. Otherwise, the collection owner uses the `set_mint_rate_limit` extrinsic, passing the `collection_id`, an optional `token_id` (`None` targets the collection scope), and the new `limit` (`None` removes it). What happens next depends on the direction of the change: + +- **Tightening** takes effect immediately. This covers adding a limit where none existed, or lowering `max` without shortening `period`. A `MintRateLimitUpdated` event is emitted. +- **Loosening** is delayed. Raising `max`, shortening `period`, mixed changes, and removing the limit altogether are scheduled to take effect after a delay of 43,200 blocks (about 72 hours). A `MintRateLimitChangeScheduled` event is emitted with the `effective_block`, and the current limit stays enforced until then. Submitting another change while one is pending replaces it and restarts the delay. + +The delay exists so that a compromised owner key cannot instantly loosen a limit and drain a token economy. It provides a window in which a pending loosening can be detected on-chain and stopped: the collection owner can call `cancel_mint_rate_limit_change` at any time before the effective block to discard the pending change, which applies immediately and emits a `MintRateLimitChangeCancelled` event. + +Every rate limit change emits an event, so off-chain monitors can track a collection's minting policy without inspecting chain storage directly. + ## ENJ Infusion The ENJ Infusion of a token represents the backing value of each unit in ENJ, which is returned to the token holder when it's ed. From 935180a6ac537bd88375a207cdef90378ad82074 Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Sun, 27 Sep 2026 13:51:48 +0300 Subject: [PATCH 2/5] Reword lending flag guidance to cover all collection owners Co-Authored-By: Claude Fable 5.1 --- .../03-enjin-matrixchain/01-multitoken-pallet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md index d8f635b..1498643 100644 --- a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md +++ b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md @@ -322,7 +322,7 @@ Token lending lets a holder temporarily transfer an NFT to another account with ### Enabling Lending -Each token has an `is_lendable` flag that controls whether it can be lent. It defaults to `true` for new and existing tokens, and only the collection owner can change it, either at creation time via `CreateToken` or later using the `mutate_token` extrinsic. Games that don't want their items to be lent should set it to `false`. +Each token has an `is_lendable` flag that controls whether it can be lent. It defaults to `true` for new and existing tokens, and only the collection owner can change it, either at creation time via `CreateToken` or later using the `mutate_token` extrinsic. Collection owners who don't want their tokens to be lent should set it to `false`. ### Lending a Token From 4e0548914ff7ecf24cdd59fbe535f91665b741e9 Mon Sep 17 00:00:00 2001 From: Rene Date: Mon, 28 Sep 2026 02:32:42 +0200 Subject: [PATCH 3/5] Clarify ephemeral listings, lending preconditions, rate limit rules and failure handling Checked against enjin/pallets@776ecdc (the rev matrixchain master pins): ephemeral expiry force-releases listing holds, lend requires an unlisted token and a different borrower, tightening is max not raised and period not shortened, the rolling window restores allowance in 1/8-period steps, failed destructions and returns are parked and retried, and lend/extend_loan can fail with LoanScheduleFull. --- .../03-enjin-matrixchain/01-multitoken-pallet.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md index 1498643..9dd9b1d 100644 --- a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md +++ b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md @@ -314,7 +314,9 @@ Until the expiration block is reached, an ephemeral token behaves like any other Once the expiration block is reached, the token can no longer be transferred and is queued for destruction. Destruction runs automatically during idle block time, in chunks, so a large batch of tokens expiring on the same block may take a few blocks to clear. Anyone can speed this up by calling the permissionless `cleanup_expired_ephemeral` extrinsic, which is fee-free when it destroys at least one token. -Destruction removes the token and its attributes from storage and emits an `EphemeralTokenDestroyed` event. If a token had also been lent, destruction takes precedence: the token is destroyed instead of being returned to the lender. +If destroying a token fails, an `EphemeralCleanupFailed` event is emitted and the token is set aside to be retried automatically during idle block time. Anyone can also retry it directly with the permissionless `retry_failed_ephemeral_cleanup` extrinsic, which is fee-free when the token is destroyed. + +Destruction removes the token and its attributes from storage and emits an `EphemeralTokenDestroyed` event. Expiration overrides any hold on the token, so if it is listed on the marketplace at that time, the listing's hold is released and the token is destroyed regardless. If a token had also been lent, destruction takes precedence: the token is destroyed instead of being returned to the lender. ## Token Lending @@ -329,8 +331,8 @@ Each token has an `is_lendable` flag that controls whether it can be lent. It de Any holder of a lendable NFT can lend it using the `lend` extrinsic, which takes the `collection_id`, `token_id`, the `borrower` account, and the `expiration` block at which the token is returned. The following rules apply: - Only NFTs with a supply cap of `Supply(1)` or `CollapsingSupply(1)` can be lent. Multi-unit tokens cannot be lent. -- The token must not already be lent, and the expiration block must be in the future. -- The token moves to the borrower through a regular transfer, so the token must not be frozen. The lender pays the borrower's . +- The token must not already be lent, and the expiration block must be in the future. A limited number of loans may come due on the same block, so if the call fails with `LoanScheduleFull`, choose a nearby block instead. +- The token moves to the borrower through a regular transfer, so it must not be frozen or listed on the marketplace, and the borrower must be a different account from the lender. The lender pays the borrower's . A successful call emits a `TokenLent` event. @@ -346,11 +348,11 @@ The borrower can end the loan early by transferring the token back to the lender ### Extending a Loan -The lender can push the expiration further out using the `extend_loan` extrinsic with a `new_expiration` that is strictly later than the current one. A loan can only be extended before its current expiration block is reached, and it can never be shortened. Extending emits a `LoanExtended` event. +The lender can push the expiration further out using the `extend_loan` extrinsic with a `new_expiration` that is strictly later than the current one. The same per-block limit applies to `new_expiration`. A loan can only be extended before its current expiration block is reached, and it can never be shortened. Extending emits a `LoanExtended` event. ### Automatic Return -When the expiration block is reached, the token is automatically returned to the lender during idle block time and a `TokenReturned` event is emitted. As with ephemeral tokens, returns are processed in chunks, and anyone can call the permissionless, fee-free `process_expired_loans` extrinsic to speed the process up. +When the expiration block is reached, the token is automatically returned to the lender during idle block time and a `TokenReturned` event is emitted. As with ephemeral tokens, returns are processed in chunks, and anyone can call the permissionless, fee-free `process_expired_loans` extrinsic to speed the process up. If an automatic return fails, a `LoanReturnFailed` event is emitted and the return is retried automatically during idle block time. Anyone can also retry it with the permissionless `retry_failed_loan_return` extrinsic, which is fee-free when the return succeeds. If a token is both ephemeral and lent, and its ephemeral expiration comes first, it is destroyed rather than returned. See [#Ephemeral Tokens](#ephemeral-tokens) above. @@ -374,13 +376,13 @@ A rate limit is defined by two values: - `period`: the length of the window, in blocks (a block is produced roughly every 6 seconds, so 14,400 blocks is about 24 hours). - `max`: the maximum number of units that may be minted within one period. -Both values must be greater than zero. The limit is enforced over a rolling window: any mint, including a `batch_mint`, that would push the total minted within the trailing `period` blocks above `max` is rejected with `MintRateLimitExceeded`. There is no fixed reset point; allowance is restored as earlier mints age out of the window. Burning units does **not** restore allowance, as the limit measures minting velocity, not net supply. +Both values must be greater than zero. The limit is enforced over a rolling window: any mint, including a `batch_mint`, that would push the total minted within the trailing `period` blocks above `max` is rejected with `MintRateLimitExceeded`. There is no fixed reset point. The window is tracked in eight equal sub-slots, and allowance is restored one sub-slot at a time as earlier mints age out, so a mint can keep counting against the limit for up to one-eighth of a `period` beyond the window. Burning units does **not** restore allowance, as the limit measures minting velocity, not net supply. ### Setting and Changing a Limit A token-scope limit can be set at creation time through the `mint_rate_limit` field of `CreateToken`. Otherwise, the collection owner uses the `set_mint_rate_limit` extrinsic, passing the `collection_id`, an optional `token_id` (`None` targets the collection scope), and the new `limit` (`None` removes it). What happens next depends on the direction of the change: -- **Tightening** takes effect immediately. This covers adding a limit where none existed, or lowering `max` without shortening `period`. A `MintRateLimitUpdated` event is emitted. +- **Tightening** takes effect immediately. A change counts as tightening when `max` is not raised and `period` is not shortened, for example adding a limit where none existed, lowering `max`, or lengthening `period`. A `MintRateLimitUpdated` event is emitted. - **Loosening** is delayed. Raising `max`, shortening `period`, mixed changes, and removing the limit altogether are scheduled to take effect after a delay of 43,200 blocks (about 72 hours). A `MintRateLimitChangeScheduled` event is emitted with the `effective_block`, and the current limit stays enforced until then. Submitting another change while one is pending replaces it and restarts the delay. The delay exists so that a compromised owner key cannot instantly loosen a limit and drain a token economy. It provides a window in which a pending loosening can be detected on-chain and stopped: the collection owner can call `cancel_mint_rate_limit_change` at any time before the effective block to discard the pending change, which applies immediately and emits a `MintRateLimitChangeCancelled` event. From 96f9367e0562e9b09a0963adfba6333ce260976e Mon Sep 17 00:00:00 2001 From: Etay Matzliah Date: Mon, 28 Sep 2026 16:59:42 +0300 Subject: [PATCH 4/5] Apply batched suggestions from code review Co-authored-by: Brad Bayliss --- .../03-enjin-matrixchain/01-multitoken-pallet.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md index 9dd9b1d..3f6a67d 100644 --- a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md +++ b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md @@ -153,7 +153,7 @@ To create an NFT, set the cap: `supply`/`collapsing supply` to `1`. - ENJ Infusion: - Infusion: The amount of ENJ to infuse to each unit. (More info in the [#ENJ Infusion](#enj-infusion) section below) - Anyone Can Infuse: Whether anyone will be able to add infusion to this token, or only the collection owner. -- Ephemeral Expiration: The block number at which the token is automatically destroyed. Leave as `None` for a regular, permanent token. This setting is immutable and requires the token to be an NFT. (More info in the [#Ephemeral Tokens](#ephemeral-tokens) section below) +- Ephemeral Expiration: The block number at which the token is scheduled to be destroyed. Leave as `None` for a regular, permanent token. This setting is immutable and requires the token to be an NFT. (More info in the [#Ephemeral Tokens](#ephemeral-tokens) section below) - Is Lendable: Whether holders of this token are allowed to lend it. Defaults to `true`, and can be changed later on using the `MutateToken` extrinsic. (More info in the [#Token Lending](#token-lending) section below) - Mint Rate Limit: An optional `period` (in blocks) and `max` amount that caps how many units of this token can be minted within any rolling period. (More info in the [#Mint Rate Limit](#mint-rate-limit) section below) @@ -302,7 +302,9 @@ If `token_id` is `None`, it removes all attributes of the collection. If `token_ ## Ephemeral Tokens -An ephemeral token is a short-lived NFT. When it is created, an expiration block is set, and once the chain reaches that block the token is automatically and irreversibly destroyed, no matter who holds it at the time. +An ephemeral token is a short-lived NFT. When it is created, an expiration block is set, and once the chain reaches that block the token is scheduled to be automatically and irreversibly destroyed, no matter who holds it at the time. + +One example of a use-case of ephemeral tokens is a time-limited holiday event wherein the cleanup of all token related to that event happens predictably and automatically. Ephemeral tokens are created with the regular `mint` extrinsic by setting the `ephemeral_expiration` field of `CreateToken` to a future block number. The following rules apply: From 6127085791d45c22face7e1e408fa5c1c0dc467f Mon Sep 17 00:00:00 2001 From: Brad Bayliss Date: Tue, 29 Sep 2026 08:20:37 +0100 Subject: [PATCH 5/5] Apply suggestion from @Bradez --- .../03-enjin-matrixchain/01-multitoken-pallet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md index 3f6a67d..c9dee24 100644 --- a/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md +++ b/docs/04-enjin-blockchain/03-enjin-matrixchain/01-multitoken-pallet.md @@ -326,7 +326,7 @@ Token lending lets a holder temporarily transfer an NFT to another account with ### Enabling Lending -Each token has an `is_lendable` flag that controls whether it can be lent. It defaults to `true` for new and existing tokens, and only the collection owner can change it, either at creation time via `CreateToken` or later using the `mutate_token` extrinsic. Collection owners who don't want their tokens to be lent should set it to `false`. +Each token has an `is_lendable` flag that controls whether it can be lent. It defaults to `false` for new and existing tokens, and only the collection owner can change it, either at creation time via `CreateToken` or later using the `mutate_token` extrinsic. Collection owners who want to enable tokens to be lent should set it to `true`. ### Lending a Token