본문으로 건너뛰기

Currency Standard

Coin과 Currency standard는 모두 fungible token을 생성하는 데 사용된다. 그러나 이들은 서로 다른 생성 방법을 사용하고 서로 다른 타입의 object에 metadata를 저장한다.

Coin은 coin::create_currency를 사용하여 asset을 생성하는 반면 Currency는 coin_registry::new_currencycoin_registry::new_currency_with_otw를 사용한다.

Coin은 CoinMetadata를 사용하는 반면 Currency는 Currency object를 사용한다. Coin 또는 Currency standard를 사용하여 Sui에서 생성된 fungible token은 _coins_로 지칭된다.

Coin은 fungible asset에 대한 단일 object wrapper를 의미하는 반면, currency라는 용어는 currency의 속성과 설정을 설명하는 CoinRegistry에서 생성되는 object를 의미한다.

Closed-Loop Token standard를 사용하여 Sui에서 생성된 fungible token의 경우, 문서는 _tokens_라는 용어를 사용한다. 실제로는 이 두 object에 대한 용어가 종종 서로 바꿔서 사용된다.

Currency Standard는 Sui에서 코인을 생성하기 위해 Move 스마트 계약이 사용하는 기술 standard이다. sui::coin_registry 모듈은 standard를 정의하는 로직을 제공하며 metadata를 통합하고 supply tracking을 강화하며 regulatory feature를 개선한다.

Sui에서 코인 생성이 standardization되었다는 것은 지갑, 거래소, 그리고 다른 스마트 계약이 추가적인 처리 로직 없이도 SUI를 관리하는 것과 동일하게 Sui에서 생성된 코인을 관리할 수 있음을 의미한다.

SUI 네이티브 currency와 네트워크에서의 사용에 대해 더 알아보려면 Sui 토크노믹스를 참조한다.

Sui의 코인은 Currency Standard를 따르면서 특화된 ability를 제공할 수 있다. 예를 들어, creator가 특정 address를 deny list에 추가할 수 있게 하여 식별된 address가 transaction의 input으로 코인을 사용할 수 없도록 하는 regulated coin을 생성할 수 있다.

Fungible tokens

Currency<T> 타입은 open-loop fungible token을 나타낸다(closed-loop token은 Token<T> 참조). Currency는 타입 파라미터 T로 denominated되며, T는 모든 Currency<T> 인스턴스에 적용되는 metadata(이름, symbol, decimal precision 등)와도 연관된다. sui::coin_registry 모듈은 Currency<T>에 대한 인터페이스를 노출하여 이를 fungible로 취급하며, 이는 전통적인 fiat currency가 작동하는 방식과 유사하게 Currency<T>의 한 인스턴스에 보관된 T의 단위가 다른 어떤 T의 단위와도 interchangeable하다는 것을 의미한다.

Coin creation

Coin Registry는 sui::coin_registry 모듈을 통해 통합된 코인 관리를 제공하는 중앙화된 시스템이다.

정보

이 레지스트리는 address 0xc에 위치한 시스템 레벨 shared object이다.

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::{Self, 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::dynamic_field as df;
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.";
/// 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";
#[error(code = 12)]
const EDeletionNotSupported: vector<u8> = b"Deleting legacy metadata is not supported";
#[error(code = 13)]
const ENotOneTimeWitness: vector<u8> = b"Type is expected to be OTW";
#[error(code = 14)]
const EBorrowLegacyMetadata: vector<u8> = b"Cannot borrow legacy metadata for migrated currency";
#[error(code = 15)]
const EDuplicateBorrow: vector<u8> = b"Attempt to return duplicate borrowed CoinMetadata";

/// 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;

/// Marker used in metadata to indicate that the currency is not migrated.
const NEW_CURRENCY_MARKER: vector<u8> = b"is_new_currency";

/// 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.
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;

/// Key used to store the legacy `CoinMetadata` for a `Currency`.
public struct LegacyMetadataKey() 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 }

/// Potato callback for the legacy `CoinMetadata` borrowing.
public struct Borrow<phantom T> {}

/// 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), ENotOneTimeWitness);
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 is_otw = builder.is_otw;
let (currency, metadata_cap) = finalize_impl!(builder, ctx);

