Skip to main content

Coin Standard

tip

The Coin and Currency standards are both used to create fungible tokens. However, they use different creation methods and store metadata in a different type of object.

Coin creates tokens using coin::create_currency while Currency uses sui::coin_registry.

Coin uses CoinMetadata while Currency uses Currency.

Fungible tokens created on Sui using the Coin or Currency standard are referred to as coins.

For fungible tokens created on Sui using the Closed-Loop Token standard, the documentation uses the term tokens. In practice, the terms for both these objects are often interchangeable.

The Coin Standard is a technical standard used by Move smart contracts for creating currency on Sui. The sui::coin module provides the logic that defines the standard.

The standardization of currency creation on Sui means that wallets, exchanges, and other smart contracts can manage currency created on Sui the same as they manage SUI, without any additional processing logic.

See Sui Tokenomics to learn more about the SUI native currency and its use on the network.

Fungible tokens

The Coin<T> type represents open-loop fungible tokens (see Token<T> for closed-loop tokens). Coins are denominated by their type parameter, T, which is also associated with metadata (like name, symbol, decimal precision, and so on) that applies to all instances of Coin<T>. The sui::coin module exposes an interface over Coin<T> that treats it as fungible, meaning that a unit of T held in one instance of Coin<T> is interchangeable with any other unit of T, much like how traditional fiat currencies operate.

Coin creation

When you create a coin using the coin::create_currency function, the publisher of the smart contract that creates the coin receives a TreasuryCap object.

Supply states

The registry supports three different supply management models:

  • Fixed supply: The total supply is permanently locked and cannot be changed.
  • Burn-only supply: Coins can be freely burned through the Currency object.
  • Uncontrolled supply: TreasuryCap holder controls minting and burning.

Regulatory states

Currencies can have different regulatory states:

  • Regulated: The currency has an associated DenyCapV2 that can restrict addresses from using it.
  • Unregulated: The currency was created without any deny list capabilities.
  • Unknown: Regulatory status is undetermined, typically from legacy migrations.

Treasury capability

danger

While TreasuryCap is freely transferable, never freeze or share the TreasuryCap. Doing so might allow malicious actors to call functions as the currency owner and the latter allows anyone to mint new coins and act as the owner of the currency.

The TreasuryCap object is required to mint new coins or to burn current ones. Consequently, only addresses that have access to this object can maintain the coin supply on the Sui network.

The TreasuryCap object is transferable, so a third party can take over the management of a coin that you create if you transfer the TreasuryCap. After transferring the capability, however, you can no longer mint and burn tokens yourself.

Regulated coins

The Coin standard includes the ability to create regulated coins. To create a regulated coin, you use the coin::create_regulated_currency_v2 function (which uses the coin::create_currency function itself), but which also returns a DenyCap capability. The DenyCap capability allows the bearer to maintain a list of addresses that cannot use the token.

tip

The regulated-coin-sample repository provides an example of regulated coin creation.

DenyList object

The list of addresses that cannot use a particular regulated coin is held within a system-created DenyList shared object. If you have access to the DenyCap, then you can use the coin::deny_list_v2_add and coin::deny_list_v2_remove functions to add and remove addresses.

Global pause switch

Regulated coin objects include an allow_global_pause Boolean field. When set to true, the bearer of the DenyCapV2 object for the coin type can use the coin::deny_list_v2_enable_global_pause function to pause coin activity indefinitely. Immediately upon the bearer initiating the pause, the network disallows the coin type as input for any transactions. At the start of the next epoch (epochs last approximately 24 hours), the network additionally disallows all addresses from receiving the coin type.

When the bearer of the DenyCapV2 object for the coin type removes the pause using coin::deny_list_v2_disable_global_pause, the coins are immediately available to use again as transaction inputs. Addresses cannot receive the coin type, however, until the following epoch.

The global pause functionality does not affect the deny list for the coin. After clearing the pause for the coin, any addresses included in the deny list are still unable to interact with the coin.

Coin metadata

Each coin you create includes metadata that describes it. Typically, smart contracts freeze this object upon creation using the transfer::public_freeze_object function because the metadata for coins should almost never change. Regulated coins freeze the metadata they create automatically.

Regular coins using the Coin standard include a CoinMetadata object. As mentioned previously, regulated coins build on top of the same procedure that creates regular coins, so they receive the same metadata object in addition to a RegulatedCoinMetadata object that includes deny list information.

Metadata fields

The fields of the metadata objects include the following:

CoinMetadata

NameDescription
idThe object ID of the metadata for the token.
decimalsThe number of decimals the token uses. If you set this field to 3, then a token of value 1000 displays as 1.000.
nameName of the coin.
symbolSymbol for the coin. This might be the same as name, but is typically fewer than 5 all capital letters. For example, SUI is the symbol for the native coin on Sui but its name is also SUI.
descriptionA short description to describe the token.
icon_urlThe URL for the token's icon, used for display in wallets, explorers, and other apps.

RegulatedCoinMetadata

