Skip to main content

Currency Standard

The Currency Standard is a technical standard used by Move smart contracts for creating currency on Sui. The sui::coin_registry module provides the logic that defines the standard, providing unified metadata, enhanced supply tracking, and improved regulatory features.

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.

Currency on Sui can offer specialized abilities while following the Currency Standard. For example, you can create regulated currency that allows its creator to add specific addresses to a deny list, so that the identified addresses cannot use the currency as inputs to transactions. Both legacy and registry systems support regulated currency with varying levels of sophistication.

Fungible tokens

On Sui, the Currency<T> type represents open-loop fungible tokens (see Token<T> for closed-loop tokens). Currencies 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 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, much like how traditional fiat currencies operate.

Currency creation

The Coin Registry is a centralized system that provides unified currency management through the sui::coin_registry module.

info

The registry is a system-level shared object located at address 0xc.

Click to open

sui::coin_registry module

/// Defines the system object for managing coin data in a central
/// registry. This module provides a centralized way to store and manage
/// metadata for all currencies in the Sui ecosystem, including their
/// supply information, regulatory status, and metadata capabilities.
module sui::coin_registry;

use std::ascii;
use std::string::String;
use std::type_name::TypeName;
use sui::bag::{Self, Bag};
use sui::balance::{Supply, Balance};
use sui::coin::{Self, TreasuryCap, DenyCapV2, CoinMetadata, RegulatedCoinMetadata, Coin};
use sui::derived_object;
use sui::transfer::Receiving;
use sui::vec_map::{Self, VecMap};

/// Metadata cap already claimed
#[error(code = 0)]
const EMetadataCapAlreadyClaimed: vector<u8> = b"Metadata cap already claimed.";
/// Only the system address can create the registry
#[error(code = 1)]
const ENotSystemAddress: vector<u8> = b"Only the system can create the registry.";
/// Currency for this coin type already exists
#[error(code = 2)]
const ECurrencyAlreadyExists: vector<u8> = b"Currency for this coin type already exists.";
/// Attempt to set the deny list state permissionlessly while it has already been set.
#[error(code = 3)]
const EDenyListStateAlreadySet: vector<u8> =
b"Cannot set the deny list state as it has already been set.";
#[error(code = 4)]
const EMetadataCapNotClaimed: vector<u8> =
b"Cannot delete legacy metadata before claiming the `MetadataCap`.";
/// Attempt to update `Currency` with legacy metadata after the `MetadataCap` has
/// been claimed. Updates are only allowed if the `MetadataCap` has not yet been
/// claimed or deleted.
#[error(code = 5)]
const ECannotUpdateManagedMetadata: vector<u8> =
b"Cannot update metadata whose `MetadataCap` has already been claimed.";
/// Attempt to set the symbol to a non-ASCII printable character
#[error(code = 6)]
const EInvalidSymbol: vector<u8> = b"Symbol has to be ASCII printable.";
#[error(code = 7)]
const EDenyCapAlreadyCreated: vector<u8> = b"Cannot claim the deny cap twice.";
/// Attempt to migrate legacy metadata for a `Currency` that already exists.
#[error(code = 8)]
const ECurrencyAlreadyRegistered: vector<u8> = b"Currency already registered.";
#[error(code = 9)]
const EEmptySupply: vector<u8> = b"Supply cannot be empty.";
#[error(code = 10)]
const ESupplyNotBurnOnly: vector<u8> = b"Cannot burn on a non burn-only supply.";
#[error(code = 11)]
const EInvariantViolation: vector<u8> = b"Code invariant violation.";

/// Incremental identifier for regulated coin versions in the deny list.
/// We start from `0` in the new system, which aligns with the state of `DenyCapV2`.
const REGULATED_COIN_VERSION: u8 = 0;

/// System object found at address `0xc` that stores coin data for all
/// registered coin types. This is a shared object that acts as a central
/// registry for coin metadata, supply information, and regulatory status.
public struct CoinRegistry has key {
id: UID,
}

/// Store only object that enables more flexible coin data
/// registration, allowing for additional fields to be added
/// without changing the `Currency` structure.
#[allow(unused_field)]
public struct ExtraField(TypeName, vector<u8>) has store;

/// Key used to derive addresses when creating `Currency<T>` objects.
public struct CurrencyKey<phantom T>() has copy, drop, store;

/// Capability object that gates metadata (name, description, icon_url, symbol)
/// changes in the `Currency`. It can only be created (or claimed) once, and can
/// be deleted to prevent changes to the `Currency` metadata.
public struct MetadataCap<phantom T> has key, store { id: UID }