// Either share directly (`new_currency` scenario), or transfer as TTO to `CoinRegistry`.
if (is_otw) transfer::transfer(currency, object::sui_coin_registry_address())
else transfer::share_object(currency);

metadata_cap
}

#[allow(lint(share_owned))]
/// Does the same as `finalize`, but also deletes the `MetadataCap` after finalization.
public fun finalize_and_delete_metadata_cap<T>(
builder: CurrencyInitializer<T>,
ctx: &mut TxContext,
) {
let is_otw = builder.is_otw;
let (mut currency, metadata_cap) = finalize_impl!(builder, ctx);

currency.delete_metadata_cap(metadata_cap);

// Either share directly (`new_currency` scenario), or transfer as TTO to `CoinRegistry`.
if (is_otw) transfer::transfer(currency, object::sui_coin_registry_address())
else transfer::share_object(currency);
}

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

/// 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,
) {
let currency = migrate_legacy_metadata_impl!(registry, legacy);
transfer::share_object(currency);
}

/// 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());
}

#[deprecated(note = b"Method disabled")]
public fun delete_migrated_legacy_metadata<T>(_: &mut Currency<T>, _: CoinMetadata<T>) {
abort EDeletionNotSupported
}

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

// === Borrowing of legacy CoinMetadata ===

/// Borrow the legacy `CoinMetadata` from a new `Currency`. To preserve the `ID`
/// of the legacy `CoinMetadata`, we create it on request and then store it as a
/// dynamic field for future borrows.
///
/// `Borrow<T>` ensures that the `CoinMetadata` is returned in the same transaction.
public fun borrow_legacy_metadata<T>(
currency: &mut Currency<T>,
ctx: &mut TxContext,
): (CoinMetadata<T>, Borrow<T>) {
assert!(!currency.is_migrated_from_legacy(), EBorrowLegacyMetadata);

if (!df::exists_(&currency.id, LegacyMetadataKey())) {
let legacy = currency.to_legacy_metadata(ctx);
df::add(&mut currency.id, LegacyMetadataKey(), legacy);
};

let mut legacy: CoinMetadata<T> = df::remove(&mut currency.id, LegacyMetadataKey());

legacy.update_coin_metadata(
currency.name,
currency.symbol.to_ascii(),
currency.description,
currency.icon_url.to_ascii(),
);

(legacy, Borrow {})
}

/// Return the borrowed `CoinMetadata` and the `Borrow` potato to the `Currency`.
///
/// Note to self: Borrow requirement prevents deletion through this method.
public fun return_borrowed_legacy_metadata<T>(
currency: &mut Currency<T>,
mut legacy: CoinMetadata<T>,
borrow: Borrow<T>,
_ctx: &mut TxContext,
) {
assert!(!df::exists_(&currency.id, LegacyMetadataKey()), EDuplicateBorrow);

let Borrow {} = borrow;

// Always store up to date value.
legacy.update_coin_metadata(
currency.name,
currency.symbol.to_ascii(),
currency.description,
currency.icon_url.to_ascii(),
);

df::add(&mut currency.id, LegacyMetadataKey(), legacy);
}

// === 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>())
}

/// Whether the currency is migrated from legacy.
fun is_migrated_from_legacy<T>(currency: &Currency<T>): bool {
!currency.extra_fields.contains(&NEW_CURRENCY_MARKER.to_string())
}

/// Create a new legacy `CoinMetadata` from a `Currency`.
fun to_legacy_metadata<T>(currency: &Currency<T>, ctx: &mut TxContext): CoinMetadata<T> {
coin::new_coin_metadata(
currency.decimals,
currency.name,
currency.symbol.to_ascii(),
currency.description,
currency.icon_url.to_ascii(),
ctx,
)
}

#[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(),
});
}

/// Internal macro to keep implementation between build and test modes.
macro fun finalize_impl<$T>(
$builder: CurrencyInitializer<$T>,
$ctx: &mut TxContext,
): (Currency<$T>, MetadataCap<$T>) {
let CurrencyInitializer { mut currency, extra_fields, is_otw: _ } = $builder;
extra_fields.destroy_empty();
let id = object::new($ctx);
currency.metadata_cap_id = MetadataCapState::Claimed(id.to_inner());

// Mark the currency as new, so in the future we can support borrowing of the
// legacy metadata.
currency
.extra_fields
.insert(
NEW_CURRENCY_MARKER.to_string(),
ExtraField(type_name::with_original_ids<bool>(), NEW_CURRENCY_MARKER),
);

(currency, MetadataCap<$T> { id })
}

