본문으로 건너뛰기

소울바운드 NFT 예시

소울바운드 NFT는 전송할 수 없는 NFT이다. NFT가 Sui account에 민트된 뒤에는 해당 NFT가 그 account에 바인딩되어 전송할 수 없게 된다. 이 구현은 Sui framework의 transfer 함수에 있는 custom logic을 활용한다. sui::transfer module에는 object를 전송하는 2개 함수인 transfer::transfertransfer::public_transfer가 있다.

일반적으로 Sui에서 새 NFT나 object type을 정의할 때는 전송 함수를 만들 필요가 없는데, Sui Framework가 누구나 object를 전송하는 데 사용할 수 있는 transfer::public_transfer를 제공하기 때문이다. 하지만 transfer::public_transfer는 전송되는 object가 keystore ability를 가져야 한다. 따라서 새 NFT type을 key ability는 있지만 store ability는 없게 정의하면, holder는 transfer::public_transfer를 사용할 수 없다. 그 결과 소울바운드 NFT가 된다.

Sui에서는 NFT에 대한 custom transfer logic도 만들 수 있다. transfer::transfer 함수에는 전송된 object가 transfer가 호출된 module에서 정의되었는지 보장하도록 Sui Move bytecode verifier가 수행하는 custom rule이 있다. struct 정의에서 store ability를 제거하면 transfer::public_transfer를 사용할 수 없게 되지만, transfer::transfer는 그 object type을 정의한 module 안에서 사용하는 한 여전히 사용할 수 있다. 이를 통해 module owner는 자신의 소울바운드 NFT에 대해 custom transfer logic을 제공할 수 있다.

Example

다음 예시는 Sui에서 기본 소울바운드 NFT를 생성한다. TestnetSoulboundNFT struct는 id, name, description, url field를 사용해 NFT를 정의한다.

public struct TestnetSoulboundNFT has key {
id: UID,
name: string::String,
description: string::String,
url: Url,
}

TestnetSoulboundNFT struct는 key ability로 정의되지만 store ability 없이 정의된다. 이는 transfer::public_transfer로 이를 전송할 수 없음을 뜻한다. 대신 같은 module에 구현된 custom transfer logic과 함께 transfer::transfer를 사용한다.

이 예시는 또한 transfer::transfer 함수를 사용해 custom transfer logic을 제공하는 방법도 보여준다. 여기는 NFT stat 재설정이나 결제 요구와 같은 추가 logic을 넣을 수 있는 지점이다. NFT가 완전히 소울바운드 상태라면 이 기능을 제공하지 않는다.

/// Transfer `nft` to `recipient`
/// Do not include this if you want the NFT fully soulbound
public fun transfer(nft: TestnetSoulboundNFT, recipient: address, _: &mut TxContext) {
// Add custom logic for transferring the NFT
transfer::transfer(nft, recipient)
}
Click to open

testnet_soulbound_nft.move

module examples::testnet_soulbound_nft;

use std::string;
use sui::event;
use sui::url::{Self, Url};

/// An example soulbound NFT that can be minted by anybody
///
/// Removing the `store` ability prevents this NFT
/// from being transferred unless this module provides
/// a transfer function.
public struct TestnetSoulboundNFT has key {
id: UID,
/// Name for the token
name: string::String,
/// Description of the token
description: string::String,
/// URL for the token
url: Url,
// TODO: allow custom attributes
}

// ===== Events =====

public struct NFTMinted has copy, drop {
// The Object ID of the NFT
object_id: ID,
// The creator of the NFT
creator: address,
// The name of the NFT
name: string::String,
}

// ===== Public view functions =====

/// Get the NFT's `name`
public fun name(nft: &TestnetSoulboundNFT): &string::String {
&nft.name
}

/// Get the NFT's `description`
public fun description(nft: &TestnetSoulboundNFT): &string::String {
&nft.description
}

/// Get the NFT's `url`
public fun url(nft: &TestnetSoulboundNFT): &Url {
&nft.url
}

// ===== Entrypoints =====

#[allow(lint(self_transfer))]
/// Create a new devnet_nft
public fun mint_to_sender(
name: vector<u8>,
description: vector<u8>,
url: vector<u8>,
ctx: &mut TxContext,
) {
let sender = ctx.sender();
let nft = TestnetSoulboundNFT {
id: object::new(ctx),
name: string::utf8(name),
description: string::utf8(description),
url: url::new_unsafe_from_bytes(url),
};

event::emit(NFTMinted {
object_id: object::id(&nft),
creator: sender,
name: nft.name,
});

transfer::transfer(nft, sender);
}

/// Transfer `nft` to `recipient`
/// Do not include this if you want the NFT fully soulbound
public fun transfer(nft: TestnetSoulboundNFT, recipient: address, _: &mut TxContext) {
// Add custom logic for transferring the NFT
transfer::transfer(nft, recipient)
}

/// Update the `description` of `nft` to `new_description`
public fun update_description(
nft: &mut TestnetSoulboundNFT,
new_description: vector<u8>,
_: &mut TxContext,
) {
nft.description = string::utf8(new_description)
}

/// Permanently delete `nft`
public fun burn(nft: TestnetSoulboundNFT, _: &mut TxContext) {
let TestnetSoulboundNFT { id, name: _, description: _, url: _ } = nft;
id.delete()
}