/// Currency stores metadata such as name, symbol, decimals, icon_url and description,
/// as well as supply states (optional) and regulatory status.
public struct Currency<phantom T> has key {
id: UID,
/// Number of decimal places the coin uses for display purposes.
decimals: u8,
/// Human-readable name for the coin.
name: String,
/// Short symbol/ticker for the coin.
symbol: String,
/// Detailed description of the coin.
description: String,
/// URL for the coin's icon/logo.
icon_url: String,
/// Current supply state of the coin (fixed supply or unknown)
/// Note: We're using `Option` because `SupplyState` does not have drop,
/// meaning we cannot swap out its value at a later state.
supply: Option<SupplyState<T>>,
/// Regulatory status of the coin (regulated with deny cap or unknown)
regulated: RegulatedState,
/// ID of the treasury cap for this coin type, if registered.
treasury_cap_id: Option<ID>,
/// ID of the metadata capability for this coin type, if claimed.
metadata_cap_id: MetadataCapState,
/// Additional fields for extensibility.
extra_fields: VecMap<String, ExtraField>,
}

/// Supply state marks the type of Currency Supply, which can be
/// - Fixed: no minting or burning;
/// - BurnOnly: no minting, burning is allowed;
/// - Unknown: flexible (supply is controlled by its `TreasuryCap`);
public enum SupplyState<phantom T> has store {
/// Coin has a fixed supply with the given Supply object.
Fixed(Supply<T>),
/// Coin has a supply that can ONLY decrease.
BurnOnly(Supply<T>),
/// Supply information is not yet known or registered.
Unknown,
}

/// Regulated state of a coin type.
/// - Regulated: `DenyCap` exists or a `RegulatedCoinMetadata` used to mark currency as regulated;
/// - Unregulated: the currency was created without deny list;
/// - Unknown: the regulatory status is unknown.
public enum RegulatedState has copy, drop, store {
/// Coin is regulated with a deny cap for address restrictions.
/// `allow_global_pause` is `None` if the information is unknown (has not been migrated from `DenyCapV2`).
Regulated { cap: ID, allow_global_pause: Option<bool>, variant: u8 },
/// The coin has been created without deny list.
Unregulated,
/// Regulatory status is unknown.
/// Result of a legacy migration for that coin (from `coin.move` constructors)
Unknown,
}

/// State of the `MetadataCap` for a single `Currency`.
public enum MetadataCapState has copy, drop, store {
/// The metadata cap has been claimed.
Claimed(ID),
/// The metadata cap has not been claimed.
Unclaimed,
/// The metadata cap has been claimed and then deleted.
Deleted,
}

/// 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,
}

/// Creates a new currency.
///
/// Note: This constructor has no long term difference from `new_currency_with_otw`.
/// This can be called from the module that defines `T` any time after it has been published.
public fun new_currency<T: /* internal */ key>(
registry: &mut CoinRegistry,
decimals: u8,
symbol: String,
name: String,
description: String,
icon_url: String,
ctx: &mut TxContext,
): (CurrencyInitializer<T>, TreasuryCap<T>) {
assert!(!registry.exists<T>(), ECurrencyAlreadyExists);
assert!(is_ascii_printable!(&symbol), EInvalidSymbol);

let treasury_cap = coin::new_treasury_cap(ctx);
let currency = Currency<T> {
id: derived_object::claim(&mut registry.id, CurrencyKey<T>()),
decimals,
name,
symbol,
description,
icon_url,
supply: option::some(SupplyState::Unknown),
regulated: RegulatedState::Unregulated,
treasury_cap_id: option::some(object::id(&treasury_cap)),
metadata_cap_id: MetadataCapState::Unclaimed,
extra_fields: vec_map::empty(),
};

(CurrencyInitializer { currency, is_otw: false, extra_fields: bag::new(ctx) }, treasury_cap)
}