/// Internal macro to keep implementation between build and test modes.
macro fun migrate_legacy_metadata_impl<$T>(
$registry: &mut CoinRegistry,
$legacy: &CoinMetadata<$T>,
): Currency<$T> {
let registry = $registry;
let legacy = $legacy;

assert!(!registry.exists<$T>(), ECurrencyAlreadyRegistered);
assert!(is_ascii_printable!(&legacy.get_symbol().to_string()), EInvalidSymbol);

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(),
}
}

/// 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]
/// For transactional tests (if CoinRegistry is used as a shared object).
public fun share_for_testing(registry: CoinRegistry) {
transfer::share_object(registry);
}

#[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>(
builder: CurrencyInitializer<T>,
ctx: &mut TxContext,
): (Currency<T>, MetadataCap<T>) {
finalize_impl!(builder, ctx)
}

#[test_only]
public fun migrate_legacy_metadata_for_testing<T>(
registry: &mut CoinRegistry,
legacy: &CoinMetadata<T>,
_ctx: &mut TxContext,
): Currency<T> {
migrate_legacy_metadata_impl!(registry, legacy)
}

Core components

CoinRegistry: 모든 코인 관련 작업을 조정하는 메인 시스템 object이다. 이 shared object는 모든 registry 작업의 entry point 역할을 하며 네트워크 초기화 중에 한 번 생성된다. Address는 0xc이다.

Currency<T>: registry 시스템의 핵심이며, 다음을 포함하여 각 코인 type에 대한 포괄적인 정보를 저장한다:

  • Metadata management: name, symbol, decimals, description, icon URL과 같은 기본 코인 정보.
  • Supply tracking: supply state 정보(fixed, burn-only, 또는 unknown)를 유지한다.
  • Regulatory status: 코인이 deny list capability로 regulated되었는지 추적한다.
  • Capability references: 코인 type에 대한 TreasuryCapMetadataCap에 대한 link.
  • Extensibility: 향후 개선을 위한 extra field를 포함한다.

Supply states

registry는 세 가지 다른 supply management model을 지원한다:

  • Fixed supply: 총 supply가 영구적으로 잠겨 변경할 수 없다.
  • Burn-only supply: 새로운 코인은 mint할 수 없지만, 기존 코인은 burn할 수 있다.
  • Uncontrolled supply: TreasuryCap holder가 minting과 burning을 제어한다.

Regulatory states

코인은 다른 regulatory state를 가질 수 있다:

  • Regulated: 코인에는 address가 이를 사용하지 못하도록 제한할 수 있는 DenyCapV2가 연결되어 있다.
  • Unregulated: 코인이 deny list capability 없이 생성되었다.
  • Unknown: regulatory status가 결정되지 않았으며, 일반적으로 legacy migration에서 발생한다.

Creation options

registry는 두 가지 다른 코인 creation flow를 지원한다:

  • Standard creation (recommended):

    코인 type이 publish된 후 언제든지 new_currency 함수를 사용한다. 이 함수는 즉시 shared Currency<T> object를 생성한다. T 타입은 public struct MyCoin has key { id: UID }와 같이 key-only type이어야 한다.

    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:

    Proper creation and RPC support requires a second transaction to promote the currency to the registry.

    새 코인에 대한 OTW creation은 2단계 프로세스이다. 초기화 프로세스는 package publication으로 시작된다. 그 다음, 코인을 registry에 배치하기 위해 coin_registry::finalize_registration 호출이 필요하다. 자세한 내용은 Coin finalization을 참조한다.

    new_currency_with_otw 함수는 uniqueness proof를 위해 OTW를 사용한다. 자세한 내용은 The Move Book의 One-Time Witness를 참조한다.

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

Coin initialization

두 생성 방법 모두 추가 구성에 사용할 수 있는 CurrencyInitializer<T>를 반환한다:

  • Make regulated: deny list capability를 추가한다.
  • Set supply model: fixed, burn-only, 또는 flexible supply 중에서 선택한다.
  • Add extensions: custom functionality를 위해 additional field를 포함한다.
