본문으로 건너뛰기

Regulated currency

Regulated currency는 deny list를 통해 access control이 관리되는 asset이다. Deny list는 해당 asset을 send하거나 receive할 수 없는 address를 식별한다. 예를 들어 stablecoin은 regulated currency의 한 유형이다.

Sui에서 regulated currency를 만들려면 Currency Standard의 Coin Registry가 sui::coin_registry module을 통해 regulatory feature를 제공한다. 이는 global pause functionality와 ecosystem integration을 built-in으로 지원한다.

이 regulatory feature의 장점은 다음과 같다:

  • Centralized tracking: Regulatory status가 registry에 저장되어 쉽게 discover할 수 있다.
  • Global pause support: Emergency situation을 위한 pause 또는 unpause functionality.
  • Compliance tooling: Wallet, exchange, compliance system과의 integration.
  • Migration support: Legacy regulated coin에서 seamless migration.

DenyList

DenyList는 network의 shared object이다. DenyCapV2를 가진 address는 여기에 access하여 Sui core type을 사용할 수 없는 address list를 지정할 수 있다. DenyList의 primary use case는 특정 type의 coin 또는 currency에 대한 access 제한에 초점을 둔다. Sui의 regulated coin은 알려진 bad actor가 해당 coin에 access하지 못하도록 해야 하는 regulation을 충족한다.

정보

DenyList object는 address 0x403의 system object이다. 직접 만들 수 없다.

사용 가능한 feature는 Currency Standard와 Sui framework의 coin_registry module을 참조한다.

Create regulated currency

Regulatory feature를 사용하려면 create a currency가 필요하다. Currency initialization 중 finalize()를 호출하기 전에 CurrencyInitializer<T>에서 make_regulated() function을 사용한다. 이렇게 하면 Currency<T>에 deny list capability가 추가되고 registry system 안에서 regulatory status가 tracking된다:

Click to open

Regulated currency creation

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

finalize()를 호출한 후 registry는 RegulatedState enum에서 regulatory status를 자동으로 tracking한다.

예시

Click to open

Create Sui account and setup CLI environment

$ sui client

If this is the first time running the sui client CLI tool, it asks you to provide a Sui full node server URL and a meaningful environment alias. It also generates an address with a random key pair in sui.keystore and a config client.yaml.

By default, the client.yaml and sui.keystore files are located in ~/.sui/sui_config. For more information, refer to the Sui client CLI tutorial.

If this is not your first time running sui client, then you already have a client.yaml file in your local environment. If you'd like to create a new address for this tutorial, use the command:

$ sui client new-address ed25519
  • Obtain test tokens.
Click to open

How to obtain tokens

If you are connected to Devnet or Testnet networks, use the Faucet UI to request tokens.

If you are connected to a local full node, learn how to get local network tokens.

이 example은 project의 smart contract(regulated_coin.move)를 만들기 위해 단일 file을 사용한다. Contract는 network에 publish할 때 regulated coin을 정의한다. Treasury capability(TreasuryCap)와 deny capability(DenyCapV2)는 contract를 publish하는 address로 transfer된다. TreasuryCap은 bearer가 coin(이 example에서는 REGULATED_COIN)을 mint하거나 burn할 수 있게 하고, DenyCapV2 bearer는 unauthorized user list에 address를 add하거나 remove할 수 있다.

Click to open

regulated_coin.move

module regulated_coin_example::regulated_coin;

use sui::coin_registry;

/// OTW for the coin.
public struct REGULATED_COIN has drop {}

fun init(otw: REGULATED_COIN, ctx: &mut TxContext) {
// Creates a new currency using `create_currency`, but with an extra capability that
// allows for specific addresses to have their coins frozen. Those addresses cannot interact
// with the coin as input objects.
let (mut currency, treasury_cap) = coin_registry::new_currency_with_otw(
otw,
5, // Decimals
b"$TABLE".to_string(), // Symbol
b"RegulaCoin".to_string(), // Name
b"Example Regulated Coin".to_string(), // Description
b"https://example.com/regulated_coin.png".to_string(), // Icon URL
ctx,
);

// Mark the currency as regulated, issue a `DenyCapV2`.
let deny_cap = currency.make_regulated(true, ctx);
let metadata_cap = currency.finalize(ctx);
let sender = ctx.sender();

// Transfer the treasury cap, deny cap, and metadata cap to the publisher.
transfer::public_transfer(treasury_cap, sender);
transfer::public_transfer(deny_cap, sender);
transfer::public_transfer(metadata_cap, sender);
}
Constant nameDescription
PACKAGE_IDPublish한 package의 object ID이다. 이 data는 publish 시 Sui가 제공하는 response의 일부이다.
ADMIN_SECRET_KEYPackage를 publish하는 address의 secret key이다. sui keytool export --key-identity <SUI_ADDRESS> 또는 wallet UI를 사용해 값을 얻을 수 있다. 값을 public에 노출하지 않는다.
ADMIN_ADDRESSContract를 publish하는 address이다.
DENY_CAP_IDDeny capability object ID이다. 이 data는 publish 시 Sui가 제공하는 response의 일부이다.
TREASURY_CAP_IDBearer가 새 coin을 mint할 수 있게 하는 treasury cap object ID이다. 이 data는 publish 시 Sui가 제공하는 response의 일부이다.
MODULE_NAMEPublish하는 module의 name이다.
COIN_NAMERegulated coin의 name이다.
SUI_FULLNODE_URLTransaction을 process하는 full node network의 URL이다. Testnet에서는 https://fullnode.testnet.sui.io:443이다.