/// Creates a new currency with using an OTW as proof of uniqueness.
///
/// This is a two-step operation:
/// 1. `Currency` is constructed in the `init` function and sent to the `CoinRegistry`;
/// 2. `Currency` is promoted to a shared object in the `finalize_registration` call;
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>) {
assert!(sui::types::is_one_time_witness(&otw));
assert!(is_ascii_printable!(&symbol), EInvalidSymbol);

let treasury_cap = coin::new_treasury_cap(ctx);
let currency = Currency<T> {
id: object::new(ctx),
decimals,
name,
symbol,
description,
icon_url,
supply: option::some(SupplyState::Unknown),
regulated: RegulatedState::Unregulated,
treasury_cap_id: option::some(object::id(&treasury_cap)),
metadata_cap_id: MetadataCapState::Unclaimed,
extra_fields: vec_map::empty(),
};

(CurrencyInitializer { currency, is_otw: true, extra_fields: bag::new(ctx) }, treasury_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> {
assert!(!currency.is_metadata_cap_claimed(), EMetadataCapAlreadyClaimed);
let id = object::new(ctx);
currency.metadata_cap_id = MetadataCapState::Claimed(id.to_inner());

MetadataCap { id }
}

// === Currency Initialization ===

/// Allows converting a currency, on init, to regulated, which creates
/// a `DenyCapV2` object, and a denylist entry. Sets regulated state to
/// `Regulated`.
///
/// This action is irreversible.
public fun make_regulated<T>(
init: &mut CurrencyInitializer<T>,
allow_global_pause: bool,
ctx: &mut TxContext,
): DenyCapV2<T> {
assert!(init.currency.regulated == RegulatedState::Unregulated, EDenyCapAlreadyCreated);
let deny_cap = coin::new_deny_cap_v2<T>(allow_global_pause, ctx);
init.currency.regulated =
RegulatedState::Regulated {
cap: object::id(&deny_cap),
allow_global_pause: option::some(allow_global_pause),
variant: REGULATED_COIN_VERSION,
};

deny_cap
}

public use fun make_supply_fixed_init as CurrencyInitializer.make_supply_fixed;

/// Initializer function to make the supply fixed.
/// Aborts if Supply is `0` to enforce minting during initialization.
public fun make_supply_fixed_init<T>(init: &mut CurrencyInitializer<T>, cap: TreasuryCap<T>) {
assert!(cap.total_supply() > 0, EEmptySupply);
init.currency.make_supply_fixed(cap)
}

public use fun make_supply_burn_only_init as CurrencyInitializer.make_supply_burn_only;

/// Initializer function to make the supply burn-only.
/// Aborts if Supply is `0` to enforce minting during initialization.
public fun make_supply_burn_only_init<T>(init: &mut CurrencyInitializer<T>, cap: TreasuryCap<T>) {
assert!(cap.total_supply() > 0, EEmptySupply);
init.currency.make_supply_burn_only(cap)
}

/// Freeze the supply by destroying the `TreasuryCap` and storing it in the `Currency`.
public fun make_supply_fixed<T>(currency: &mut Currency<T>, cap: TreasuryCap<T>) {
match (currency.supply.swap(SupplyState::Fixed(cap.into_supply()))) {
// Impossible: We cannot fix a supply or make a supply burn-only twice.
SupplyState::Fixed(_supply) | SupplyState::BurnOnly(_supply) => abort EInvariantViolation,
// We replaced "unknown" with fixed supply.
SupplyState::Unknown => (),
};
}

/// Make the supply `BurnOnly` by giving up the `TreasuryCap`, and allowing
/// burning of Coins through the `Currency`.
public fun make_supply_burn_only<T>(currency: &mut Currency<T>, cap: TreasuryCap<T>) {
match (currency.supply.swap(SupplyState::BurnOnly(cap.into_supply()))) {
// Impossible: We cannot fix a supply or make a supply burn-only twice.
SupplyState::Fixed(_supply) | SupplyState::BurnOnly(_supply) => abort EInvariantViolation,
// We replaced "unknown" with frozen supply.
SupplyState::Unknown => (),
};
}

#[allow(lint(share_owned))]
/// Finalize the coin initialization, returning `MetadataCap`
public fun finalize<T>(builder: CurrencyInitializer<T>, ctx: &mut TxContext): MetadataCap<T> {
let CurrencyInitializer { mut currency, is_otw, extra_fields } = builder;
extra_fields.destroy_empty();
let id = object::new(ctx);
currency.metadata_cap_id = MetadataCapState::Claimed(id.to_inner());

if (is_otw) transfer::transfer(currency, object::sui_coin_registry_address())
else transfer::share_object(currency);

MetadataCap<T> { id }
}

/// 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,
) {
// 1. Consume Currency
// 2. Re-create it with a "derived" address.
let Currency {
id,
decimals,
name,
symbol,
description,
icon_url,
supply,
regulated,
treasury_cap_id,
metadata_cap_id,
extra_fields,
} = transfer::receive(&mut registry.id, currency);
id.delete();
// Now, create the derived version of the coin currency.
transfer::share_object(Currency {
id: derived_object::claim(&mut registry.id, CurrencyKey<T>()),
decimals,
name,
symbol,
description,
icon_url,
supply,
regulated,
treasury_cap_id,
metadata_cap_id,
extra_fields,
})
}

/// 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>) {
let MetadataCap { id } = cap;
currency.metadata_cap_id = MetadataCapState::Deleted;
id.delete();
}

/// Burn the `Coin` if the `Currency` has a `BurnOnly` supply state.
public fun burn<T>(currency: &mut Currency<T>, coin: Coin<T>) {
currency.burn_balance(coin.into_balance());
}

