본문으로 건너뛰기

Permissioned Assets 액션

Permissioned Asset Standard (PAS)는 Sui의 permissioned 자산 프레임워크이다. 서로 다른 타입의 자산은 Accounts 아래에 존재하며(주소마다 하나, type과 독립적), 동일한 programmable 트랜잭션 블록 (PTB) 안에서 승인되고 해결되어야 하는 hot potato 요청을 통해서만 Account 사이를 이동할 수 있다.

모든 액션은 같은 pattern을 따른다.

Account 생성

Account creation은 permissionless이다. 주소마다 하나의 Account가 있으며, Account는 어떤 자산 type이든 보유할 수 있다.

account::create_and_share(&mut namespace, @0xAlice);
account::create_and_share(&mut namespace, @0xBob);

잔액 transfer

permissioned 자산의 잔액을 한 Account에서 다른 Account로 전송할 수 있다. 여기에는 소유자 Auth proof가 필요하다.

요청는 다음 필드를 포함한다.

FieldDescription
senderWallet 또는 객체 주소(Account 주소가 아님)
recipientWallet 또는 객체 주소(Account 주소가 아님)
sender_account_idsource Account의 ID
recipient_account_iddestination Account의 derived ID
fundstransfer되는 T

Resolution(Balance<C>의 경우): send_funds::resolve_balance(request, &policy)는 approval을 검증하고 balance::send_funds를 통해 잔액을 recipient Account로 직접 보낸다.

resolve_balance 함수는 잔액을 직접 보낸다. caller는 이를 다시 받지 않는다. 잔액은 바로 destination Account로 이동한다.

다음 예시는 완전한 transfer flow를 보여준다.

// 1. Create auth proof
let auth = account::new_auth(ctx);

// 2. Create the transfer request (withdraws Balance from sender account)
let mut request = from_account.send_balance<MY_COIN>(
&auth,
&to_account,
amount,
ctx,
);

// 3. Approve with your witness
request.approve(MyTransferApproval());

// 4. Resolve — sends Balance to recipient account
send_funds::resolve_balance(request, &policy);

registry-gated transfer 구현은 KYC example kyc_registry를 참조한다.

잔액 clawback

Account에서 토큰을 강제로 출금할 수 있다. 이는 admin 액션이므로 Auth가 필요하지 않다. 정책에는 clawback_allowed: true가 있어야 한다.

요청는 다음 필드를 포함한다.

FieldDescription
ownersource Account의 Wallet 또는 객체 주소
account_idsource Account의 ID
fundsclawback되는 T

Resolution: clawback_funds::resolve(request, &policy)는 approval과 policy.clawback_allowed를 검증한 뒤 T를 caller에게 반환한다.

send_funds와 달리 caller가 자금을 받고, 이를 어떻게 처리할지(burn, 다른 곳에 예치 등) 결정한다.

다음 예시는 clawback flow를 보여준다.

// 1. Create clawback request (no Auth needed)
let mut request = from_account.clawback_balance<MY_COIN>(amount, ctx);

// 2. Approve
request.approve(MyClawbackApproval());

// 3. Resolve — returns the Balance to the caller
let balance: Balance<MY_COIN> = clawback_funds::resolve(request, &policy);

// 4. Do something with the balance (deposit elsewhere, burn, and so on)
to_account.deposit_balance(balance);

clawback-to-burn 구현은 KYC example treasury::burn를 참조한다.

잔액 unlock

closed-loop system에서 토큰을 완전히 제거할 수 있다.

주의

unlock_funds를 활성화하면 자산이 closed-loop system을 완전히 떠나고 이후 모든 transfer control을 bypass할 수 있다. unlock된 자산은 PAS restriction이 없는 일반 onchain 객체가 된다. 사용자가 managed system에서 자산을 출금해야 하는 사용 사례가 명시적으로 필요한 경우에만 이 액션을 허용한다.

resolution path는 2개다.

PathWhen to useResolution
unlock_funds::resolve(request, &policy)Managed 자산(Policy<T>가 존재)matching approval 필요
unlock_funds::resolve_unrestricted_balance(request, &namespace)Unmanaged 잔액(Policy 없음)approval 불필요

unrestricted path는 Balance<C> 전용이다. SUI처럼 실수로 Account에 들어갈 수 있는 자산을 위해 존재한다. 해당 type에 Policy<Balance<C>>가 있으면 managed 자산 control을 bypass할 수 없으므로 중단된다.

Managed 자산(정책 존재)

다음 예시는 managed 자산의 unlock flow를 보여준다.

let auth = account::new_auth(ctx);
let mut request = account.unlock_balance<MY_COIN>(&auth, amount, ctx);

request.approve(MyUnlockApproval());
let balance: Balance<MY_COIN> = unlock_funds::resolve(request, &policy);
// Balance is now outside the system

registry-gated unlock(redemption) 구현은 Loyalty example treasury::redeem을 참조한다.

Unmanaged 자산(정책 없음)

예를 들어 SUI가 실수로 Account로 전송된 경우:

let auth = account::new_auth(ctx);
let request = account.unlock_balance<SUI>(&auth, amount, ctx);

// No approval needed — resolves with empty approval set
let balance: Balance<SUI> = unlock_funds::resolve_unrestricted_balance(request, &namespace);

잔액 예치

Account에 토큰을 예치할 수 있다. 요청는 필요하지 않다.

account.deposit_balance(balance);

Account 주소로 직접 보낼 수도 있다. 이는 트랜잭션에서 Account 객체를 사용할 수 있기 전 유용하다.

balance.send_funds(namespace.account_address(owner));

Move에서 액션 해결(contract 내부)

contract는 자체 함수 안에서 승인와 해결을 호출한다. 잔액은 사용자의 control 밖으로 나가지 않는다.

public fun burn(
policy: &Policy<Balance<MY_COIN>>,
cap: &mut TreasuryCap<MY_COIN>,
mut request: Request<ClawbackFunds<Balance<MY_COIN>>>,
ctx: &mut TxContext,
) {
// Approve inside your contract
request.approve(MyClawbackApproval());

// Resolve — balance returned to this function
let balance = clawback_funds::resolve(request, policy);

// Burn the balance
cap.burn(balance.into_coin(ctx));
}

Use case: Burn, seize 또는 contract가 잔액을 필요로 하는 모든 operation. treasury::burn (KYC)와 treasury::redeem (Loyalty)을 참조한다.

PTB에서 액션 해결(클라이언트-side)

single PTB 안에서 여러 Move call에 걸쳐 요청을 만들고 승인한 뒤, 별도 Move call로 해결한다.

Use case: 컴플라이언스 contract가 승인하고 PAS가 delivery를 처리하는 transfer.

정보

Step 3과 4는 같은 PTB 안에 있어야 한다. 요청는 hot potato이다. 트랜잭션이 resolution 없이 끝나면 중단된다.

예제 패키지

ExampleActionsApproval pattern
Loyaltysend_funds(permissionless), unlock_funds(per-주소 registry)Transfer는 open 상태이고 point는 system에 남는다. Redemption은 RedeemRegistry로 gate되고 Move에서 해결된다. unlock되면 자금은 managed system을 완전히 떠난다.
KYCsend_funds(KYC registry), clawback_funds(issuer)모든 transfer는 recipient를 KYCRegistry에 대해 check한다. Clawback은 무조건 승인되어 issuer가 어떤 계정의 토큰도 burn할 수 있게 한다.