Create Fungible Tokens: Currency Standard
Assets created using the Currency Standard are free-flowing, wrappable, freely transferable fungible assets that you can store in any app. The Currency Standard uses the sui::coin_registry module and the Currency object for metadata.
The Currency<T> type represents open-loop fungible tokens. Currencies are denominated by their type parameter T, which is also associated with metadata (like name, symbol, and decimal precision) that applies to all instances of Currency<T>. The sui::coin_registry module exposes an interface over Currency<T> that treats it as fungible, meaning that a unit of T held in one instance of Currency<T> is interchangeable with any other unit of T, similar to how traditional fiat currencies operate.
Coin Registry
The Coin Registry system provides a centralized approach to currency management through the sui::coin_registry module. The registry is a shared object located at address 0xc that stores metadata, supply information, and regulatory status for all registered coin types.
Creation options
The registry supports two different coin creation flows:
- Standard creation (
coin_registry::new_currency): Recommended for most cases. You can call this method any time after the coin type is published. - One-Time Witness creation (
coin_registry::new_currency_with_otw): Uses a One-Time Witness for uniqueness proof. Requires a two-step publish and finalize process.
Both creation methods return a CurrencyInitializer<T> that allows for additional configuration:
- Regulated tokens: Add deny list capabilities to make your token regulated.
- Supply model: Choose between fixed, burn-only, or flexible supply.
- Extensions: Include additional fields for custom functionality.
/// Hot potato wrapper to enforce registration after "new_currency" data creation.
/// Destroyed in the `finalize` call and either transferred to the `CoinRegistry`
/// (in case of an OTW registration) or shared directly (for dynamically created
/// currencies).
public struct CurrencyInitializer<phantom T> {
currency: Currency<T>,
extra_fields: Bag,
is_otw: bool,
}
Standard creation (recommended)
Use the new_currency function at any time after the coin type is published. It creates a shared Currency<T> object, where type T must be a key-only type (public struct MyCoin has key { id: UID }):
#[allow(lint(self_transfer))]
public fun new_currency(registry: &mut CoinRegistry, ctx: &mut TxContext): Coin<MyCoin> {
let (mut currency, mut treasury_cap) = coin_registry::new_currency(
registry,
6, // Decimals
b"MyCoin".to_string(), // Symbol
b"My Coin".to_string(), // Name
b"Standard Unregulated Coin".to_string(), // Description
b"https://example.com/my_coin.png".to_string(), // Icon URL
ctx,
);
let total_supply = treasury_cap.mint(TOTAL_SUPPLY, ctx);
currency.make_supply_burn_only(treasury_cap);
let metadata_cap = currency.finalize(ctx);
transfer::public_transfer(metadata_cap, ctx.sender());
total_supply
}
Once you call the finalize function, currency creation is complete:
#[allow(lint(share_owned))]
/// Finalize the coin initialization, returning `MetadataCap`
public fun finalize<T>(builder: CurrencyInitializer<T>, ctx: &mut TxContext): MetadataCap<T>
One-Time Witness creation
Proper creation and RPC support requires a second transaction to promote the currency to the registry.
One-Time Witness creation of a new coin is a two-step process. The initialization process begins with package publication:
fun init(witness: MY_COIN_NEW, ctx: &mut TxContext) {
let (builder, treasury_cap) = coin_registry::new_currency_with_otw(
witness,
6, // Decimals
b"MY_COIN".to_string(), // Symbol
b"My Coin".to_string(), // Name
b"Standard Unregulated Coin".to_string(), // Description
b"https://example.com/my_coin.png".to_string(), // Icon URL
ctx,
);
let metadata_cap = builder.finalize(ctx);
transfer::public_transfer(treasury_cap, ctx.sender());
transfer::public_transfer(metadata_cap, ctx.sender());
}
Then, call coin_registry::finalize_registration to place the coin into the registry:
# Requires the ID of the Currency object created during publishing.
# This step is only required for OTW-created currencies.
sui client ptb \
--assign @created_currency_object_id currency_to_promote \
--move-call 0x2::coin_registry::finalize_registration <CURRENCY_TYPE> @0xc currency_to_promote
/// The second step in the "otw" initialization of coin metadata, that takes in
/// the `Currency<T>` that was transferred from init, and transforms it in to a
/// "derived address" shared object.
///
/// Can be performed by anyone.
public fun finalize_registration<T>(
registry: &mut CoinRegistry,
currency: Receiving<Currency<T>>,
_ctx: &mut TxContext,
)
Configure supply model
Before calling finalize, you can configure the supply model for your currency. The Currency Standard supports three supply models:
Fixed supply
Mint the total supply during initialization, then call make_supply_fixed. The Currency locks the TreasuryCap, and no further minting or burning is possible.
fun init(witness: FIXED_SUPPLY, ctx: &mut TxContext) {
let (mut currency, mut treasury_cap) = coin_registry::new_currency_with_otw(
witness,
6, // Decimals
b"FIXED_SUPPLY".to_string(), // Symbol
b"Fixed Supply Coin".to_string(), // Name
b"Cannot be minted nor burned".to_string(), // Description
b"https://example.com/my_coin.png".to_string(), // Icon URL
ctx,
);
let total_supply = treasury_cap.mint(TOTAL_SUPPLY, ctx);
currency.make_supply_fixed(treasury_cap);
let metadata_cap = currency.finalize(ctx);
transfer::public_transfer(metadata_cap, ctx.sender());
transfer::public_transfer(total_supply, ctx.sender());
}
Burn-only (deflationary) supply
Mint the initial supply, then call make_supply_burn_only. The Currency locks the TreasuryCap to prevent further minting, but the Currency object still allows burns.
fun init(witness: BURN_ONLY_SUPPLY, ctx: &mut TxContext) {
let (mut currency, mut treasury_cap) = coin_registry::new_currency_with_otw(
witness,
6, // Decimals
b"BURN_ONLY_SUPPLY".to_string(), // Symbol
b"Deflationary Supply Coin".to_string(), // Name
b"Cannot be minted, but can be burned".to_string(), // Description
b"https://example.com/my_coin.png".to_string(), // Icon URL
ctx,
);
let total_supply = treasury_cap.mint(TOTAL_SUPPLY, ctx);
currency.make_supply_burn_only(treasury_cap);
let metadata_cap = currency.finalize(ctx);
transfer::public_transfer(metadata_cap, ctx.sender());
transfer::public_transfer(total_supply, ctx.sender());
}
To burn coins after initialization:
public fun burn(currency: &mut Currency<BURN_ONLY_SUPPLY>, coin: Coin<BURN_ONLY_SUPPLY>) {
currency.burn(coin);
}
Uncontrolled supply (default)
If you do not call make_supply_fixed or make_supply_burn_only, the supply remains uncontrolled. The TreasuryCap holder can mint and burn freely. This is the default behavior shown in the OTW creation example above. (The standard creation example calls make_supply_burn_only, so it is not uncontrolled.)
Create a regulated currency
To create a currency with deny-list support, call make_regulated on the CurrencyInitializer before calling finalize. This returns a DenyCapV2 that you use to manage the deny list.
fun init(witness: REGCOIN_NEW, ctx: &mut TxContext) {
let (mut currency, treasury_cap) = coin_registry::new_currency_with_otw(
witness,
6, // Decimals
b"REGCOIN".to_string(), // Symbol
b"Regulated Coin".to_string(), // Name
b"Currency with DenyList Support".to_string(), // Description
b"https://example.com/regcoin.png".to_string(), // Icon URL
ctx,
);
let deny_cap = currency.make_regulated(true, ctx);
let metadata_cap = currency.finalize(ctx);
let sender = ctx.sender();
transfer::public_transfer(treasury_cap, sender);
transfer::public_transfer(metadata_cap, sender);
transfer::public_transfer(deny_cap, sender)
}
The make_regulated function takes a bool parameter controlling whether global pause is allowed. When true, the DenyCapV2 holder can freeze all transfers of the token across the network. See Regulated Tokens for details on managing the deny list.
Mint and burn coins
How you mint and burn depends on the supply model:
Uncontrolled supply: The TreasuryCap holder mints and burns directly:
// Mint new coins
let coin = treasury_cap.mint(amount, ctx);
// Burn coins
treasury_cap.burn(coin);
Burn-only supply: You cannot mint because the Currency locks the TreasuryCap. Burn through the Currency object:
currency.burn(coin);
Fixed supply: Neither minting nor burning is possible. The Currency locks the TreasuryCap and freezes the supply.
Manage metadata
The MetadataCap<T> controls who can update the currency's name, description, and icon URL. The finalize function returns it.
Claim the metadata cap
If you did not transfer the MetadataCap during initialization, or if you want to reclaim it from a shared Currency, use claim_metadata_cap:
/// Claim a `MetadataCap` for a coin type.
/// Only allowed from the owner of `TreasuryCap`, and only once.
///
/// Aborts if the `MetadataCap` has already been claimed.
/// Deleted `MetadataCap` cannot be reclaimed.
public fun claim_metadata_cap<T>(
currency: &mut Currency<T>,
_: &TreasuryCap<T>,
ctx: &mut TxContext,
): MetadataCap<T>
Update metadata
The MetadataCap holder can update the currency's display information:
// === Currency Setters ===
/// Update the name of the `Currency`.
public fun set_name<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, name: String)
/// Update the description of the `Currency`.
public fun set_description<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, description: String)
/// Update the icon URL of the `Currency`.
public fun set_icon_url<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, icon_url: String)
Freeze metadata
To make metadata permanently immutable, delete the MetadataCap:
/// Delete the metadata cap making further updates of `Currency` metadata impossible.
/// This action is IRREVERSIBLE, and the `MetadataCap` can no longer be claimed.
public fun delete_metadata_cap<T>(currency: &mut Currency<T>, cap: MetadataCap<T>)
Once deleted, no one can update the currency's name, description, or icon URL.
Complete example
The following example creates a non-OTW currency with a burn-only supply, mints the total supply, and transfers both the minted coins and the MetadataCap to the publisher:
#[allow(lint(self_transfer))]
public fun new_currency(registry: &mut CoinRegistry, ctx: &mut TxContext): Coin<MyCoin> {
let (mut currency, mut treasury_cap) = coin_registry::new_currency(
registry,
6, // Decimals
b"MyCoin".to_string(), // Symbol
b"My Coin".to_string(), // Name
b"Standard Unregulated Coin".to_string(), // Description
b"https://example.com/my_coin.png".to_string(), // Icon URL
ctx,
);
let total_supply = treasury_cap.mint(TOTAL_SUPPLY, ctx);
currency.make_supply_burn_only(treasury_cap);
let metadata_cap = currency.finalize(ctx);
transfer::public_transfer(metadata_cap, ctx.sender());
total_supply
}