/// Burn the `Balance` if the `Currency` has a `BurnOnly` supply state.
public fun burn_balance<T>(currency: &mut Currency<T>, balance: Balance<T>) {
assert!(currency.is_supply_burn_only(), ESupplyNotBurnOnly);
match (currency.supply.borrow_mut()) {
SupplyState::BurnOnly(supply) => { supply.decrease_supply(balance); },
_ => abort EInvariantViolation, // unreachable
}
}

// === Currency Setters ===

/// Update the name of the `Currency`.
public fun set_name<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, name: String) {
currency.name = name;
}

#[test_only]
/// Update the symbol of the `Currency`.
public fun set_symbol<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, symbol: String) {
assert!(is_ascii_printable!(&symbol), EInvalidSymbol);
currency.symbol = symbol;
}

/// Update the description of the `Currency`.
public fun set_description<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, description: String) {
currency.description = description;
}

/// Update the icon URL of the `Currency`.
public fun set_icon_url<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, icon_url: String) {
currency.icon_url = icon_url;
}

/// Register the treasury cap ID for a migrated `Currency`. All currencies created with
/// `new_currency` or `new_currency_with_otw` have their treasury cap ID set during
/// initialization.
public fun set_treasury_cap_id<T>(currency: &mut Currency<T>, cap: &TreasuryCap<T>) {
currency.treasury_cap_id.fill(object::id(cap));
}

// == Migrations from legacy coin flows ==

/// Register `CoinMetadata` in the `CoinRegistry`. This can happen only once, if the
/// `Currency` did not exist yet. Further updates are possible through
/// `update_from_legacy_metadata`.
public fun migrate_legacy_metadata<T>(
registry: &mut CoinRegistry,
legacy: &CoinMetadata<T>,
_ctx: &mut TxContext,
) {
assert!(!registry.exists<T>(), ECurrencyAlreadyRegistered);
assert!(is_ascii_printable!(&legacy.get_symbol().to_string()), EInvalidSymbol);

transfer::share_object(Currency<T> {
id: derived_object::claim(&mut registry.id, CurrencyKey<T>()),
decimals: legacy.get_decimals(),
name: legacy.get_name(),
symbol: legacy.get_symbol().to_string(),
description: legacy.get_description(),
icon_url: legacy
.get_icon_url()
.map!(|url| url.inner_url().to_string())
.destroy_or!(b"".to_string()),
supply: option::some(SupplyState::Unknown),
regulated: RegulatedState::Unknown, // We don't know if it's regulated or not!
treasury_cap_id: option::none(),
metadata_cap_id: MetadataCapState::Unclaimed,
extra_fields: vec_map::empty(),
});
}

/// Update `Currency` from `CoinMetadata` if the `MetadataCap` is not claimed. After
/// the `MetadataCap` is claimed, updates can only be made through `set_*` functions.
public fun update_from_legacy_metadata<T>(currency: &mut Currency<T>, legacy: &CoinMetadata<T>) {
assert!(!currency.is_metadata_cap_claimed(), ECannotUpdateManagedMetadata);

currency.name = legacy.get_name();
currency.symbol = legacy.get_symbol().to_string();
currency.description = legacy.get_description();
currency.decimals = legacy.get_decimals();
currency.icon_url =
legacy.get_icon_url().map!(|url| url.inner_url().to_string()).destroy_or!(b"".to_string());
}

/// Delete the legacy `CoinMetadata` object if the metadata cap for the new registry
/// has already been claimed.
///
/// This function is only callable after there's "proof" that the author of the coin
/// can manage the metadata using the registry system (so having a metadata cap claimed).
public fun delete_migrated_legacy_metadata<T>(currency: &mut Currency<T>, legacy: CoinMetadata<T>) {
assert!(currency.is_metadata_cap_claimed(), EMetadataCapNotClaimed);
legacy.destroy_metadata();
}

/// Allow migrating the regulated state by access to `RegulatedCoinMetadata` frozen object.
/// This is a permissionless operation which can be performed only once.
public fun migrate_regulated_state_by_metadata<T>(
currency: &mut Currency<T>,
metadata: &RegulatedCoinMetadata<T>,
) {
// Only allow if this hasn't been migrated before.
assert!(currency.regulated == RegulatedState::Unknown, EDenyListStateAlreadySet);
currency.regulated =
RegulatedState::Regulated {
cap: metadata.deny_cap_id(),
allow_global_pause: option::none(),
variant: REGULATED_COIN_VERSION,
};
}

/// Mark regulated state by showing the `DenyCapV2` object for the `Currency`.
public fun migrate_regulated_state_by_cap<T>(currency: &mut Currency<T>, cap: &DenyCapV2<T>) {
currency.regulated =
RegulatedState::Regulated {
cap: object::id(cap),
allow_global_pause: option::some(cap.allow_global_pause()),
variant: REGULATED_COIN_VERSION,
};
}

