返回 SC 笔记
SC Day 54

Move/Sui 对象模型 - owned/shared/immutable objects + 转移策略 + NFT模块

### 1. Sui 对象模型总览

2026-05-24
第三阶段:安全审计
suiobject-modelnftownedsharedimmutabletransfer

日期: 2026-05-24 方向: Move / Sui 阶段: 第三阶段:安全审计 标签: #sui #object-model #nft #owned #shared #immutable #transfer


今日目标

  1. 深入理解 Sui 的三种对象类型及其使用场景
  2. 掌握对象创建、转移、共享和冻结操作
  3. 理解 Sui 对象模型与 EVM 存储模型的根本差异
  4. 实现一个完整的 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);
}

关键特性

  • 任何人可读,无人可写
  • 不需要共识(没有写入冲突)
  • 永久不可变,无法解冻
  • 适合协议配置、元数据、包信息等

对比总结

特性OwnedSharedImmutable
谁可以使用仅 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 PatternMintCap 控制铸造权限需要限制铸造的 NFT
Display Standardsui::display 定义展示模板所有需要展示的 NFT
Event Emission铸造/转移/销毁时发送事件前端监听和索引
Destroy Pattern解构 + object::delete安全销毁对象

Sui vs EVM 存储对比

维度EVMSui
基本单元Storage slot (32 bytes)Object (任意大小)
标识合约地址 + slot indexObject 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 对象(如 MintCapAdminCap)控制权限。只有持有该对象的地址才能调用需要权限的函数。优势是权限可以转移(转让 Cap 对象),可以细粒度控制(不同 Cap 对应不同权限),且由 runtime 强制执行(无法伪造对象)。


参考资源