/// 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,
}

Coin finalization

new_currency 함수로 생성된 Currency는 finalize 함수가 호출된 후 즉시 완성된다.

OTW currencies

OTW created currencies의 경우 currency를 registry로 promote하기 위해 finalize_registration반드시 호출해야 한다.

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

package init 함수에서 OTW를 사용하여 코인을 생성하는 package를 publish하는 경우, 초기화 후 coin_registry::finalize_registration을 호출해야 한다. 이 함수는 코인을 shared object로 변환한다.

OTW currencies

OTW created currencies의 경우 currency를 registry로 promote하기 위해 finalize_registration반드시 호출해야 한다.

# 게시 중에 생성된 Currency object의 ID를 사용해야 한다.
# 이 단계는 OTW로 생성된 currency에만 필요하다.
sui client ptb
--assign @created_currency_object_id currency_to_promote
--move-call 0x2::coin_registry::finalize_registration <CURRENCY_CYPE> @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,
)

Treasury capability

coin_registry::new_currency 또는 coin_registry::new_currency_with_otw 함수를 사용하여 코인을 생성하면, 코인을 생성하는 스마트 계약의 publisher는 코인 finalization 시 TreasuryCap object를 받는다. TreasuryCap object는 새로운 코인을 mint하거나 현재 코인을 burn하는 데 필요하다(코인 supply state에 따라 다름). 따라서 이 object에 액세스할 수 있는 address만 Sui 네트워크에서 코인 supply를 유지할 수 있다.

TreasuryCap object는 transferable이므로, TreasuryCap을 전달하면 제3자가 생성한 코인의 관리를 인수할 수 있다. object를 전달한 후에는 더 이상 직접 코인을 mint하거나 burn할 수 없다.

Regulated coins

Currency Standard는 규제된 코인 생성을 지원한다. finalize를 호출하기 전 initialization 단계에서 make_regulated 함수를 사용한다. 이 함수는 Currency<T>에 deny list capability를 추가하고 Coin Registry 내에서 regulatory status를 추적한다. 이 함수는 bearer가 deny list의 address 목록을 유지할 수 있게 하는 DenyCap을 반환한다.

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

특정 규제된 코인을 사용할 수 없는 address 목록은 시스템이 생성한 DenyList shared object에 보관된다. DenyCap에 액세스할 수 있다면 coin::deny_list_v2_addcoin::deny_list_v2_remove 함수를 사용하여 address를 추가하고 제거할 수 있다.

Global pause switch

규제된 코인 object에는 allow_global_pause Boolean field가 포함된다. 이 field가 true로 설정되면, 코인 type의 DenyCapV2 object bearer는 coin::deny_list_v2_enable_global_pause 함수를 사용하여 코인 활동을 무기한 일시 중지할 수 있다. bearer가 일시 중지를 시작하면 네트워크는 즉시 어떤 transaction에서도 코인 type을 input으로 허용하지 않는다. 다음 epoch(각 epoch는 약 24시간 지속) 시작 시, 네트워크는 모든 address가 코인 type을 받는 것 또한 추가로 허용하지 않는다.

코인 type의 DenyCapV2 object bearer가 coin::deny_list_v2_disable_global_pause를 사용하여 일시 중지를 제거하면, 코인은 즉시 다시 transaction input으로 사용할 수 있다. 그러나 address는 다음 epoch까지 코인 type을 받을 수 없다.

글로벌 일시 중지 기능은 코인의 deny list에 영향을 주지 않는다. 코인의 일시 중지를 해제한 후에도 deny list에 포함된 어떤 address든 코인과 상호 작용할 수 없다.

Currency metadata

Currency metadata는 registry의 Currency<T> object에 중앙에서 저장된다. Metadata update는 다음과 같을 수 있는 MetadataCap<T> capability에 의해 제어된다:

  • Unclaimed: MetadataCap이 아직 클레임되지 않았다.
  • Claimed: MetadataCap이 클레임되었으며 update에 사용할 수 있다.
  • Deleted: MetadataCap이 영구적으로 삭제되어, 향후 update가 방지된다.

Metadata fields

Metadata object의 field는 다음을 포함한다:

NameDescription
registryCoinRegistry system object ID(0x3)이다.
decimals코인이 사용하는 decimals 개수이다. 이 필드를 3으로 설정하면, 값이 1000인 코인은 1.000으로 표시된다.
name코인의 name이다.
symbol코인의 symbol이다. 이는 name과 같을 수 있지만, 일반적으로 모두 대문자 5자 미만이다. 예를 들어, SUI는 Sui의 네이티브 코인에 대한 symbol이지만 nameSUI이다.
description코인을 설명하기 위한 짧은 description이다.
icon_url지갑, explorer, 그리고 다른 앱에서 display에 사용하는 코인 icon의 URL이다.

Minting and burning coins

Coin Registry는 고급 supply management model을 지원한다:

  • Unknown supply: TreasuryCap을 통한 전통적인 minting/burning.
  • Fixed supply: 총 supply가 영구적으로 잠겨, minting 또는 burning이 허용되지 않는다.
  • Burn-only supply: 새로운 minting은 허용되지 않지만, 기존 코인은 registry 함수를 통해 burn할 수 있다.

Mint

새 코인을 생성하려면 coin::mint 함수를 사용한다.

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

signature는 TreasuryCap, 생성된 코인의 value, 그리고 transaction context로 함수를 호출하면 Coin<T>가 생성됨을 보여준다. 이 함수는 TreasuryCap의 총 supply를 자동으로 업데이트한다. display 시 코인 value는 metadata의 decimals 값을 따른다. 따라서 decimal 값이 6인 코인의 value로 1000000을 제공하면, 코인의 값은 1.000000으로 표시된다.

Burn

burn-only supply status인 코인은 TreasuryCap 없이도 coin_registry::burncoin_registry::burn_balance 함수를 사용하여 registry를 통해 직접 burn할 수 있다.

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

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

unknown supply status인 코인의 경우, coin::burn 함수를 사용하여 현재 코인을 파기한다.

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

signature는 TreasuryCap과 burn하려는 코인 object만이 필요한 input임을 보여주며, supply가 감소한 양(코인의 value)을 반환한다. 이 함수는 supply에 존재하는 것보다 더 많은 코인을 burn하도록 허용하지 않는다.

Adding and removing addresses to and from the deny list

deny list는 규제된 코인에만 적용된다. 앞서 언급했듯이, 규제된 코인을 생성하면 bearer가 시스템이 생성한 DenyList object에서 address를 추가하고 제거하도록 권한을 부여하는 DenyCapV2를 받는다. 코인의 목록에 있는 어떤 address든 추가되는 즉시 transaction의 input으로 코인을 사용할 수 없다. deny list에 address가 추가된 다음 epoch에, 해당 address는 추가로 코인 type을 수신할 수도 없다. 즉, 코인 type에 대한 deny list에 추가된 address는 즉시 코인을 보낼 수 없게 된다. 그 다음 epoch의 시작 시점에, 해당 address는 여전히 코인을 보낼 수 없을 뿐만 아니라 받을 수도 없다. 그 시점부터 DenyCapV2 bearer가 deny list에서 명시적으로 제거할 때까지 해당 address는 코인과 상호작용할 수 없다.

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

Query functions

Currency<T> object에서 직접 정보를 쿼리할 수 있다.

Metadata

  • decimals<T>(): 소수점 개수를 가져온다.

    public fun decimals<T>(currency: &Currency<T>): u8
  • name<T>(): 코인 name을 가져온다.

    public fun name<T>(currency: &Currency<T>): String
  • symbol<T>(): 코인 symbol을 가져온다.

    public fun symbol<T>(currency: &Currency<T>): String
  • description<T>(): 코인 description을 가져온다.

    public fun description<T>(currency: &Currency<T>): String
  • icon_url<T>(): icon URL을 가져온다.

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

Supply information

  • total_supply<T>(): 현재 총 supply를 가져온다.

    public fun total_supply<T>(currency: &Currency<T>): Option<u64>
  • is_supply_fixed<T>(): supply가 fixed인지 확인한다.

    public fun is_supply_fixed<T>(currency: &Currency<T>): bool
  • is_supply_burn_only<T>(): supply가 burn-only인지 확인한다.

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