// === Public getters ===

/// Get the number of decimal places for the coin type.
public fun decimals<T>(currency: &Currency<T>): u8 { currency.decimals }

/// Get the human-readable name of the coin.
public fun name<T>(currency: &Currency<T>): String { currency.name }

/// Get the symbol/ticker of the coin.
public fun symbol<T>(currency: &Currency<T>): String { currency.symbol }

/// Get the description of the coin.
public fun description<T>(currency: &Currency<T>): String { currency.description }

/// Get the icon URL for the coin.
public fun icon_url<T>(currency: &Currency<T>): String { currency.icon_url }

/// Check if the metadata capability has been claimed for this `Currency` type.
public fun is_metadata_cap_claimed<T>(currency: &Currency<T>): bool {
match (currency.metadata_cap_id) {
MetadataCapState::Claimed(_) | MetadataCapState::Deleted => true,
_ => false,
}
}

/// Check if the metadata capability has been deleted for this `Currency` type.
public fun is_metadata_cap_deleted<T>(currency: &Currency<T>): bool {
match (currency.metadata_cap_id) {
MetadataCapState::Deleted => true,
_ => false,
}
}

/// Get the metadata cap ID, or none if it has not been claimed.
public fun metadata_cap_id<T>(currency: &Currency<T>): Option<ID> {
match (currency.metadata_cap_id) {
MetadataCapState::Claimed(id) => option::some(id),
_ => option::none(),
}
}

/// Get the treasury cap ID for this coin type, if registered.
public fun treasury_cap_id<T>(currency: &Currency<T>): Option<ID> {
currency.treasury_cap_id
}

/// Get the deny cap ID for this coin type, if it's a regulated coin.
/// Returns `None` if:
/// - The `Currency` is not regulated;
/// - The `Currency` is migrated from legacy, and its regulated state has not been set;
public fun deny_cap_id<T>(currency: &Currency<T>): Option<ID> {
match (currency.regulated) {
RegulatedState::Regulated { cap, .. } => option::some(cap),
RegulatedState::Unregulated | RegulatedState::Unknown => option::none(),
}
}

/// Check if the supply is fixed.
public fun is_supply_fixed<T>(currency: &Currency<T>): bool {
match (currency.supply.borrow()) {
SupplyState::Fixed(_) => true,
_ => false,
}
}

/// Check if the supply is burn-only.
public fun is_supply_burn_only<T>(currency: &Currency<T>): bool {
match (currency.supply.borrow()) {
SupplyState::BurnOnly(_) => true,
_ => false,
}
}

/// Check if the currency is regulated.
public fun is_regulated<T>(currency: &Currency<T>): bool {
match (currency.regulated) {
RegulatedState::Regulated { .. } => true,
_ => false,
}
}

/// Get the total supply for the `Currency<T>` if the Supply is in fixed or
/// burn-only state. Returns `None` if the SupplyState is Unknown.
public fun total_supply<T>(currency: &Currency<T>): Option<u64> {
match (currency.supply.borrow()) {
SupplyState::Fixed(supply) => option::some(supply.value()),
SupplyState::BurnOnly(supply) => option::some(supply.value()),
SupplyState::Unknown => option::none(),
}
}

/// Check if coin data exists for the given type T in the registry.
public fun exists<T>(registry: &CoinRegistry): bool {
derived_object::exists(&registry.id, CurrencyKey<T>())
}

#[allow(unused_function)]
/// Create and share the singleton `CoinRegistry` -- this function is
/// called exactly once, during the upgrade epoch.
/// Only the system address (0x0) can create the registry.
fun create(ctx: &TxContext) {
assert!(ctx.sender() == @0x0, ENotSystemAddress);

transfer::share_object(CoinRegistry {
id: object::sui_coin_registry_object_id(),
});
}

/// Nit: consider adding this function to `std::string` in the future.
macro fun is_ascii_printable($s: &String): bool {
let s = $s;
s.as_bytes().all!(|b| ascii::is_printable_char(*b))
}

#[test_only]
/// Create a coin data registry for testing purposes.
/// This function is test-only and should only be used in tests.
public fun create_coin_data_registry_for_testing(ctx: &mut TxContext): CoinRegistry {
assert!(ctx.sender() == @0x0, ENotSystemAddress);

CoinRegistry {
id: object::new(ctx),
}
}

#[test_only]
/// Unwrap CurrencyInitializer for testing purposes.
/// This function is test-only and should only be used in tests.
public fun unwrap_for_testing<T>(init: CurrencyInitializer<T>): Currency<T> {
let CurrencyInitializer { currency, extra_fields, .. } = init;
extra_fields.destroy_empty();
currency
}

