返回 SC 笔记
SC Day 61

Move/Sui - Transfer Policy + Kiosk框架 + NFT市场模块

### 1. Sui Kiosk 是什么?

2026-06-22
第三阶段:安全审计
KioskTransferPolicyNFT市场Sui去中心化商务

日期: 2026-06-22 方向: Move/Sui 阶段: 第三阶段:安全审计 标签: #Kiosk #TransferPolicy #NFT市场 #Sui #去中心化商务


今日目标

  1. 理解 Sui Kiosk 框架的设计理念和去中心化商务模型
  2. 掌握 TransferPolicy 的创建和自定义规则(版税、锁定等)
  3. 实现基于 Kiosk 的 NFT 市场模块(上架/购买/下架 + 版税强制执行)
  4. 对比 Kiosk 与传统 NFT 市场合约的架构差异

核心概念

1. Sui Kiosk 是什么?

Kiosk 是 Sui 原生的去中心化商务基础设施,它不是一个单一的市场合约,而是一套协议层原语,让每个用户拥有自己的"店铺"(Kiosk),在其中管理和交易资产。

传统 NFT 市场(如 OpenSea/Blur):
┌──────────────────────────────────┐
│       中心化市场合约              │
│  ┌─────┐ ┌─────┐ ┌─────┐       │
│  │NFT A│ │NFT B│ │NFT C│       │
│  └─────┘ └─────┘ └─────┘       │
│  所有 NFT 托管在同一个合约中     │
└──────────────────────────────────┘

Sui Kiosk 模型:
┌─────────┐  ┌─────────┐  ┌─────────┐
│ Alice的  │  │ Bob的   │  │ Carol的 │
│ Kiosk   │  │ Kiosk   │  │ Kiosk   │
│ ┌─────┐ │  │ ┌─────┐ │  │ ┌─────┐ │
│ │NFT A│ │  │ │NFT B│ │  │ │NFT C│ │
│ └─────┘ │  │ └─────┘ │  │ └─────┘ │
└─────────┘  └─────────┘  └─────────┘
每个用户有自己的 Kiosk,资产不离开用户控制

2. Kiosk 核心组件

Kiosk 框架由以下核心类型组成:

组件作用类比
Kiosk用户的个人店铺,存储和管理资产个人商店
KioskOwnerCapKiosk 的所有权凭证店铺钥匙
TransferPolicy<T>类型 T 的转移规则集合行业法规
TransferPolicyCap<T>管理 TransferPolicy 的凭证监管机构授权
TransferRequest<T>一次转移操作的请求/票据交易单据
PurchaseCap<T>购买某个商品的授权凭证购买许可

3. TransferPolicy 机制

TransferPolicy 是 Kiosk 框架的核心创新。它允许 NFT 创作者/发行方强制执行转移规则,无论在哪个市场交易:

交易流程:
1. 买家从 Kiosk 购买 NFT → 获得 TransferRequest
2. TransferRequest 必须满足所有 TransferPolicy 规则
3. 满足所有规则后,TransferRequest 才能被 confirm(确认)
4. 如果规则未满足,交易无法完成

规则类型:
┌──────────────────────────────────────────────┐
│             TransferPolicy<MyNFT>             │
│                                               │
│  Rule 1: royalty_rule  → 支付 2% 版税         │
│  Rule 2: kiosk_lock_rule → NFT 必须放入 Kiosk │
│  Rule 3: personal_kiosk_rule → 限个人 Kiosk   │
│                                               │
│  所有规则都满足 → 交易完成                      │
│  任一规则不满足 → 交易失败                      │
└──────────────────────────────────────────────┘

4. Kiosk 交易生命周期

卖家上架:
  kiosk::list<T>(&mut kiosk, &cap, item_id, price)
  → NFT 标记为"出售中",设定价格

买家购买:
  kiosk::purchase<T>(&mut kiosk, item_id, payment)
  → 返回 (T, TransferRequest<T>)
  → 买家获得 NFT 和一个待完成的转移请求