NameDescription
idThe ID of the metadata object for the regulated token.
coin_metadata_objectThe ID of the underlying metadata object (CoinMetadata) for the regulated token.
deny_cap_objectThe ID of the token's DenyCapV2 object, which is necessary to maintain the deny list entries that controls who can and cannot use the token.

Minting and burning coins

The coin module provides the logic for creating and destroying coins on the Sui network (as long as you own the associated TreasuryCap). These functions are the same for all coins and each requires the TreasuryCap as an input.

Mint

Use the coin::mint function to create new tokens.

public fun mint<T>(cap: &mut TreasuryCap<T>, value: u64, ctx: &mut TxContext): Coin<T> {
Coin {
id: object::new(ctx),
balance: cap.total_supply.increase_supply(value),
}
}

The signature shows that a Coin<T> results from calling the function with a TreasuryCap, value for the coin created, and the transaction context. The function updates the total supply in TreasuryCap automatically. Upon display, the coin value respects the decimals value in the metadata. So, if you supply 1000000 as the coin value that has a decimal value of 6, the coin's value displays as 1.000000.

Burn

Use the coin::burn function to destroy current tokens.

#[allow(lint(public_entry))]
public entry fun burn<T>(cap: &mut TreasuryCap<T>, c: Coin<T>): u64 {
let Coin { id, balance } = c;
id.delete();
cap.total_supply.decrease_supply(balance)
}

The signature shows that only the TreasuryCap and coin object you want to burn are necessary inputs, returning the amount by which the supply was decreased (value of the coin). The function does not allow you to burn more coins than are available in the supply.

Adding and removing addresses to and from the deny list

The deny list is only applicable to regulated coins. As mentioned previously, when you create a regulated coin you receive a DenyCapV2 that authorizes the bearer to add and remove addresses from the system-created DenyList object. Any address on the list for your coin cannot use the coin as an input to transactions starting immediately upon being added. At the epoch that follows address addition to the deny list, the addresses additionally cannot receive the coin type. In other words, an address that gets added to the deny list for a coin type is immediately unable to send the coin. At the start of the following epoch, the address is still unable to send the coin but is also unable to receive it. From that point, the address cannot interact with the coin until expressly removed from the deny list by the DenyCapV2 bearer.

Add address to deny list

Use the coin::deny_list_v2_add function to add the provided address to the deny list for your coin. The signature for the function is:

public fun deny_list_v2_add<T>(
deny_list: &mut DenyList,
_deny_cap: &mut DenyCapV2<T>,
addr: address,
ctx: &mut TxContext,
) {
let ty = type_name::with_original_ids<T>().into_string().into_bytes();
deny_list.v2_add(DENY_LIST_COIN_INDEX, ty, addr, ctx)
}

When using this function, you provide the DenyList object (0x403), the DenyCap you receive on coin creation, the address to add to the list, and the transaction context. After using this function, the address you provide is unable to use your coin by the next epoch.

Remove address from deny list

Use the coin::deny_list_v2_remove function to remove addresses from the deny list for your coin.

public fun deny_list_v2_remove<T>(
deny_list: &mut DenyList,
_deny_cap: &mut DenyCapV2<T>,
addr: address,
ctx: &mut TxContext,
) {
let ty = type_name::with_original_ids<T>().into_string().into_bytes();
deny_list.v2_remove(DENY_LIST_COIN_INDEX, ty, addr, ctx)
}

When using this function, you provide the DenyList object (0x403), the DenyCapV2 you receive on coin creation, the address to remove from the list, and the transaction context. If you try to remove an address that isn't on the list, you receive an ENotFrozen error and the function aborts. After calling this function, the address you provide is able to use your coin by the next epoch.

Using an SDK

You can use either the TypeScript or Rust SDK to manipulate the addresses held in the DenyList for your coin. The following examples are based on the regulated coin sample.

const tx = new Transaction();

tx.moveCall({
target: `0x2::coin::deny_list_v2_add`,
arguments: [
tx.object(<SUI-DENY-LIST-OBJECT-ID>),
tx.object(<DENY-CAP-ID>),
tx.pure.address(options.address),
],
typeArguments: [<COIN-TYPE>],
});
  • <SUI-DENY-LIST-OBJECT-ID> is "0x403".
  • <DENY-CAP-ID> is the object of type DenyCapV2<REGULATED_COIN> you receive from publishing the contract.
  • options.address is the address to ban.
  • <COIN-TYPE> is ${PACKAGE-ID}::${MODULE-NAME}::${COIN-NAME}, which is ${PACKAGE-ID}::regulated_coin::REGULATED_COIN based on the example.

Globally pausing and unpausing regulated coin activity

Globally pausing coin activity is only applicable to regulated coin types.

Pause coin activity

To pause activity across the network for a regulated coin type with the allow_global_pause field set to true, use coin::deny_list_v2_enable_global_pause. You must provide the DenyCapV2 object for the coin type to initiate the pause. Transaction activity is paused immediately, and no addresses can receive the coin in the epoch that follows the call to pause.

#[allow(unused_mut_parameter)]
public fun deny_list_v2_enable_global_pause<T>(
deny_list: &mut DenyList,
deny_cap: &mut DenyCapV2<T>,
ctx: &mut TxContext,
)