#[test_only]
public fun finalize_unwrap_for_testing<T>(
init: CurrencyInitializer<T>,
ctx: &mut TxContext,
): (Currency<T>, MetadataCap<T>) {
let CurrencyInitializer { mut currency, extra_fields, .. } = init;
extra_fields.destroy_empty();
let id = object::new(ctx);
currency.metadata_cap_id = MetadataCapState::Claimed(id.to_inner());
(currency, MetadataCap { id })
}

#[test_only]
public fun migrate_legacy_metadata_for_testing<T>(
registry: &mut CoinRegistry,
legacy: &CoinMetadata<T>,
_ctx: &mut TxContext,
): Currency<T> {
assert!(!registry.exists<T>(), ECurrencyAlreadyRegistered);

Currency<T> {
id: derived_object::claim(&mut registry.id, CurrencyKey<T>()),
decimals: legacy.get_decimals(),
name: legacy.get_name(),
symbol: legacy.get_symbol().to_string(),
description: legacy.get_description(),
icon_url: legacy
.get_icon_url()
.map!(|url| url.inner_url().to_string())
.destroy_or!(b"".to_string()),
supply: option::some(SupplyState::Unknown),
regulated: RegulatedState::Unknown,
treasury_cap_id: option::none(),
metadata_cap_id: MetadataCapState::Unclaimed,
extra_fields: vec_map::empty(),
}
}

Core components

CoinRegistry: The main system object that coordinates all currency-related operations. This shared object serves as the entry point for all registry operations and is created once during network initialization. It's address is 0xc.

Currency<T>: The core of the registry system, storing comprehensive information about each coin type including:

  • Metadata management: Basic currency information like name, symbol, decimals, description, and icon URL.
  • Supply tracking: Maintains supply state information (fixed, burn-only, or unknown).
  • Regulatory status: Tracks whether the coin is regulated with deny list capabilities.
  • Capability references: Links to TreasuryCaps and MetadataCaps for the coin type.
  • Extensibility: Includes extra fields for future enhancements.

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: New coins cannot be minted, but existing coins can be burned.
  • 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.

Creation options

The registry supports two different currency creation flows:

  • Standard creation (recommended):

    Use the new_currency function at any time after the currency type is published. The function immediately creates a shared Currency<T> object. The type T must be a key-only type, as in public struct MyCoin has key { id: UID }.

    public fun new_currency<T: /* internal */ key>(
    registry: &mut CoinRegistry,
    decimals: u8,
    symbol: String,
    name: String,
    description: String,
    icon_url: String,
    ctx: &mut TxContext,
    ): (CurrencyInitializer<T>, TreasuryCap<T>)
  • One-Time Witness (OTW) creation:

    caution

    OTW creation of a new currency is a two-step process. The initialization process begins with package publication. Then, a call to finalize_registration is needed to place the currency into the registry.

    The new_currency_with_otw function uses an OTW for uniqueness proof. See One-Time Witness in The Move Book for more information.

    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>)

Currency initialization

Both creation methods return a CurrencyInitializer<T> that allows for additional configuration:

  • Make regulated: Add deny list capabilities.
  • Set supply model: Choose between fixed, burn-only, or flexible supply.
  • Add 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,
}

Currency finalization

A currency is not complete until after you call the finalize function. The function returns the MetadataCap<T> for metadata management. If you used an OTW to create the currency, the function sends the currency to the coin registry; otherwise, it creates the final shared Currency<T> object.

#[allow(lint(share_owned))]
/// Finalize the coin initialization, returning `MetadataCap`
public fun finalize<T>(builder: CurrencyInitializer<T>, ctx: &mut TxContext): MetadataCap<T>

If you publish a package that uses an OTW to create a currency in the package init function, then a call must be made to finalize_registration after initialization. The function transforms the currency into a shared object.

/// 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,
)

Regulated currency

The Currency Standard supports creating regulated coins. Use the make_regulated function during the initialization phase before calling finalize. The function adds deny list capabilities to the Currency<T> and tracks the regulatory status within the Coin Registry. The function returns a DenyCap that allows the bearer to maintain the list of addresses on the deny list.

Click to open

Regulated coin example

module examples::regcoin_new;

use sui::coin::{Self, DenyCapV2};
use sui::coin_registry;
use sui::deny_list::DenyList;

public struct REGCOIN_NEW has drop {}

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,
);

// Claim `DenyCapV2` and mark currency as regulated.
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)
}
public fun add_addr_from_deny_list(
denylist: &mut DenyList,
denycap: &mut DenyCapV2<REGCOIN_NEW>,
denyaddy: address,
ctx: &mut TxContext,
) {
coin::deny_list_v2_add(denylist, denycap, denyaddy, ctx);
}