Capability status

  • is_metadata_cap_claimed<T>(): metadata cap이 claimed인지 확인한다.

    public fun is_metadata_cap_claimed<T>(currency: &Currency<T>): bool
  • is_metadata_cap_deleted<T>(): metadata cap이 deleted인지 확인한다.

    public fun is_metadata_cap_deleted<T>(currency: &Currency<T>): bool
  • treasury_cap_id<T>(): treasury cap object ID를 가져온다.

    public fun treasury_cap_id<T>(currency: &Currency<T>): Option<ID>
  • metadata_cap_id<T>(): metadata cap object ID를 가져온다.

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

Regulatory information

  • is_regulated<T>(): 코인이 regulated인지 확인한다.

    public fun is_regulated<T>(currency: &Currency<T>): bool
  • deny_cap_id<T>(): deny cap object ID를 가져온다.

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

Update currency metadata

Metadata update에는 코인의 TreasuryCap bearer만 사용할 수 있는 MetadataCap<T> object가 필요하다. TreasuryCap bearer는 claim_metadata_cap 함수를 사용하여 MetadataCap을 한 번만 클레임할 수 있다. Currencymetadata_cap_id field를 통해 capability의 claimed status를 추적한다.

MetadataCap bearer는 사용 가능한 getter 함수를 사용하여 값을 업데이트할 수 있다.

  • set_name<T>(): 코인 name을 업데이트한다.

    public fun set_name<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, name: String)
  • set_description<T>(): 코인 description을 업데이트한다.

    public fun set_description<T>(currency: &mut Currency<T>, _: &MetadataCap<T>, description: String)
  • set_icon_url<T>(): icon URL을 업데이트한다.

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

다음을 사용하여 metadata capability를 관리한다:

  • claim_metadata_cap<T>(): metadata capability를 한 번 클레임한다.

    public fun claim_metadata_cap<T>(
    currency: &mut Currency<T>,
    _: &TreasuryCap<T>,
    ctx: &mut TxContext,
    ): MetadataCap<T>
  • delete_metadata_cap<T>(): 향후 update를 방지하기 위해 capability를 영구적으로 삭제한다.

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

    delete_metadata_cap<T>()를 사용하여 MetadataCap을 삭제하는 것은 되돌릴 수 없는 액션이다.

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

Best practices

For coin creators

  • Set supply model early: 초기화 중에 supply model(fixed, burn-only, 또는 flexible)을 결정한다.
  • Consider regulation: deny list capability가 필요한지 평가한다.
  • Manage metadata cap: metadata capability를 유지할지, 전송할지, 삭제할지 결정한다.

For app developers

  • Query registry first: legacy method로 돌아가기 전에 registry에서 코인 정보를 확인한다.
  • Handle migration states: 다양한 migration state에 있는 coin을 고려한다.
  • Respect supply models: 다양한 supply state(fixed, burn-only, unknown)의 의미를 이해한다.
  • Check regulatory status: regulated coin과 그 제한 사항을 인지한다.

For infrastructure providers

  • Monitor registry changes: 새로운 코인 registration 및 update를 추적한다.
  • Index supply changes: burn-only coin에 대한 burn 이벤트를 모니터링한다.
  • Handle legacy coins: registry 및 legacy metadata 시스템을 모두 지원한다.
  • Cache efficiently: registry 데이터는 자주 변경되지 않으므로 캐시할 수 있다.

Security considerations

Capability security:

  • MetadataCap: metadata capability는 코인 branding을 제어하므로 보호한다.
  • TreasuryCap: treasury capability는 minting 및 burning 권한을 결정한다.
  • DenyCapV2: deny capability는 코인 사용을 제한할 수 있다.

Validation: registry는 몇 가지 중요한 validation을 수행한다:

  • Symbol validation: symbol은 ASCII printable character여야 한다.
  • Uniqueness: 각 코인 type은 한 번만 등록할 수 있다.
  • Supply consistency: supply state는 downgrading할 수 없다.
  • Permission checks: 적절한 capability holder만 변경할 수 있다.

Migration safety:

  • One-time migration: legacy metadata는 한 번만 migration할 수 있다.
  • Capability proof: metadata deletion에는 capability ownership이 필요하다.
  • State consistency: regulatory state migration은 double-setting을 방지한다.