Unpause coin activity

To restart network activity for a paused regulated coin, use the coin::deny_list_v2_disable_global_pause function. As with pausing, you must provide the DenyCapV2 object for the coin type. Transaction activity resumes immediately, and addresses can begin receiving the coin in the epoch that follows the call to remove the pause.

#[allow(unused_mut_parameter)]
public fun deny_list_v2_disable_global_pause<T>(
deny_list: &mut DenyList,
deny_cap: &mut DenyCapV2<T>,
ctx: &mut TxContext,
)

Reading coin data

Metadata

danger

CoinMetadata is planned for deprecation. The Coin standard itself is not.

You can use the following functions to retrieve data from coins.

Use the following functions to get the values for the respective fields on the metadata object for coins.

FunctionSignature
get_decimalspublic fun get_decimals<T>(metadata: &coin::CoinMetadata<T>): u8
get_namepublic fun get_name<T>(metadata: &coin::CoinMetadata<T>): string::String
get_symbolpublic fun get_symbol<T>(metadata: &coin::CoinMetadata<T>): ascii::String
get_descriptionpublic fun get_description<T>(metadata: &coin::CoinMetadata<T>): string::String
get_icon_urlpublic fun get_icon_url<T>(metadata: &coin::CoinMetadata<T>): option::Option<url::Url>

Total supply

Use the coin::supply function to get the current supply of a given coin.

Update coin metadata

If the CoinMetadata object was not frozen upon creation, you can use the following functions to update its values.

Each function signature is similar. Replace <FUNCTION-NAME> and <ATTRIBUTE-TYPE> with the values defined in the table to get the signature of each function:

public entry fun <FUNCTION-NAME><T>(
_treasury: &coin::TreasuryCap<T>,
metadata: &mut coin::CoinMetadata<T>,
<ATTRIBUTE-TYPE>
)
<FUNCTION-NAME><ATTRIBUTE-TYPE>
update_namename: string::String
update_symbolsymbol: ascii::String
update_descriptiondescription: string::String
update_icon_urlurl: ascii::String
info

RegulatedCoinMetadata is frozen upon creation, so there are no functions to update its data.

Migration from Coin to Currency Standard

Sui provides a migration path from the CoinMetadata<T> system while maintaining backward compatibility.

The migration system is designed with specific constraints to maintain data integrity and preserve existing functionality. Migration can only occur permissionlessly when done by reference, meaning the original CoinMetadata object remains intact while its data is copied to create a new Currency entry in the registry. This approach allows for safe registration of new currency data and updates to existing currency data, but only as long as the MetadataCap has not yet been claimed.

The system cannot allow permissionless migration by value, however, where the original CoinMetadata object would be consumed or destroyed during migration. This restriction exists because some coins have governance mechanisms that control CoinMetadata updates. Allowing value-based migration would irreversibly break those existing governance workflows by destroying the metadata objects that governance systems expect to manage.

The destruction of legacy CoinMetadata objects is only permitted after the corresponding MetadataCap has been claimed, serving as proof that the currency's owner has taken control through the new registry system. This ensures that legacy metadata cannot be accidentally destroyed while governance systems still depend on it, and provides a clear transition path where owners must explicitly claim control before legacy objects can be cleaned up.

This design preserves backward compatibility while enabling a smooth transition to the centralized registry system, protecting existing governance mechanisms until owners are ready to migrate fully to the new system.

Some of the benefits to migrate to the Coin Registry include:

  • Centralized management: Single source of truth for all coin information.
  • Enhanced features: Access to advanced supply models and regulatory tracking.
  • Ecosystem integration: Better support for wallets, exchanges, and apps.
  • Future-proofing: Access to ongoing registry enhancements.

Migration process

  1. Metadata migration: Use migrate_legacy_metadata<T>() to create a new Currency<T> entry based on existing CoinMetadata<T> information.

    public fun migrate_legacy_metadata<T>(
    registry: &mut CoinRegistry,
    legacy: &CoinMetadata<T>,
    _ctx: &mut TxContext,
    )
  2. Regulatory migration: For coins with deny list capabilities, use:

    • migrate_regulated_state_by_metadata<T>(): Migrate based on existing metadata.

      public fun migrate_regulated_state_by_metadata<T>(
      currency: &mut Currency<T>,
      metadata: &RegulatedCoinMetadata<T>,
      )
    • migrate_regulated_state_by_cap<T>(): Migrate based on deny capability.

      public fun migrate_regulated_state_by_cap<T>(currency: &mut Currency<T>, cap: &DenyCapV2<T>)

Migration function mappings

Update smart contract logic that relies on the coin module to use the coin_registry module instead:

  • coin::create_currency -> coin_registry::new_currency_with_otw
  • coin::create_regulated_currency_v2 -> coin_registry::new_currency_with_otw
public fun new_currency_with_otw<T: drop>(
otw: T,
decimals: u8,
symbol: String,
name: String,
description: String,
icon_url: String,
ctx: &mut TxContext,
): (CurrencyInitializer<T>, TreasuryCap<T>)