public fun remove_addr_from_deny_list(
denylist: &mut DenyList,
denycap: &mut DenyCapV2<REGCOIN_NEW>,
denyaddy: address,
ctx: &mut TxContext,
) {
coin::deny_list_v2_remove(denylist, denycap, denyaddy, ctx);
}

DenyList object

A DenyList shared object created by the system contains addresses that aren't able to use a particular regulated currency. If you have access to the DenyCap, then you can use the sui::coin module to maintain the list of users. Use the functions coin::deny_list_v2_add and coin::deny_list_v2_remove functions to add and remove addresses.

Global pause switch

Regulated currencies include an allow_global_pause Boolean field. When set to true, the bearer of the DenyCapV2 object for the currency 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 ~24 hours), the network additionally disallows all addresses from receiving the coin type.

When the bearer of the DenyCapV2 object for the currency 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 currency. After clearing the pause for the currency, any addresses included in the deny list are still unable to interact with the currency's coin.

TreasuryCap

When you create a currency using the coin_registry::new_currency or coin_registry::new_currency_with_otw functions, the publisher of the smart contract that creates the coin receives a TreasuryCap object upon finalization of the currency. The TreasuryCap object is required to mint new currency coins or to burn current ones (depending on currency supply state). Consequently, only addresses that have access to this object are able to maintain the currency supply on the Sui network.

The TreasuryCap object is transferable, so a third party can take over the management of a currency that you create if you transfer the TreasuryCap to them. After transferring the object, you are no longer able to mint and burn coins yourself.

Currency metadata

Currency metadata is stored centrally within the Currency<T> object in the registry. Metadata updates are controlled by the MetadataCap<T> capability, which can be:

  • Unclaimed: MetadataCap has not yet been claimed.
  • Claimed: MetadataCap has been claimed and can be used for updates.
  • Deleted: MetadataCap has been permanently deleted, preventing future updates.

Metadata fields

The fields of the metadata objects include the following:

CoinMetadata

NameDescription
registryThe CoinRegistry system object ID (0x3).
decimalsThe number of decimals the token uses. If you set this field to 3, then a token of value 1000 would display as 1.000.
nameName of the coin.
symbolSymbol for the coin. This might be the same value as name, but is typically fewer than five letters, all capitals. For example, SUI is the symbol for the native coin on Sui but its name is also SUI. Only ASCII printable characters allowed.
descriptionA short description to describe the token.
icon_urlRequired field. The URL for the token's icon, used for display in wallets, explorers, and other apps.

Minting and burning coins

The Coin Registry supports advanced supply management models:

  • Unknown supply: Traditional minting/burning via TreasuryCap.
  • Fixed supply: Total supply is permanently locked, no minting or burning allowed.
  • Burn-only supply: No new minting allowed, but existing coins can be burned through registry functions.

Mint

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

public fun mint<T>(cap: &mut TreasuryCap<T>, value: u64, ctx: &mut TxContext): Coin<T>

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

For currencies with a burn-only supply status, coins can be burned directly through the registry using coin_registry::burn and coin_registry::burn_balance functions without requiring the TreasuryCap.

public fun burn<T>(currency: &mut Currency<T>, coin: Coin<T>)

public fun burn_balance<T>(currency: &mut Currency<T>, balance: Balance<T>)

For currencies with an unknown supply status, use the coin::burn function to destroy current coins.

#[allow(lint(public_entry))]
public entry fun burn<T>(cap: &mut TreasuryCap<T>, c: Coin<T>): u64

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 currencies. When you create a regulated currency, 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 currency is unable to use its coins 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 currency 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 currency 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,
)

Registry query functions

You can query information directly from the Currency<T> object.

Basic metadata

  • decimals<T>(): Get decimal precision.

    public fun decimals<T>(currency: &Currency<T>): u8
  • name<T>(): Get coin name.

    public fun name<T>(currency: &Currency<T>): String
  • symbol<T>(): Get coin symbol.

    public fun symbol<T>(currency: &Currency<T>): String
  • description<T>(): Get coin description.

    public fun description<T>(currency: &Currency<T>): String
  • icon_url<T>(): Get icon URL.

    public fun icon_url<T>(currency: &Currency<T>): String

Supply information

  • total_supply<T>(): Get current total supply.

    public fun total_supply<T>(currency: &Currency<T>): Option<u64>
  • is_supply_fixed<T>(): Check if supply is fixed.

    public fun is_supply_fixed<T>(currency: &Currency<T>): bool
  • is_supply_burn_only<T>(): Check if supply is burn-only.

    public fun is_supply_burn_only<T>(currency: &Currency<T>): bool

