Move/Sui 对象模型 - owned/shared/immutable objects + 转移策略 + NFT模块
### 1. Sui 对象模型总览
日期: 2026-05-24 方向: Move / Sui 阶段: 第三阶段:安全审计 标签: #sui #object-model #nft #owned #shared #immutable #transfer
今日目标
- 深入理解 Sui 的三种对象类型及其使用场景
- 掌握对象创建、转移、共享和冻结操作
- 理解 Sui 对象模型与 EVM 存储模型的根本差异
- 实现一个完整的 NFT 模块(mint / transfer / burn / 属性管理)
核心概念
1. Sui 对象模型总览
在 EVM 中,所有状态存储在合约的 storage slot 里,访问是顺序的。Sui 采用了完全不同的方法——对象模型(Object Model)。
每个对象有唯一的 ObjectID,是独立的存储单元。对象之间可以并行操作(如果没有共享依赖),这是 Sui 高吞吐量的关键。
EVM 模型:
┌─────────────────────────┐
│ Contract Storage │
│ slot 0: owner │ ← 所有用户共享一个状态树
│ slot 1: totalSupply │ ← 串行访问
│ slot 2: balances[...] │
└─────────────────────────┘
Sui 对象模型:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Object A │ │ Object B │ │ Object C │ ← 独立对象
│ owner: 0x1│ │ owner: 0x2│ │ shared │ ← 可并行操作
│ data: ... │ │ data: ... │ │ data: ... │
└──────────┘ └──────────┘ └──────────┘
2. 三种对象类型
2.1 Owned Objects(拥有对象)
被某个地址(EOA 或另一个对象)独占拥有。只有 owner 可以在交易中使用它。
// 创建 owned object
public entry fun create_and_transfer(ctx: &mut TxContext) {
let obj = MyObject {
id: object::new(ctx),
value: 42,
};
// transfer 到发送者 → 成为 owned object
transfer::transfer(obj, tx_context::sender(ctx));
}
关键特性:
- 只有 owner 可以在交易中引用
- 不需要共识(Sui 的 fast path)——极低延迟
- 适合个人资产(钱包余额、NFT、个人配置等)
2.2 Shared Objects(共享对象)
没有特定 owner,任何人都可以在交易中读写。
// 创建 shared object
public entry fun create_shared(ctx: &mut TxContext) {
let obj = SharedPool {
id: object::new(ctx),
total_liquidity: 0,
};
// share_object → 成为 shared object
transfer::share_object(obj);
}
关键特性:
- 任何人都可以在交易中使用
- 需要共识排序(因为可能有并发写入)——延迟稍高
- 适合共享状态(DEX 流动性池、拍卖合约、全局配置等)
- 一旦共享就不能变回 owned
2.3 Immutable Objects(不可变对象)
被冻结后永远不能修改,任何人都可以读取但无人可以写入。
// 创建 immutable object
public entry fun freeze_config(ctx: &mut TxContext) {
let config = ProtocolConfig {
id: object::new(ctx),
version: 1,
max_supply: 1000000,
};
// freeze_object → 永久不可变
transfer::freeze_object(config);
}
关键特性:
- 任何人可读,无人可写
- 不需要共识(没有写入冲突)
- 永久不可变,无法解冻
- 适合协议配置、元数据、包信息等
对比总结
| 特性 | Owned | Shared | Immutable |
|---|---|---|---|
| 谁可以使用 | 仅 owner | 任何人 | 任何人(只读) |
| 可修改 | 是(owner) | 是(任何人) | 否 |
| 需要共识 | 不需要 | 需要 | 不需要 |
| 延迟 | 极低(~400ms) | 较高(~2s) | 极低 |
| 可转移 | 是 | 否 | 否 |
| 适用场景 | 个人资产 | 共享池 | 元数据/配置 |
3. Wrapped Objects(包装对象)
对象可以作为另一个对象的字段存在,称为包装(wrapping)。被包装的对象在全局存储中"消失"——无法直接通过 ID 访问。
struct Wrapper has key {
id: UID,
inner: InnerObject, // InnerObject 被包装在 Wrapper 中
}
// InnerObject 必须有 store ability(才能被存入另一个 struct)
struct InnerObject has key, store {
id: UID,
data: u64,
}
4. 对象引用方式
在函数参数中,对象可以按不同方式传递,对应不同权限:
// 按值传递(获取所有权)——可以转移、销毁、包装
public fun consume(obj: MyObject) { ... }
// 不可变引用——只能读取
public fun read(obj: &MyObject): u64 { obj.value }
// 可变引用——可以修改
public fun modify(obj: &mut MyObject) { obj.value = 42; }
| 传递方式 | 权限 | 对象会被消费? | 适用场景 |
|---|---|---|---|
T | 完全所有权 | 是 | 转移、销毁、包装 |
&T | 只读 | 否 | 查询数据 |
&mut T | 读写 | 否 | 修改状态 |
代码实战
完整 NFT 模块实现
/// 一个功能完整的 NFT 模块,展示 Sui 对象模型的各种操作
module nft_gallery::nft {
// ===== 导入 =====
use std::string::{Self, String};
use sui::object::{Self, UID, ID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use sui::event;
use sui::url::{Self, Url};
use sui::package;
use sui::display;
// ===== 错误码 =====
const E_NOT_OWNER: u64 = 0;
const E_EMPTY_NAME: u64 = 1;
const E_ALREADY_LISTED: u64 = 2;
const E_NOT_LISTED: u64 = 3;
// ===== 事件 =====
struct NFTMinted has copy, drop {
id: ID,
name: String,
creator: address,
}
struct NFTTransferred has copy, drop {
id: ID,
from: address,
to: address,
}
struct NFTBurned has copy, drop {
id: ID,
burner: address,
}
// ===== One-Time Witness =====
/// 一次性 witness,用于创建 Publisher 对象
/// 结构体名必须和模块名大写匹配
struct NFT has drop {}
// ===== 对象定义 =====
/// NFT 主体对象
struct GalleryNFT has key, store {
id: UID,
/// NFT 名称
name: String,
/// NFT 描述
description: String,
/// 图片 URL
image_url: Url,
/// 创建者地址
creator: address,
/// 铸造编号
edition: u64,
}
/// NFT 集合信息(Shared Object)
/// 任何人都可以读取集合统计信息
struct Collection has key {
id: UID,
/// 集合名称
name: String,
/// 集合创建者
creator: address,
/// 已铸造总数
total_minted: u64,
/// 最大供应量(0 = 无限)
max_supply: u64,
}
/// 铸造权限凭证(Owned Object,只有持有者可以铸造)
struct MintCap has key, store {
id: UID,
collection_id: ID,
}
// ===== 初始化 =====
/// 模块发布时自动调用(只执行一次)
fun init(witness: NFT, ctx: &mut TxContext) {
// 创建 Publisher 对象(用于设置 Display)
let publisher = package::claim(witness, ctx);
// 设置 NFT 的 Display 模板
let mut display = display::new_with_fields<GalleryNFT>(
&publisher,
vector[
string::utf8(b"name"),
string::utf8(b"description"),
string::utf8(b"image_url"),
string::utf8(b"creator"),
],
vector[
string::utf8(b"{name}"),
string::utf8(b"{description}"),
string::utf8(b"{image_url}"),
string::utf8(b"Created by {creator}"),
],
ctx,
);
display::update_version(&mut display);
transfer::public_transfer(publisher, tx_context::sender(ctx));
transfer::public_transfer(display, tx_context::sender(ctx));
}
// ===== 集合管理 =====
/// 创建新的 NFT 集合
/// 返回 MintCap 给创建者(只有持有 MintCap 才能铸造)
public entry fun create_collection(
name: vector<u8>,
max_supply: u64,
ctx: &mut TxContext,
) {
let sender = tx_context::sender(ctx);
let collection_uid = object::new(ctx);
let collection_id = object::uid_to_inner(&collection_uid);
let collection = Collection {
id: collection_uid,
name: string::utf8(name),
creator: sender,
total_minted: 0,
max_supply,
};
let mint_cap = MintCap {
id: object::new(ctx),
collection_id,
};
// Collection 是 shared object(任何人可以查看统计)
transfer::share_object(collection);
// MintCap 是 owned object(只有创建者持有)
transfer::transfer(mint_cap, sender);
}
// ===== 铸造 =====
/// 铸造新 NFT
/// 需要:MintCap(权限凭证)+ Collection(共享对象,更新计数)
public entry fun mint(
_mint_cap: &MintCap, // 证明你有铸造权限(只读引用即可)
collection: &mut Collection, // 可变引用:需要更新 total_minted
name: vector<u8>,
description: vector<u8>,
image_url: vector<u8>,
recipient: address,
ctx: &mut TxContext,
) {
let name_str = string::utf8(name);
assert!(!string::is_empty(&name_str), E_EMPTY_NAME);
// 检查供应量
if (collection.max_supply > 0) {
assert!(
collection.total_minted < collection.max_supply,
E_ALREADY_LISTED
);
};
collection.total_minted = collection.total_minted + 1;
let nft_uid = object::new(ctx);
let nft_id = object::uid_to_inner(&nft_uid);
let nft = GalleryNFT {
id: nft_uid,
name: name_str,
description: string::utf8(description),
image_url: url::new_unsafe_from_bytes(image_url),
creator: tx_context::sender(ctx),
edition: collection.total_minted,
};
event::emit(NFTMinted {
id: nft_id,
name: nft.name,
creator: nft.creator,
});
// 转移给接收者(成为 owned object)
transfer::transfer(nft, recipient);
}
// ===== 转移 =====
/// 转移 NFT 给新所有者
/// 参数类型是 GalleryNFT(按值传递,获取所有权)
public entry fun transfer_nft(
nft: GalleryNFT,
recipient: address,
ctx: &TxContext,
) {
event::emit(NFTTransferred {
id: object::uid_to_inner(&nft.id),
from: tx_context::sender(ctx),
to: recipient,
});
transfer::transfer(nft, recipient);
}
// ===== 销毁 =====
/// 销毁(burn)NFT
/// 参数类型是 GalleryNFT(按值传递,获取所有权后销毁)
public entry fun burn(nft: GalleryNFT, ctx: &TxContext) {
event::emit(NFTBurned {
id: object::uid_to_inner(&nft.id),
burner: tx_context::sender(ctx),
});
// 解构 NFT 并删除 UID
let GalleryNFT {
id,
name: _,
description: _,
image_url: _,
creator: _,
edition: _,
} = nft;
object::delete(id); // 从全局存储中删除
}
// ===== 只读访问器 =====
public fun name(nft: &GalleryNFT): &String { &nft.name }
public fun description(nft: &GalleryNFT): &String { &nft.description }
public fun creator(nft: &GalleryNFT): address { nft.creator }
public fun edition(nft: &GalleryNFT): u64 { nft.edition }
public fun total_minted(collection: &Collection): u64 { collection.total_minted }
// ===== 修改函数 =====
/// 更新 NFT 描述(只有 creator 可以)
public entry fun update_description(
nft: &mut GalleryNFT,
new_description: vector<u8>,
ctx: &TxContext,
) {
assert!(nft.creator == tx_context::sender(ctx), E_NOT_OWNER);
nft.description = string::utf8(new_description);
}
// ===== 测试 =====
#[test_only]
use sui::test_scenario;
#[test]
fun test_full_lifecycle() {
let creator = @0xA;
let buyer = @0xB;
// 创建集合
let mut scenario = test_scenario::begin(creator);
{
create_collection(
b"Test Collection",
100, // max supply
test_scenario::ctx(&mut scenario),
);
};
// 铸造 NFT
test_scenario::next_tx(&mut scenario, creator);
{
let mint_cap = test_scenario::take_from_sender<MintCap>(&scenario);
let mut collection = test_scenario::take_shared<Collection>(&scenario);
mint(
&mint_cap,
&mut collection,
b"NFT #1",
b"First NFT",
b"https://example.com/nft1.png",
creator,
test_scenario::ctx(&mut scenario),
);
assert!(total_minted(&collection) == 1, 0);
test_scenario::return_to_sender(&scenario, mint_cap);
test_scenario::return_shared(collection);
};
// 转移 NFT
test_scenario::next_tx(&mut scenario, creator);
{
let nft = test_scenario::take_from_sender<GalleryNFT>(&scenario);
assert!(*name(&nft) == string::utf8(b"NFT #1"), 1);
assert!(edition(&nft) == 1, 2);
transfer_nft(nft, buyer, test_scenario::ctx(&scenario));
};
// 验证 buyer 收到 NFT
test_scenario::next_tx(&mut scenario, buyer);
{
let nft = test_scenario::take_from_sender<GalleryNFT>(&scenario);
assert!(creator(&nft) == creator, 3);
// 销毁
burn(nft, test_scenario::ctx(&scenario));
};
test_scenario::end(scenario);
}
}
Sui Move 项目结构
my_nft_project/
├── Move.toml # 包配置文件
├── sources/
│ └── nft.move # 源代码
└── tests/
└── nft_tests.move # 测试(也可内联在源文件中)
# Move.toml
[package]
name = "nft_gallery"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" }
[addresses]
nft_gallery = "0x0"
关键要点总结
对象类型选择决策树
需要创建新对象?
├── 只有一个人使用?→ Owned Object (transfer::transfer)
├── 多人都需要修改?→ Shared Object (transfer::share_object)
├── 永远不会改变?→ Immutable Object (transfer::freeze_object)
└── 作为另一个对象的一部分?→ Wrapped(直接作为字段)
NFT 设计模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| Capability Pattern | 用 MintCap 控制铸造权限 | 需要限制铸造的 NFT |
| Display Standard | 用 sui::display 定义展示模板 | 所有需要展示的 NFT |
| Event Emission | 铸造/转移/销毁时发送事件 | 前端监听和索引 |
| Destroy Pattern | 解构 + object::delete | 安全销毁对象 |
Sui vs EVM 存储对比
| 维度 | EVM | Sui |
|---|---|---|
| 基本单元 | Storage slot (32 bytes) | Object (任意大小) |
| 标识 | 合约地址 + slot index | Object ID (32 bytes) |
| 所有权 | 合约内逻辑控制 | Runtime 强制执行 |
| 并行性 | 合约级串行 | 对象级并行 |
| 读取方式 | SLOAD 操作码 | 函数参数传入 |
| 写入方式 | SSTORE 操作码 | 修改 &mut 引用 |
常见误区
误区 1:"Shared object 和 owned object 性能一样"
错误!Owned object 的交易可以跳过共识(Sui 的 fast path),延迟约 400ms。Shared object 需要通过共识排序,延迟约 2 秒。在设计时应尽量使用 owned object。
误区 2:"可以把 shared object 变回 owned object"
不行。share_object 是不可逆的操作。一旦共享,就永远是共享的。设计时要慎重选择。
误区 3:"Sui 的 NFT 和 ERC-721 是一样的"
Sui 的 NFT 天然就是独立对象,有内建所有权,不需要 mapping(uint256 => address) 来追踪。转移 NFT 是移动整个对象,而不是修改映射中的条目。这意味着 Sui NFT 的转移可以完全并行。
误区 4:"对象 ID 可以预测"
Sui 的 Object ID 由 object::new(ctx) 生成,包含交易哈希和计数器。虽然不完全随机,但在同一个交易中创建多个对象时 ID 是不同的。不应依赖 ID 的可预测性来设计业务逻辑。
面试关联
Q: 解释 Sui 的对象模型,以及为什么它比 EVM 的存储模型更适合高吞吐量?
简短回答:Sui 将状态拆分为独立对象,每个有唯一 ID 和 owner。不涉及共享状态的交易可以并行处理且跳过共识,实现极低延迟。
详细回答:
- EVM 模型:所有状态在合约内共享 → 同一合约的交易必须串行 → 吞吐量瓶颈
- Sui 模型:状态拆分为独立对象 → owned object 之间无依赖 → 可并行处理
- Fast Path:只涉及 owned object 的交易不需要共识(~400ms)
- 设计启示:将个人状态设计为 owned object,只在必要时使用 shared object
- 实际效果:对于 NFT 铸造、Token 转账等高频操作,Sui 可以实现远超 EVM 的吞吐量
Q: 在设计 DeFi 应用时,如何决定使用 owned vs shared object?
回答:
- Owned:用户个人的余额、NFT、配置、票据等个人资产
- Shared:流动性池、订单簿、拍卖合约等需要多方交互的状态
- Immutable:协议参数、合约元数据、已确定的配置
- 设计原则:尽量减少 shared object 的使用,将热点数据拆分为多个独立对象以提高并行度
Q: Sui 的 Capability Pattern 如何实现权限控制?
回答:通过创建特殊的 capability 对象(如 MintCap、AdminCap)控制权限。只有持有该对象的地址才能调用需要权限的函数。优势是权限可以转移(转让 Cap 对象),可以细粒度控制(不同 Cap 对应不同权限),且由 runtime 强制执行(无法伪造对象)。
参考资源
- Sui Object Model — 官方对象模型文档
- Sui Move Examples: NFT — 官方 NFT 示例
- Sui Display Standard — Display 标准文档
- Sui Object Ownership — 所有权详解
- Move Book: Object Wrapping — 对象包装
- Sui vs EVM Architecture — 架构对比文章
- Mysten Labs: Why Objects? — 对象模型设计理念