본문으로 건너뛰기

Regulated currency

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

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

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

  • Centralized tracking: Regulatory status가 registry에 저장되어 쉽게 discover할 수 있다.
  • Global pause support: Emergency situation을 위한 pause 또는 unpause 기능.
  • Compliance tooling: Wallet, exchange, 컴플라이언스 system과의 integration.
  • Migration support: Legacy regulated 코인에서 seamless 마이그레이션.

DenyList

DenyList는 network의 공유 객체이다. DenyCapV2를 가진 주소는 여기에 access하여 Sui core 타입을 사용할 수 없는 주소 list를 지정할 수 있다. DenyList의 primary 사용 사례는 특정 타입의 코인 또는 currency에 대한 access 제한에 초점을 둔다. Sui의 regulated 코인은 알려진 bad actor가 해당 코인에 access하지 못하도록 해야 하는 규제을 충족한다.

정보

DenyList 객체는 주소 0x403의 system 객체이다. 직접 만들 수 없다.

사용 가능한 feature는 Currency Standard와 Sui 프레임워크의 coin_registry 모듈을 참조한다.

Create regulated currency

Regulatory feature를 사용하려면 create a currency가 필요하다. Currency initialization 중 make_regulated()를 호출하기 전에 CurrencyInitializer<T>에서 finalize() 함수를 사용한다. 이렇게 하면 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의 스마트 계약(regulated_coin.move)를 만들기 위해 단일 file을 사용한다. Contract는 network에 publish할 때 regulated 코인을 정의한다. Treasury capability(TreasuryCap)와 deny capability(DenyCapV2)는 contract를 publish하는 주소로 transfer된다. TreasuryCap은 bearer가 코인(이 example에서는 REGULATED_COIN)을 민트하거나 burn할 수 있게 하고, DenyCapV2 bearer는 unauthorized user list에 주소를 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한 패키지의 객체 ID이다. 이 data는 publish 시 Sui가 제공하는 response의 일부이다.
ADMIN_SECRET_KEYPackage를 publish하는 주소의 secret key이다. sui keytool export --key-identity <SUI_ADDRESS> 또는 지갑 UI를 사용해 값을 얻을 수 있다. 값을 public에 노출하지 않는다.
ADMIN_ADDRESSContract를 publish하는 주소이다.
DENY_CAP_IDDeny capability 객체 ID이다. 이 data는 publish 시 Sui가 제공하는 response의 일부이다.
TREASURY_CAP_IDBearer가 새 코인을 민트할 수 있게 하는 treasury cap 객체 ID이다. 이 data는 publish 시 Sui가 제공하는 response의 일부이다.
MODULE_NAMEPublish하는 모듈의 name이다.
COIN_NAMERegulated 코인의 name이다.
SUI_FULLNODE_URLTransaction을 process하는 full node network의 URL이다. Testnet에서는 https://fullnode.testnet.sui.io:443이다.

TypeScript 및 Rust 클라이언트는 coin 패키지의 mint 함수 호출을 처리한다. coin 패키지에는 동일한 작업을 수행하는 데 사용할 수 있는 mint_and_transfer 함수도 포함되어 있지만, 한 명령에서 코인을 민트하고 다른 명령에서 전송하는 composability가 더 바람직하다. 두 개의 explicit 명령를 사용하면 민트와 transfer 사이에 future logic을 구현할 수 있다. Programmable 트랜잭션 블록 structure 덕분에 network에서는 여전히 단일 트랜잭션만 만들고 지불한다.

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 함수는 Sui 프레임워크 coin 모듈 documentation을 참조한다. 가장 일반적인 함수는 다음과 같다:

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는 코인의 deny list에 할당된 주소를 관리하는 함수를 제공한다. 이 함수는 deny_list_v2_add 모듈의 deny_list_v2_removecoin 함수를 호출한다.

Address를 deny list에 추가하면 해당 주소로 토큰을 여전히 보낼 수 있다는 점을 볼 수 있다. 그 주소는 함수를 호출한 epoch가 끝날 때까지 코인을 받을 수 있다. Blocked 주소에서 regulated 코인을 보내려고 하면 error가 발생한다. 다음 epoch가 시작된 후에는 해당 주소가 코인을 더 이상 받을 수도 없다. Address를 remove하면 즉시 코인을 받을 수 있지만, removal 이후 다음 epoch까지는 그 주소가 코인을 트랜잭션 입력으로 포함할 수 없다.

이 함수를 사용하려면 add하거나 remove하려는 주소를 전달한다. 그러면 frontend 함수가 프레임워크의 관련 Move 모듈을 호출하고 DenyList 객체(0x403)와 DenyCapV2 객체 ID를 추가한다. DenyCapV2 ID는 스마트 계약을 publish할 때 받는다. 이 example에서는 frontend 함수가 읽는 .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);
});