满足规则:
  royalty_rule::pay(&mut policy, &mut request, payment)
  → 向 TransferRequest 添加"已付版税"证明

确认转移:
  transfer_policy::confirm_request(&policy, request)
  → 验证所有规则都已满足
  → 转移请求被消耗(销毁)

买家存入自己的 Kiosk:
  kiosk::place<T>(&mut buyer_kiosk, &buyer_cap, nft)
  → NFT 进入买家的 Kiosk

5. 内置规则模块

Sui 框架提供了多种开箱即用的规则:

规则模块功能
royalty_rulesui::kiosk::royalty_rule强制支付版税(固定或百分比)
kiosk_lock_rulesui::kiosk::kiosk_lock_ruleNFT 购买后必须锁定在 Kiosk 中
personal_kiosk_rulesui::kiosk::personal_kiosk_rule只允许个人 Kiosk 接收
floor_price_rule自定义最低价格限制
allowlist_rule自定义白名单市场才能交易

代码实战

实战 1: 基础 NFT 类型定义

module marketplace::dragon_nft {
    use std::string::{Self, String};
    use sui::display;
    use sui::package;
    use sui::event;

    // === 类型定义 ===

    /// OTW (One-Time Witness) 用于初始化 Display 和 TransferPolicy
    public struct DRAGON_NFT has drop {}

    /// NFT 结构体 - 注意 `store` 能力使其可在 Kiosk 中使用
    public struct DragonNFT has key, store {
        id: UID,
        name: String,
        description: String,
        image_url: String,
        power: u64,
        element: String,  // fire, water, earth, wind
        level: u8,
    }

    // === 事件 ===
    public struct NFTMinted has copy, drop {
        id: ID,
        name: String,
        minter: address,
    }

    // === 管理能力 ===
    public struct AdminCap has key, store {
        id: UID,
    }

    /// 初始化函数 - 创建 Display 和 Publisher
    fun init(otw: DRAGON_NFT, ctx: &mut TxContext) {
        // 创建 Publisher 对象(用于创建 TransferPolicy)
        let publisher = package::claim(otw, ctx);

        // 创建 Display 模板
        let mut disp = display::new<DragonNFT>(&publisher, ctx);
        display::add(&mut disp, string::utf8(b"name"), string::utf8(b"{name}"));
        display::add(&mut disp, string::utf8(b"description"), string::utf8(b"{description}"));
        display::add(&mut disp, string::utf8(b"image_url"), string::utf8(b"{image_url}"));
        display::add(&mut disp, string::utf8(b"power"), string::utf8(b"{power}"));
        display::update_version(&mut disp);

        // 创建管理员能力
        let admin = AdminCap { id: object::new(ctx) };

        transfer::public_transfer(publisher, ctx.sender());
        transfer::public_transfer(disp, ctx.sender());
        transfer::public_transfer(admin, ctx.sender());
    }

    /// 铸造 NFT
    public fun mint(
        _admin: &AdminCap,
        name: String,
        description: String,
        image_url: String,
        power: u64,
        element: String,
        recipient: address,
        ctx: &mut TxContext,
    ) {
        let nft = DragonNFT {
            id: object::new(ctx),
            name,
            description,
            image_url,
            power,
            element,
            level: 1,
        };

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

        transfer::public_transfer(nft, recipient);
    }

    /// 升级 NFT(需要拥有 NFT)
    public fun level_up(nft: &mut DragonNFT) {
        assert!(nft.level < 100, 0);
        nft.level = nft.level + 1;
        nft.power = nft.power + 10;
    }

    // === 只读访问器 ===
    public fun name(nft: &DragonNFT): &String { &nft.name }
    public fun power(nft: &DragonNFT): u64 { nft.power }
    public fun level(nft: &DragonNFT): u8 { nft.level }
}

实战 2: TransferPolicy 设置与版税规则

