NFTs, Digital Assets, Kiosk & Transfer Policies

On Sui, any key + store object can be an NFT — there is no mandatory interface.

34.1 NFTs on Sui

On Sui, any key + store object can be an NFT — there is no mandatory interface. Combine it with Display for rendering and Kiosk for trading.

module nft::hero;

use std::string::String;
use sui::event;

public struct Hero has key, store {
    id: UID,
    name: String,
    image_url: String,
    power: u64,
}

public struct MintCap has key { id: UID, minted: u64, max_supply: u64 }

public struct HeroMinted has copy, drop { id: ID, to: address }

const ESoldOut: u64 = 0;

fun init(ctx: &mut TxContext) {
    transfer::transfer(MintCap { id: object::new(ctx), minted: 0, max_supply: 10_000 }, ctx.sender());
}

public fun mint(cap: &mut MintCap, name: String, image_url: String, to: address, ctx: &mut TxContext) {
    assert!(cap.minted < cap.max_supply, ESoldOut);
    cap.minted = cap.minted + 1;
    let hero = Hero { id: object::new(ctx), name, image_url, power: 1 };
    event::emit(HeroMinted { id: object::id(&hero), to });
    transfer::public_transfer(hero, to);
}

public fun burn(hero: Hero) {
    let Hero { id, name: _, image_url: _, power: _ } = hero;
    id.delete();
}

34.2 Kiosk (Sui)

Kiosk (sui::kiosk) is a shared object owned by a user that stores, lists, and sells assets while enforcing the creator's transfer policy (e.g., royalties). Marketplaces integrate with kiosks rather than taking custody of NFTs.

use sui::kiosk::{Self, Kiosk, KioskOwnerCap};
use sui::transfer_policy::{Self, TransferPolicy};
use sui::coin::Coin;
use sui::sui::SUI;

/// Seller: create a kiosk, place and list an item.
public fun open_and_list(hero: Hero, price: u64, ctx: &mut TxContext) {
    let (mut kiosk, cap) = kiosk::new(ctx);
    let id = object::id(&hero);
    kiosk.place(&cap, hero);
    kiosk.list<Hero>(&cap, id, price);
    transfer::public_share_object(kiosk);
    transfer::public_transfer(cap, ctx.sender());
}

/// Buyer: purchase, satisfy the policy, take the item.
public fun buy(
    kiosk: &mut Kiosk,
    policy: &TransferPolicy<Hero>,
    id: ID,
    payment: Coin<SUI>,
): Hero {
    let (hero, request) = kiosk.purchase<Hero>(id, payment);   // request is a hot potato
    // ...satisfy rules here (e.g., pay royalty) which add receipts to `request`...
    policy.confirm_request(request);
    hero
}

34.3 Transfer policies

The creator (holding the Publisher) creates a TransferPolicy<T>; every Kiosk purchase produces a TransferRequest<T> that must be confirmed against it:

Shared object listing required rules for T Lets the creator add/remove rules and withdraw collected fees

Hot potato from purchase; rules add receipts; confirm_request checks all receipts

Royalty, kiosk lock (item must stay in a kiosk), personal kiosk, floor price — reference implementations in Mysten's kiosk package

Place an item so it can never leave kiosks (enforced royalties)

fun init(otw: HERO, ctx: &mut TxContext) {
    let publisher = sui::package::claim(otw, ctx);
    let (policy, policy_cap) = transfer_policy::new<Hero>(&publisher, ctx);
    transfer::public_share_object(policy);
    transfer::public_transfer(policy_cap, ctx.sender());
    transfer::public_transfer(publisher, ctx.sender());
}

34.4 Aptos Digital Asset standard (Token Objects)

Aptos NFTs use aptos_token_objects (address 0x4), built on the Object framework: a collection object and token objects.

Aptos DA concept create_fixed_collection / create_unlimited_collection; name unique per creator

create_named_token (deterministic address), create (unique address)

Add your own resources to the token object (stats, equipment) using the object signer

Disable ungated transfer with TransferRef aptos_token_objects::royalty (marketplace-enforced by convention)

aptos_token_objects::aptos_token offers ready-made mint/burn/property maps

0x3::token (Token v1) — older standard

module my_addr::heroes {
    use std::option;
    use std::string::{String, utf8};
    use aptos_token_objects::collection;
    use aptos_token_objects::token;
    use aptos_token_objects::royalty;

    const COLLECTION: vector<u8> = b"Move Heroes";

    fun init_module(creator: &signer) {
        collection::create_fixed_collection(
            creator,
            utf8(b"Heroes from the Move course"),
            10_000,                                         // max supply
            utf8(COLLECTION),
            option::some(royalty::create(5, 100, @my_addr)), // 5% royalty
            utf8(b"https://example.com/collection.json"),
        );
    }

    public entry fun mint(creator: &signer, name: String, uri: String, to: address) {
        let constructor_ref = token::create_named_token(
            creator,
            utf8(COLLECTION),
            utf8(b"A Move hero"),
            name,
            option::none(),        // royalty: inherit from collection
            uri,
        );
        let transfer_ref = aptos_framework::object::generate_transfer_ref(&constructor_ref);
        let linear = aptos_framework::object::generate_linear_transfer_ref(&transfer_ref);
        a
…