Capability status

  • is_metadata_cap_claimed<T>(): Check if metadata cap is claimed.

    public fun is_metadata_cap_claimed<T>(currency: &Currency<T>): bool
  • is_metadata_cap_deleted<T>(): Check if metadata cap is deleted.

    public fun is_metadata_cap_deleted<T>(currency: &Currency<T>): bool
  • treasury_cap_id<T>(): Get treasury cap object ID.

    public fun treasury_cap_id<T>(currency: &Currency<T>): Option<ID>
  • metadata_cap_id<T>(): Get metadata cap object ID.

    public fun metadata_cap_id<T>(currency: &Currency<T>): Option<ID>

Regulatory information

  • is_regulated<T>(): Check if coin is regulated.

    public fun is_regulated<T>(currency: &Currency<T>): bool
  • deny_cap_id<T>(): Get deny cap object ID.

    public fun deny_cap_id<T>(currency: &Currency<T>): Option<ID>

Update coin metadata

Metadata updates require the MetadataCap<T> object, which is only available to the bearer of the TreasuryCap for a currency. The bearer of the TreasuryCap can claim the MetadataCap using the claim_metadata_cap function only once. The Currency tracks the claimed status of the capability through its metadata_cap_id field.

The bearer of the MetadataCap can use the available getter functions to update values.

  • set_name<T>(): Update coin name.

    public fun set_name<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, name: String)
  • set_symbol<T>(): Update coin symbol.

    #[test_only]
    public fun set_symbol<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, symbol: String)
  • set_description<T>(): Update coin description.

    public fun set_description<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, description: String)
  • set_icon_url<T>(): Update icon URL.

    public fun set_icon_url<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, icon_url: String)

Manage the metadata capability using:

  • claim_metadata_cap<T>(): Claim the metadata capability one time.

    public fun claim_metadata_cap<T>(
    currency: &mut Currency<T>,
    _: &TreasuryCap<T>,
    ctx: &mut TxContext,
    ): MetadataCap<T>
  • delete_metadata_cap<T>(): Permanently delete the capability to prevent future updates.

    public fun delete_metadata_cap<T>(currency: &mut Currency<T>, cap: MetadataCap<T>)
    caution

    Deleting the MetadataCap using delete_metadata_cap<T>() is an irreversible action.

Migration from Coin to Currency Standard

The Currency Standard replaces the legacy Coin 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 dApps.
  • 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

Smart contracts should update 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>)

Best practices

For currency creators

  • Use OTW creation: Prefer the OTW-based creation for better uniqueness guarantees.
  • Set supply model early: Decide on supply model during initialization (fixed, burn-only, or flexible).
  • Consider regulation: Evaluate whether deny list capabilities are needed.
  • Manage metadata cap: Decide whether to keep, transfer, or delete the metadata capability.

For dApp developers

  • Query registry first: Check the registry for coin information before falling back to legacy methods.
  • Handle migration states: Account for coins in various migration states.
  • Respect supply models: Understand the implications of different supply states (fixed, burn-only, unknown).
  • Check regulatory status: Be aware of regulated coins and their restrictions.

For infrastructure providers

  • Monitor registry changes: Track new coin registrations and updates.
  • Index supply changes: Monitor burn events for burn-only coins.
  • Handle legacy coins: Support both registry and legacy metadata systems.
  • Cache efficiently: Registry data changes infrequently and can be cached.

Security considerations

Capability security:

  • MetadataCap: Protect metadata capabilities as they control coin branding.
  • TreasuryCap: Treasury capabilities determine minting/burning permissions.
  • DenyCapV2: Deny capabilities can restrict coin usage.

Validation: The registry enforces several important validations:

  • Symbol validation: Symbols must be ASCII printable characters.
  • Uniqueness: Each coin type can only be registered once.
  • Supply consistency: Supply states cannot be downgraded.
  • Permission checks: Only appropriate capability holders can make changes.

Migration safety:

  • One-time migration: Legacy metadata can only be migrated once.
  • Capability proof: Metadata deletion requires capability ownership.
  • State consistency: Regulatory state migration prevents double-setting.
Create Currencies and Tokens

Learn how to create currencies and mint coins and tokens on the Sui network using the Coin Registry system.

Closed-Loop Token

Closed-Loop tokens can only be used for a specific service or by authorized users.

Derived Objects

Derived objects enable deterministic object addresses, Transfer-to-Object capabilities, guaranteed uniqueness, and native parallelization for building scalable composable systems on Sui.

`coin` module rustdoc documentation

Automated documentation output for the Sui framework coin module.

`token` module rustdoc documentation

Automated documentation output for the Sui framework token module.

Sui Tokenomics

Sui's tokenomics is designed to support the long-term financial needs of Web3. It uses the native SUI coin as the currency of the network and to pay for the network's gas fees.