module marketplace::royalty_setup {
    use sui::transfer_policy::{Self, TransferPolicy, TransferPolicyCap};
    use sui::package::Publisher;
    use sui::sui::SUI;
    use sui::coin::Coin;
    use marketplace::dragon_nft::DragonNFT;

    // === 常量 ===
    const ROYALTY_BPS: u16 = 500;       // 5% 版税 (基点)
    const MIN_ROYALTY_MIST: u64 = 100_000_000;  // 最低 0.1 SUI

    // === 错误码 ===
    const ERoyaltyTooLow: u64 = 1;
    const ENotPublisher: u64 = 2;

    /// 创建 TransferPolicy(由 NFT 发行方调用)
    /// Publisher 证明调用者是 DragonNFT 模块的发布者
    public fun create_policy(
        publisher: &Publisher,
        ctx: &mut TxContext,
    ): (TransferPolicy<DragonNFT>, TransferPolicyCap<DragonNFT>) {
        // transfer_policy::new 会验证 Publisher 与类型 T 匹配
        let (policy, cap) = transfer_policy::new<DragonNFT>(publisher, ctx);
        (policy, cap)
    }

    /// 创建并共享 TransferPolicy
    public fun create_and_share_policy(
        publisher: &Publisher,
        ctx: &mut TxContext,
    ) {
        let (policy, cap) = transfer_policy::new<DragonNFT>(publisher, ctx);
        // 共享 policy 让所有人都能使用
        transfer::public_share_object(policy);
        // cap 转给发行方,用于管理规则
        transfer::public_transfer(cap, ctx.sender());
    }
}

实战 3: 自定义版税规则模块

module marketplace::custom_royalty_rule {
    use sui::sui::SUI;
    use sui::coin::{Self, Coin};
    use sui::transfer_policy::{
        Self,
        TransferPolicy,
        TransferPolicyCap,
        TransferRequest,
    };

    // === 规则配置 ===

    /// Rule Witness - 每个规则需要一个唯一的 witness 类型
    public struct RoyaltyRule has drop {}

    /// 规则配置,存储在 TransferPolicy 中
    public struct RoyaltyConfig has store, drop {
        /// 版税百分比(基点,10000 = 100%)
        royalty_bps: u16,
        /// 最低版税金额(MIST)
        min_amount: u64,
        /// 版税接收地址
        beneficiary: address,
    }

    // === 错误码 ===
    const ERoyaltyNotPaid: u64 = 0;
    const ERoyaltyTooLow: u64 = 1;
    const EInvalidBps: u64 = 2;

    /// 添加版税规则到 TransferPolicy
    /// 只有持有 TransferPolicyCap 的发行方可以调用
    public fun add_rule<T: key + store>(
        policy: &mut TransferPolicy<T>,
        cap: &TransferPolicyCap<T>,
        royalty_bps: u16,
        min_amount: u64,
        beneficiary: address,
    ) {
        assert!(royalty_bps <= 10000, EInvalidBps);  // 最高 100%

        let config = RoyaltyConfig {
            royalty_bps,
            min_amount,
            beneficiary,
        };

        transfer_policy::add_rule(RoyaltyRule {}, policy, cap, config);
    }

    /// 支付版税 - 买家在购买后调用
    /// 这会向 TransferRequest 添加"已付版税"的证明(receipt)
    public fun pay_royalty<T: key + store>(
        policy: &mut TransferPolicy<T>,
        request: &mut TransferRequest<T>,
        payment: &mut Coin<SUI>,
        ctx: &mut TxContext,
    ) {
        // 获取规则配置
        let config: &RoyaltyConfig = transfer_policy::get_rule(
            RoyaltyRule {},
            policy,
        );

        // 计算版税金额
        let paid_amount = transfer_policy::paid(request);
        let mut royalty_amount = (
            (paid_amount as u128) * (config.royalty_bps as u128) / 10000u128
        ) as u64;

        // 确保不低于最低金额
        if (royalty_amount < config.min_amount) {
            royalty_amount = config.min_amount;
        };

        // 确保买家支付了足够的版税
        assert!(coin::value(payment) >= royalty_amount, ERoyaltyTooLow);

        // 从 payment 中分割版税金额
        let royalty_coin = coin::split(payment, royalty_amount, ctx);

        // 将版税转给受益人
        transfer::public_transfer(royalty_coin, config.beneficiary);

        // 向 TransferRequest 添加已付款证明(receipt)
        transfer_policy::add_receipt(RoyaltyRule {}, request);
    }
}

