Move/Sui - Transfer Policy + Kiosk框架 + NFT市场模块
### 1. Sui Kiosk 是什么?
日期: 2026-06-22 方向: Move/Sui 阶段: 第三阶段:安全审计 标签: #Kiosk #TransferPolicy #NFT市场 #Sui #去中心化商务
今日目标
- 理解 Sui Kiosk 框架的设计理念和去中心化商务模型
- 掌握 TransferPolicy 的创建和自定义规则(版税、锁定等)
- 实现基于 Kiosk 的 NFT 市场模块(上架/购买/下架 + 版税强制执行)
- 对比 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 | 用户的个人店铺,存储和管理资产 | 个人商店 |
KioskOwnerCap | Kiosk 的所有权凭证 | 店铺钥匙 |
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_rule | sui::kiosk::royalty_rule | 强制支付版税(固定或百分比) |
kiosk_lock_rule | sui::kiosk::kiosk_lock_rule | NFT 购买后必须锁定在 Kiosk 中 |
personal_kiosk_rule | sui::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 更安全,资产不需要托管给市场合约
- 从创作者角度:版税强制执行是巨大吸引力
参考资源
- Sui Kiosk 官方文档
- Transfer Policy 标准
- Kiosk SDK
- Sui Framework - kiosk.move 源码
- TransferPolicy 源码
- OriginByte NFT 标准(早期 Sui NFT 框架,后被 Kiosk 取代)