TypeScript 및 Rust client는 coin package의 mint function 호출을 처리한다. coin package에는 동일한 작업을 수행하는 데 사용할 수 있는 mint_and_transfer function도 포함되어 있지만, 한 command에서 coin을 mint하고 다른 command에서 transfer하는 composability가 더 바람직하다. 두 개의 explicit command를 사용하면 mint와 transfer 사이에 future logic을 구현할 수 있다. Programmable transaction block structure 덕분에 network에서는 여전히 단일 transaction만 만들고 지불한다.

program.command('mint-and-transfer')
.description('Mints coins and transfers to an address.')
.requiredOption('--amount <amount>', 'The amount of coins to mint.')
.requiredOption('--address <address>', 'Address to send coins.')

.action((options) => {
console.log("Executing mint new coins and transfer to address...");

console.log("Amount to mint: ", options.amount);
console.log("Address to send coins: ", options.address);
console.log("TREASURY_CAP_ID: ", TREASURY_CAP_ID);
console.log("COIN_TYPE: ", COIN_TYPE);

if(!TREASURY_CAP_ID) throw new Error("TREASURY_CAP_ID environment variable is not set.");

const txb = new Transaction();

const coin = txb.moveCall({
target: `0x2::coin::mint`,
arguments: [
txb.object(TREASURY_CAP_ID),
txb.pure.u64(options.amount),
],
typeArguments: [COIN_TYPE],
});

txb.transferObjects([coin], txb.pure.address(options.address));

executeTx(txb);
});

사용 가능한 모든 Coin function은 Sui framework coin module documentation을 참조한다. 가장 일반적인 function은 다음과 같다:

Click to open

coin::mint<T>

/// Create a coin worth `value` and increase the total supply
/// in `cap` accordingly.
public fun mint<T>(cap: &mut TreasuryCap<T>, value: u64, ctx: &mut TxContext): Coin<T> {
Coin {
id: object::new(ctx),
balance: cap.total_supply.increase_supply(value),
}
}
Click to open

coin::mint_balance<T>

/// Mint some amount of T as a `Balance` and increase the total
/// supply in `cap` accordingly.
/// Aborts if `value` + `cap.total_supply` >= U64_MAX
public fun mint_balance<T>(cap: &mut TreasuryCap<T>, value: u64): Balance<T> {
cap.total_supply.increase_supply(value)
}
Click to open

coin::mint_and_transfer<T>

// === Entrypoints ===

#[allow(lint(public_entry))]
/// Mint `amount` of `Coin` and send it to `recipient`. Invokes `mint()`.
public entry fun mint_and_transfer<T>(
c: &mut TreasuryCap<T>,
amount: u64,
recipient: address,
ctx: &mut TxContext,
) {
transfer::public_transfer(c.mint(amount, ctx), recipient)
}
Click to open

coin::burn<T>

#[allow(lint(public_entry))]
/// Destroy the coin `c` and decrease the total supply in `cap`
/// accordingly.
public entry fun burn<T>(cap: &mut TreasuryCap<T>, c: Coin<T>): u64 {
let Coin { id, balance } = c;
id.delete();
cap.total_supply.decrease_supply(balance)
}

Managing the deny list

Frontend code는 coin의 deny list에 할당된 address를 관리하는 function을 제공한다. 이 function은 coin module의 deny_list_v2_adddeny_list_v2_remove function을 호출한다.

Address를 deny list에 추가하면 해당 address로 token을 여전히 보낼 수 있다는 점을 볼 수 있다. 그 address는 function을 호출한 epoch가 끝날 때까지 coin을 받을 수 있다. Blocked address에서 regulated coin을 보내려고 하면 error가 발생한다. 다음 epoch가 시작된 후에는 해당 address가 coin을 더 이상 받을 수도 없다. Address를 remove하면 즉시 coin을 받을 수 있지만, removal 이후 다음 epoch까지는 그 address가 coin을 transaction input으로 포함할 수 없다.

이 function을 사용하려면 add하거나 remove하려는 address를 전달한다. 그러면 frontend function이 framework의 관련 Move module을 호출하고 DenyList object(0x403)와 DenyCapV2 object ID를 추가한다. DenyCapV2 ID는 smart contract를 publish할 때 받는다. 이 example에서는 frontend function이 읽는 .env file에 해당 값을 추가한다.

program.command('deny-list-add')
.description('Adds an address to the deny list.')
.requiredOption('--address <address>', 'Address to add.')

.action((options) => {
console.log("Executing addition to deny list...");
console.log("Address to add to deny list: ", options.address);
const txb = new Transaction();

txb.moveCall({
target: `0x2::coin::deny_list_v2_add`,
arguments: [
txb.object(SUI_DENY_LIST_OBJECT_ID),
txb.object(DENY_CAP_ID),
txb.pure.address(options.address),
],
typeArguments: [COIN_TYPE],
});

executeTx(txb);
});


program.command('deny-list-remove')
.description('Removes an address from the deny list.')
.requiredOption('--address <address>', 'Address to add.')
.requiredOption('--deny_list <address>', 'Deny list object ID.')

.action((options) => {
console.log("Executing removal from deny list...");
console.log("Address to remove in deny list: ", options.address);

if(!DENY_CAP_ID) throw new Error("DENY_CAP_ID environment variable is not set. Are you sure the active address owns the deny list object?");

const txb = new Transaction();

txb.moveCall({
target: `0x2::coin::deny_list_v2_remove`,
arguments: [
txb.object(SUI_DENY_LIST_OBJECT_ID),
txb.object(DENY_CAP_ID),
txb.pure.address(options.address),
],
typeArguments: [COIN_TYPE],
});

executeTx(txb);
});