实战 4: 完整 NFT 市场模块

module marketplace::nft_marketplace {
    use sui::kiosk::{Self, Kiosk, KioskOwnerCap};
    use sui::transfer_policy::{Self, TransferPolicy, TransferRequest};
    use sui::sui::SUI;
    use sui::coin::{Self, Coin};
    use sui::event;
    use marketplace::dragon_nft::DragonNFT;
    use marketplace::custom_royalty_rule;

    // === 事件 ===
    public struct ItemListed has copy, drop {
        kiosk_id: ID,
        item_id: ID,
        price: u64,
        seller: address,
    }

    public struct ItemPurchased has copy, drop {
        kiosk_id: ID,
        item_id: ID,
        price: u64,
        buyer: address,
    }

    public struct ItemDelisted has copy, drop {
        kiosk_id: ID,
        item_id: ID,
        seller: address,
    }

    // === 错误码 ===
    const EInsufficientPayment: u64 = 0;
    const EItemNotListed: u64 = 1;

    // === 卖家操作 ===

    /// 创建新的 Kiosk(每个卖家一个)
    public fun create_kiosk(ctx: &mut TxContext): (Kiosk, KioskOwnerCap) {
        kiosk::new(ctx)
    }

    /// 将 NFT 放入 Kiosk
    public fun place_nft(
        kiosk: &mut Kiosk,
        cap: &KioskOwnerCap,
        nft: DragonNFT,
    ) {
        kiosk::place(kiosk, cap, nft);
    }

    /// 上架 NFT(设定价格)
    public fun list_nft(
        kiosk: &mut Kiosk,
        cap: &KioskOwnerCap,
        item_id: object::ID,
        price: u64,
        ctx: &TxContext,
    ) {
        kiosk::list<DragonNFT>(kiosk, cap, item_id, price);

        event::emit(ItemListed {
            kiosk_id: object::id(kiosk),
            item_id,
            price,
            seller: ctx.sender(),
        });
    }

    /// 放入 + 上架一步完成
    public fun place_and_list(
        kiosk: &mut Kiosk,
        cap: &KioskOwnerCap,
        nft: DragonNFT,
        price: u64,
        ctx: &TxContext,
    ) {
        let item_id = object::id(&nft);
        kiosk::place(kiosk, cap, nft);
        kiosk::list<DragonNFT>(kiosk, cap, item_id, price);

        event::emit(ItemListed {
            kiosk_id: object::id(kiosk),
            item_id,
            price,
            seller: ctx.sender(),
        });
    }

    /// 下架 NFT
    public fun delist_nft(
        kiosk: &mut Kiosk,
        cap: &KioskOwnerCap,
        item_id: object::ID,
        ctx: &TxContext,
    ) {
        kiosk::delist<DragonNFT>(kiosk, cap, item_id);

        event::emit(ItemDelisted {
            kiosk_id: object::id(kiosk),
            item_id,
            seller: ctx.sender(),
        });
    }

    // === 买家操作 ===

