본문으로 건너뛰기

규제 통화와 Deny List

stablecoin과 같은 regulated currency를 Sui에서 생성할 수 있다. 이 coin은 SUI 같은 다른 coin과 비슷하지만, deny list를 사용해 currency에 대한 access를 제어하는 기능을 포함한다.

Coin Registry system은 sui::coin_registry module을 통해 강화된 regulatory feature를 제공한다:

  1. During initialization: finalize()를 호출하기 전에 CurrencyInitializer<T>make_regulated() 함수를 사용한다.
  2. Regulatory tracking: registry가 RegulatedState enum에서 regulatory status를 자동으로 추적한다.
  3. Enhanced compliance: global pause 기능과 더 나은 ecosystem integration을 built-in으로 지원한다.

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

  • Centralized tracking: regulatory status가 registry에 저장되어 쉽게 발견할 수 있다.
  • Global pause support: 긴급 상황을 위한 pause 또는 unpause 기능이 강화된다.
  • Compliance tooling: wallet, exchange, compliance system과 더 잘 통합된다.
  • Migration support: 레거시 규제된 코인에서 매끄럽게 마이그레이션할 수 있다.

DenyList

DenyListDenyCapV2 bearer가 접근하여 Sui core type을 사용할 수 없는 address 목록을 지정할 수 있는 singleton shared object이다. 하지만 DenyList의 초기 사용 사례는 지정된 type의 coin에 대한 access를 제한하는 데 초점을 맞춘다. 이는 transaction의 입력으로 사용하는 것을 특정 address에 대해 차단할 수 있어야 하는 Sui 규제된 코인(regulated coin)을 만들 때 유용하다. Sui의 규제된 코인은 알려진 악의적 행위자가 해당 coin에 접근하지 못하게 해야 한다는 규제를 충족한다.

정보

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

사용 가능한 기능에 대해 알아보려면 Currency Standard 문서와 Sui framework의 coin_registry module을 참조한다.

Regulated coin example

규제된 코인 예시는 Sui repo의 examples/regulated-coin directory에 있다. 이 예시는 Sui의 규제된 코인 기능 일부를 보여주는 온체인 package에 대해 TypeScript 및 Rust 기반 command line access를 모두 제공한다.

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.

이 주제는 Sui repo의 own fork에서 code에 접근하고 있다고 가정한다.

이 project를 사용하기 위해 Sui wallet이 반드시 필요한 것은 아니지만, 하나 있으면 결과를 시각화하는 데 도움이 될 수 있다.

이 예시는 Sui에서 package를 publish하는 것과 Move language에 익숙하다고 가정한다. example app에 대한 더 자세한 가이드는 앱 예시를 참조한다. Move language에 대한 자세한 내용은 The Move Book을 참조한다.

Publishing to a network

smart contract는 다른 package와 같은 방식으로 network에 publish한다. publish 과정에 대한 더 자세한 내용이 필요하다면 Hello, World!를 참조한다.

예시에는 publish를 자동화하기 위해 실행할 수 있는 publish.sh file이 포함되어 있다. script는 Testnet에 publish한다고 가정하므로, local network나 Devnet에서 실행할 계획이라면 실행 전에 반드시 업데이트해야 한다.

publish script는 frontend folder 각각에 필요한 .env file도 생성한다. script를 사용하지 않는다면 .env file을 수동으로 만들고 frontend가 찾기를 기대하는 variable 값들을 제공해야 한다. script를 사용하더라도 ADMIN_SECRET_KEY와 그 값을 제공해야 한다.

경고

address의 secret key를 public에 노출하지 않도록 주의한다.

Constant nameDescription
PACKAGE_IDpublish한 package의 object ID이다. 이 데이터는 publish 시 Sui가 제공하는 응답의 일부이다.
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이다. 이 데이터는 publish 시 Sui가 제공하는 응답의 일부이다.
TREASURY_CAP_IDbearer가 새 coin을 mint할 수 있게 하는 treasury cap object ID이다. 이 데이터는 publish 시 Sui가 제공하는 응답의 일부이다.
MODULE_NAMEpublish하는 module의 이름이다.
COIN_NAME규제된 코인의 이름이다.
SUI_FULLNODE_URLtransaction을 처리하는 full node network의 URL이다. Testnet에서 이 값은 https://fullnode.testnet.sui.io:443이다.

Smart contract

이 예시는 project의 smart contract를 생성하기 위해 단일 file(regulated_coin.move)을 사용한다. 이 contract는 이를 network에 publish할 때 규제된 코인을 정의한다. treasury capability(TreasuryCap)와 deny capability(DenyCapV2)는 contract를 publish하는 address로 transfer된다. TreasuryCap은 bearer가 coin을 mint하거나 burn할 수 있게 하며(이 예시에서는 REGULATED_COIN), DenyCapV2 bearer는 승인되지 않은 사용자 목록에 address를 추가하고 제거할 수 있다.

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

Creating regulated coins

Sui Currency Standard는 One-Time Witness를 사용해 currency를 생성하는 new_currency_with_otw 함수를 제공한다. currency를 regulated하게 만들기 위해 module은 regulated currency를 초기화하고 DenyCapV2를 반환하는 make_regulated 함수를 제공한다. DenyCapV2 bearer는 coin에 대한 access를 제어하거나 규제하는 목록에 address를 추가하고 제거할 수 있다. 이 ability는 stablecoin 같은 asset에 필요한 요구 사항이다.

TypeScript client와 Rust client는 coin package의 mint 함수 호출을 처리한다. coin package에는 같은 작업을 수행하는 데 사용할 수 있는 mint_and_transfer 함수도 포함되어 있지만, 1개의 command에서 coin을 mint하고 다른 command에서 transfer하는 composability가 더 바람직하다. 2개의 explicit command를 사용하면 coin을 mint하는 단계와 transfer 사이에 future logic을 구현할 수 있다. programmable transaction block의 구조 덕분에 여전히 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 함수는 Sui framework의 coin module 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)
}

Manage deny list

coin에 할당된 deny list address를 관리할 수 있도록 frontend code는 몇 가지 추가 함수를 제공한다. 이 추가 함수는 coin module의 deny_list_v2_adddeny_list_v2_remove 함수를 호출한다.

address를 deny list에 추가하면, 여전히 그 address로 token을 보낼 수 있다는 점을 알 수 있다. 그렇다면 이는 해당 address가 함수를 호출한 epoch가 끝날 때까지는 여전히 coin을 받을 수 있기 때문이다. 이제 차단된 address에서 규제된 코인을 보내려고 하면 시도는 error를 발생시킨다. 다음 epoch가 시작된 뒤에는 그 address가 더 이상 coin을 받을 수도 없다. address를 제거하면 즉시 coin을 받을 수 있지만, 그 address가 coin을 transaction input으로 포함하려면 제거 후 다음 epoch까지 기다려야 한다.

이 함수를 사용하려면 추가하거나 제거하려는 address를 전달한다. 그다음 frontend 함수가 framework의 관련 move module을 호출하면서 DenyList object(0x403)와 여러분의 DenyCapV2 object ID를 추가한다. DenyCapV2 ID는 smart contract를 publish할 때 받는다. 이 예시에서는 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);
});