Skip to main content

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. The type parameter T denominates a currency and has associated 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.

In short: the Currency<T> object holds the metadata for coin type T, and the TreasuryCap mints and burns Coin<T> objects unless the supply model locks the TreasuryCap inside the Currency (see Configure the supply model).

What is the 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.

Which creation option should I use?​

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 you publish the coin type.
  • One-Time Witness creation (coin_registry::new_currency_with_otw): Uses a One-Time Witness (OTW) 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,
}

How do I create a currency?​

To create a currency with the Currency Standard:

  1. Publish a package that defines your coin type.
  2. Call coin_registry::new_currency after publishing, or call coin_registry::new_currency_with_otw in the package's init function, to get a CurrencyInitializer<T> and a TreasuryCap<T>.
  3. Optionally, add deny-list support with make_regulated. To lock the supply, mint the total supply with the TreasuryCap, then call make_supply_fixed or make_supply_burn_only.
  4. Call finalize to complete currency creation. The finalize function returns the MetadataCap<T>.
  5. For One-Time Witness creation only, call coin_registry::finalize_registration in a second transaction to place the currency into the registry.

Use the new_currency function at any time after you publish the coin type. It creates a shared Currency<T> object, where type T must be a key-only type, meaning a struct that has only the key ability (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. Replace <CURRENCY_OBJECT_ID> with the ID of the Currency object that package publication creates, and replace <CURRENCY_TYPE> with your coin type:

# Requires the ID of the Currency object created during publishing.
# This step is only required for OTW-created currencies.
sui client ptb \
--assign currency_to_promote @<CURRENCY_OBJECT_ID> \
--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,
)

How do I configure the 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. The OTW creation example shows this default behavior. (The standard creation example calls make_supply_burn_only, so it is not uncontrolled.)

How do I 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 the currency allows global pause. 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.

How do I 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.

How do I manage currency 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>)

After you delete the MetadataCap, 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
}

This example covers creation only. To mint or burn coins after creation, see Mint and burn coins.