    /// 购买 NFT(自动处理版税)
    /// 这是一个复合函数,封装了完整的购买流程
    public fun buy_nft(
        seller_kiosk: &mut Kiosk,
        item_id: object::ID,
        mut payment: Coin<SUI>,
        policy: &mut TransferPolicy<DragonNFT>,
        buyer_kiosk: &mut Kiosk,
        buyer_cap: &KioskOwnerCap,
        ctx: &mut TxContext,
    ) {
        // 步骤 1: 从卖家 Kiosk 购买
        // purchase 返回 (NFT, TransferRequest)
        let (nft, mut transfer_request) = kiosk::purchase<DragonNFT>(
            seller_kiosk,
            item_id,
            coin::split(&mut payment, kiosk::purchase_cap_min_price(&kiosk::list_with_purchase_cap(seller_kiosk, /* ... */)), ctx),
        );

        // 步骤 2: 支付版税(满足 TransferPolicy 规则)
        custom_royalty_rule::pay_royalty(
            policy,
            &mut transfer_request,
            &mut payment,
            ctx,
        );

        // 步骤 3: 确认转移请求(验证所有规则已满足)
        transfer_policy::confirm_request(policy, transfer_request);

        // 步骤 4: 将 NFT 放入买家的 Kiosk
        kiosk::place(buyer_kiosk, buyer_cap, nft);

        // 步骤 5: 返还多余的付款
        if (coin::value(&payment) > 0) {
            transfer::public_transfer(payment, ctx.sender());
        } else {
            coin::destroy_zero(payment);
        };

        event::emit(ItemPurchased {
            kiosk_id: object::id(seller_kiosk),
            item_id,
            price: 0, // 简化,实际应记录价格
            buyer: ctx.sender(),
        });
    }

    /// 简化版购买流程(更接近实际使用)
    public fun buy_and_take(
        seller_kiosk: &mut Kiosk,
        item_id: object::ID,
        mut payment: Coin<SUI>,
        policy: &mut TransferPolicy<DragonNFT>,
        ctx: &mut TxContext,
    ): DragonNFT {
        // 从 Kiosk 购买
        let (nft, mut request) = kiosk::purchase<DragonNFT>(
            seller_kiosk,
            item_id,
            coin::split(&mut payment, 0, ctx), // 简化
        );

        // 支付版税
        custom_royalty_rule::pay_royalty(
            policy,
            &mut request,
            &mut payment,
            ctx,
        );

        // 确认请求
        transfer_policy::confirm_request(policy, request);

        // 退还余额
        if (coin::value(&payment) > 0) {
            transfer::public_transfer(payment, ctx.sender());
        } else {
            coin::destroy_zero(payment);
        };

        nft
    }

    // === 卖家提现 ===

    /// 从 Kiosk 提取收益
    public fun withdraw_profits(
        kiosk: &mut Kiosk,
        cap: &KioskOwnerCap,
        amount: option::Option<u64>,
        ctx: &mut TxContext,
    ): Coin<SUI> {
        kiosk::withdraw(kiosk, cap, amount, ctx)
    }
}

实战 5: Kiosk Lock 规则(强制 NFT 留在 Kiosk)

module marketplace::kiosk_lock_rule {
    use sui::kiosk::Kiosk;
    use sui::transfer_policy::{
        Self,
        TransferPolicy,
        TransferPolicyCap,
        TransferRequest,
    };

    /// Rule Witness
    public struct KioskLockRule has drop {}

    /// 空配置(这个规则不需要额外配置)
    public struct Config has store, drop {}

    /// 添加锁定规则
    public fun add_rule<T: key + store>(
        policy: &mut TransferPolicy<T>,
        cap: &TransferPolicyCap<T>,
    ) {
        transfer_policy::add_rule(KioskLockRule {}, policy, cap, Config {});
    }

    /// 满足锁定规则:证明 NFT 已被锁定在 Kiosk 中
    /// 买家必须调用 kiosk::lock 而不是 kiosk::place
    public fun prove<T: key + store>(
        request: &mut TransferRequest<T>,
        kiosk: &mut Kiosk,
    ) {
        // 验证 item 已锁定在目标 kiosk 中
        // 实际实现中会检查 kiosk 的内部状态
        transfer_policy::add_receipt(KioskLockRule {}, request);
    }
}

关键要点总结

Kiosk vs 传统 NFT 市场合约

维度传统市场(EVM)Sui Kiosk
资产托管NFT 转入市场合约NFT 留在用户的 Kiosk 中
版税执行市场自愿执行,可绕过协议层强制执行,无法绕过
市场锁定用户被锁定在特定市场任何市场都能与 Kiosk 交互
可组合性各市场互不兼容统一的 Kiosk 标准
上架成本Gas 费用(approve + list)仅修改 Kiosk 状态
安全性依赖市场合约安全资产始终在用户控制
规则扩展市场合约硬编码规则模块可插拔

TransferPolicy 的设计哲学

1. 创作者主权: NFT 发行方定义规则,不是市场定义
2. 强制执行: 规则在类型系统层面强制,无法绕过
3. 可组合: 多个规则可叠加,互不干扰
4. 市场无关: 规则适用于所有使用 Kiosk 的市场

完整购买流程图解

买家                    卖家 Kiosk              TransferPolicy
  │                        │                        │
  │── purchase() ─────────>│                        │
  │<── (NFT, Request) ─────│                        │
  │                        │                        │
  │── pay_royalty() ───────────────────────────────>│
  │<── receipt added ──────────────────────────────│
  │                        │                        │
  │── confirm_request() ──────────────────────────>│
  │<── request consumed ───────────────────────────│
  │                        │                        │
  │── place(buyer_kiosk) ──>│ (买家的 Kiosk)        │
  │                        │                        │

常见误区

误区 1: "Kiosk 只是换了个名字的市场合约"

纠正: Kiosk 是基础设施层,不是市场。每个用户有自己的 Kiosk,市场前端只是帮助发现和匹配买卖双方。资产所有权从未转移给市场。

误区 2: "TransferPolicy 可以被买家绕过"

纠正: 不可能。TransferRequest 是一个线性类型(必须被消耗),而 confirm_request 会验证所有规则的 receipt。Move 的类型系统保证了这一点 —— 你无法丢弃 TransferRequest,它没有 drop 能力。

误区 3: "每次交易都需要创建新的 Kiosk"

纠正: 用户通常只有一个 Kiosk,可以在其中管理所有 NFT。Kiosk 是长期存在的共享对象。

误区 4: "版税规则会降低流动性"

纠正: 虽然版税增加了交易成本,但 Kiosk 的标准化反而提高了流动性,因为所有市场共享同一个 Kiosk 生态系统。EVM 上版税被绕过导致的"版税战争"不会在 Sui 上发生。


面试关联

Q1: Sui Kiosk 如何解决 EVM NFT 市场的版税逃逸问题?

回答框架:

EVM 上版税执行依赖市场合约自愿遵守,Blur 等市场为了竞争选择不执行版税,导致创作者收入锐减。

Sui Kiosk 通过 TransferPolicy 在类型系统层面强制执行规则。NFT 类型的创建者通过 Publisher 对象创建 TransferPolicy,任何通过 Kiosk 完成的交易都必须满足所有规则。由于 TransferRequest 是线性类型(没有 drop 能力),它必须通过 confirm_request 被消耗,而 confirm_request 会验证所有规则 receipt。

这从根本上改变了版税执行模型:从"市场承诺执行"变为"协议强制执行"。

Q2: 如果你设计一个 NFT 市场,会选择传统合约方式还是 Kiosk?

回答思路:

  • 如果在 Sui 上,必选 Kiosk —— 这是 Sui 的标准做法
  • 从产品角度:Kiosk 降低了市场切换成本,需要在其他方面建立竞争壁垒(发现算法、社区、工具)
  • 从安全角度:Kiosk 更安全,资产不需要托管给市场合约
  • 从创作者角度:版税强制执行是巨大吸引力

参考资源

  1. Sui Kiosk 官方文档
  2. Transfer Policy 标准
  3. Kiosk SDK
  4. Sui Framework - kiosk.move 源码
  5. TransferPolicy 源码
  6. OriginByte NFT 标准(早期 Sui NFT 框架,后被 Kiosk 取代)