返回 SC 笔记
SC Day 52

Move/Sui 基础语法 - module/struct/function/acquires + Counter模块

### 1. Move 语言设计哲学

2026-05-22
第三阶段:安全审计
movesuimodulestructobject-modelcounter

日期: 2026-05-22 方向: Move / Sui 阶段: 第三阶段:安全审计 标签: #move #sui #module #struct #object-model #counter


今日目标

  1. 理解 Move 语言的核心设计哲学——资源安全性
  2. 掌握 Move 的基本语法:module、struct、function、abilities
  3. 理解 Sui Move 与 Aptos Move 的关键差异
  4. 实现一个完整的 Counter 模块(Sui Move 版本)

核心概念

1. Move 语言设计哲学

Move 是专为区块链设计的编程语言,最初由 Facebook(Meta)Diem 项目创建。其核心设计原则:

原则说明与 Solidity 对比
资源安全资源不能被复制或隐式丢弃Solidity 中 token 余额只是数字
类型线性每个资源有明确的所有权和生命周期Solidity 无此概念
形式化验证内置 Move Prover 支持Solidity 依赖外部工具
模块封装struct 只能在定义模块内操作Solidity 合约间可直接操作

Move 的"资源"概念对应现实世界的资产——你不能凭空复制一张钞票,也不能让它消失。这种安全性在语言层面就得到保证。

2. Module 声明

Module 是 Move 代码的基本组织单元,类似 Solidity 中的 contract:

// Sui Move module 声明
module counter::counter {
    // module 内容
}

// 格式: module <package_address>::<module_name>
// 在 Sui 中,package_address 通常在 Move.toml 中配置

Aptos Move vs Sui Move 的 module 差异

// Aptos Move: 使用地址前缀
module 0x1::coin {
    // ...
}

// Sui Move: 使用包名前缀
module my_package::coin {
    // ...
}

3. Struct 与 Abilities

Struct 是 Move 中定义数据类型的方式。每个 struct 可以拥有不同的 abilities(能力),控制其行为:

四种 Abilities

Ability含义说明
copy可以被复制值类型行为,如整数
drop可以被隐式丢弃离开作用域时自动销毁
store可以存储在其他 struct 中可作为字段被包含
key可以作为存储中的顶层对象Sui 中表示是一个 Object

Abilities 组合含义

// 纯数据类型:可复制、可丢弃、可存储
struct Point has copy, drop, store {
    x: u64,
    y: u64,
}

// 资源类型(Sui Object):有key + store,不能copy/drop
// 这意味着它不能被复制,不能被随意丢弃——必须显式处理
struct Counter has key, store {
    id: UID,   // Sui 对象必须有 id 字段
    value: u64,
}

// 一次性使用类型:只有 drop
// 常用于 witness pattern
struct COUNTER has drop {}

关键规则

没有 copy → 不能复制(资产安全!)
没有 drop → 不能丢弃(必须显式消费或转移)
没有 store → 不能存入其他结构体
没有 key → 不能作为独立对象存在于存储中

4. Function 类型

Move 中有多种函数可见性修饰符:

module example::demo {
    // 私有函数:仅模块内部可调用
    fun internal_helper(): u64 {
        42
    }

    // 公共函数:任何模块都可以调用
    public fun get_value(): u64 {
        internal_helper()
    }

    // entry 函数:可以作为交易的入口点(从客户端直接调用)
    // entry 函数不能有返回值(在 Sui Move 中可以有返回值但有限制)
    entry fun do_something() {
        // ...
    }

    // public entry:既可以被其他模块调用,也可以作为交易入口
    public entry fun public_action() {
        // ...
    }

    // friend 函数(Sui Move):仅允许指定的 friend 模块调用
    public(friend) fun friend_only(): u64 {
        100
    }
}
可见性模块内部其他模块交易入口
fun (private)YesNoNo
public funYesYesNo
entry funYesNoYes
public entry funYesYesYes
public(friend) funYesFriend 模块No

5. Aptos Move 的全局存储操作

在 Aptos Move 中,资源存储在账户地址下,使用以下全局操作:

// Aptos Move 风格(非 Sui)
module 0x1::counter {
    struct Counter has key {
        value: u64,
    }

    // move_to: 将资源存储到账户下
    public fun create(account: &signer) {
        move_to(account, Counter { value: 0 });
    }

    // borrow_global: 不可变借用某地址下的资源
    public fun get_value(addr: address): u64 acquires Counter {
        let counter = borrow_global<Counter>(addr);
        counter.value
    }

    // borrow_global_mut: 可变借用
    public fun increment(addr: address) acquires Counter {
        let counter = borrow_global_mut<Counter>(addr);
        counter.value = counter.value + 1;
    }

    // move_from: 从账户下移走资源(获取所有权)
    public fun destroy(account: &signer): u64 acquires Counter {
        let Counter { value } = move_from<Counter>(signer::address_of(account));
        value
    }

    // exists: 检查资源是否存在
    public fun has_counter(addr: address): bool {
        exists<Counter>(addr)
    }
}

acquires 关键字:当函数访问全局存储中的资源时,必须声明 acquires。这是 Aptos Move 特有的。

6. Sui Move 的对象模型(与 Aptos 的核心差异)

Sui Move 不使用全局存储操作 (move_to, borrow_global 等),而是使用对象模型:

Aptos Move                    Sui Move
─────────                     ────────
move_to(signer, resource)  →  transfer::transfer(object, recipient)
borrow_global<T>(addr)     →  函数参数直接接收 &T 或 &mut T
borrow_global_mut<T>(addr) →  函数参数直接接收 &mut T
move_from<T>(addr)         →  函数参数接收 T(按值传递=获取所有权)
exists<T>(addr)            →  由 Sui runtime 在交易执行前验证

在 Sui 中,对象通过 UID 唯一标识,runtime 负责对象的存取:

// Sui Move: 对象作为函数参数传入
public fun increment(counter: &mut Counter) {
    counter.value = counter.value + 1;
}

// 调用时,Sui runtime 根据 object ID 自动加载对象
// 开发者不需要手动从存储中取出

代码实战

Sui Move Counter 模块完整实现

/// 一个简单的计数器模块,展示 Sui Move 基础概念
module counter::counter {
    // ===== 导入 =====
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use sui::event;

    // ===== 错误码 =====
    const E_NOT_ZERO: u64 = 0;
    const E_OVERFLOW: u64 = 1;

    // ===== 事件 =====
    struct CounterCreated has copy, drop {
        id: address,
        creator: address,
    }

    struct CounterIncremented has copy, drop {
        id: address,
        new_value: u64,
    }

    // ===== 对象定义 =====
    /// Counter 对象 - 一个简单的计数器
    /// has key: 可以作为 Sui 对象存在
    /// has store: 可以被嵌套在其他对象中,也可以被转移
    struct Counter has key, store {
        id: UID,
        value: u64,
        owner: address, // 记录创建者
    }

    // ===== 公共函数 =====

    /// 创建一个新的 Counter 对象并转移给调用者
    public entry fun create(ctx: &mut TxContext) {
        let sender = tx_context::sender(ctx);
        let id = object::new(ctx);
        let counter_addr = object::uid_to_address(&id);

        let counter = Counter {
            id,
            value: 0,
            owner: sender,
        };

        // 发送创建事件
        event::emit(CounterCreated {
            id: counter_addr,
            creator: sender,
        });

        // 将对象转移给创建者(成为 owned object)
        transfer::transfer(counter, sender);
    }

    /// 创建一个共享的 Counter(任何人都可以操作)
    public entry fun create_shared(ctx: &mut TxContext) {
        let sender = tx_context::sender(ctx);
        let counter = Counter {
            id: object::new(ctx),
            value: 0,
            owner: sender,
        };

        // 共享对象 - 任何人都可以访问和修改
        transfer::share_object(counter);
    }

    /// 递增计数器
    /// 参数类型 &mut Counter 表示需要可变引用(写权限)
    public entry fun increment(counter: &mut Counter) {
        assert!(counter.value < 18446744073709551615, E_OVERFLOW);
        counter.value = counter.value + 1;

        event::emit(CounterIncremented {
            id: object::uid_to_address(&counter.id),
            new_value: counter.value,
        });
    }

    /// 按指定值递增
    public entry fun increment_by(counter: &mut Counter, amount: u64) {
        assert!(counter.value + amount >= counter.value, E_OVERFLOW); // 溢出检查
        counter.value = counter.value + amount;
    }

    /// 重置计数器
    public entry fun reset(counter: &mut Counter) {
        counter.value = 0;
    }

    /// 获取当前值(只读)
    /// 参数类型 &Counter 表示不可变引用(只读)
    public fun value(counter: &Counter): u64 {
        counter.value
    }

    /// 获取 owner
    public fun owner(counter: &Counter): address {
        counter.owner
    }

    /// 销毁计数器(只有值为0时才允许)
    public entry fun destroy_zero(counter: Counter) {
        assert!(counter.value == 0, E_NOT_ZERO);
        let Counter { id, value: _, owner: _ } = counter;
        object::delete(id);
    }

    /// 转移 Counter 给另一个地址
    public entry fun transfer_counter(counter: Counter, recipient: address) {
        transfer::transfer(counter, recipient);
    }

    // ===== 测试 =====
    #[test_only]
    use sui::test_scenario;

    #[test]
    fun test_create_and_increment() {
        let owner = @0xA;

        // 第一个交易:创建 counter
        let mut scenario = test_scenario::begin(owner);
        {
            create(test_scenario::ctx(&mut scenario));
        };

        // 第二个交易:递增 counter
        test_scenario::next_tx(&mut scenario, owner);
        {
            let mut counter = test_scenario::take_from_sender<Counter>(&scenario);
            assert!(value(&counter) == 0, 0);

            increment(&mut counter);
            assert!(value(&counter) == 1, 1);

            increment(&mut counter);
            assert!(value(&counter) == 2, 2);

            test_scenario::return_to_sender(&scenario, counter);
        };

        // 第三个交易:验证状态持久化
        test_scenario::next_tx(&mut scenario, owner);
        {
            let counter = test_scenario::take_from_sender<Counter>(&scenario);
            assert!(value(&counter) == 2, 3);
            test_scenario::return_to_sender(&scenario, counter);
        };

        test_scenario::end(scenario);
    }

    #[test]
    fun test_destroy_zero() {
        let owner = @0xA;
        let mut scenario = test_scenario::begin(owner);
        {
            create(test_scenario::ctx(&mut scenario));
        };

        test_scenario::next_tx(&mut scenario, owner);
        {
            let counter = test_scenario::take_from_sender<Counter>(&scenario);
            assert!(value(&counter) == 0, 0);
            destroy_zero(counter); // 值为0,允许销毁
        };

        test_scenario::end(scenario);
    }

    #[test]
    #[expected_failure(abort_code = E_NOT_ZERO)]
    fun test_destroy_non_zero_fails() {
        let owner = @0xA;
        let mut scenario = test_scenario::begin(owner);
        {
            create(test_scenario::ctx(&mut scenario));
        };

        test_scenario::next_tx(&mut scenario, owner);
        {
            let mut counter = test_scenario::take_from_sender<Counter>(&scenario);
            increment(&mut counter);
            destroy_zero(counter); // 值不为0,应该失败
        };

        test_scenario::end(scenario);
    }
}

与 Solidity 的对比

// Solidity 版本的 Counter
contract Counter {
    uint256 public value;
    address public owner;

    event CounterIncremented(uint256 newValue);

    constructor() {
        owner = msg.sender;
    }

    function increment() external {
        value++;
        emit CounterIncremented(value);
    }

    function reset() external {
        require(msg.sender == owner, "Not owner");
        value = 0;
    }
}

对比表:

特性Solidity CounterSui Move Counter
状态存储合约内全局变量独立对象(可多个实例)
所有权无内建所有权概念对象有明确 owner
创建多实例需要 mapping 或工厂模式每次调用 create 产生新对象
并发访问整个合约串行不同对象可并行处理
销毁需自己实现逻辑解构+delete,编译器保证完整性

关键要点总结

Move 语言核心特性

  1. 资源安全:struct 没有 copy ability 就不能复制,没有 drop 就不能丢弃
  2. 模块封装:struct 的字段只能在定义模块内访问
  3. 显式所有权:Sui 中对象有明确的所有者,runtime 强制执行
  4. 静态类型:所有类型在编译时确定,不存在动态调用

Sui Move vs Aptos Move 核心差异

维度Aptos MoveSui Move
存储模型全局存储(账户→资源)对象模型(对象有独立 ID)
存取方式move_to/borrow_global对象作为函数参数传入
并行性乐观并行(BlockSTM)对象级并行(owned object 无冲突)
acquires需要声明不需要
对象类型owned/shared/immutable

abilities 速查表

key                → Sui 对象(独立存在)
key + store        → 可转移的 Sui 对象(最常见)
copy + drop        → 纯值类型(如坐标点)
copy + drop + store → 可存储的值类型
drop               → 一次性使用(witness pattern)
(无 ability)       → 热土豆模式(必须在同一交易内消费)

常见误区

误区 1:"Move 的 struct 类似 Solidity 的 struct"

错误!Solidity 的 struct 是纯数据结构,可以随意复制。Move 的 struct 通过 abilities 系统控制行为——没有 copy 就不能复制,没有 drop 就不能丢弃。这是安全性的根基。

误区 2:"Sui 的对象就是 Solidity 的合约实例"

不完全对。Solidity 中每个合约部署一个实例,状态共享。Sui 中同一个模块可以创建无数个独立对象,每个有独立 owner 和独立 ID,可以并行操作。

误区 3:"entry 函数 = public 函数"

错误entry 函数可以从客户端直接调用(作为交易入口),但不能被其他 Move 模块调用。public 函数可以被其他模块调用,但不能直接作为交易入口。public entry 二者兼备。

误区 4:"Move 没有合约升级"

Sui Move 支持包升级,但有严格限制——不能删除已有的 public 函数,不能修改已有 struct 的布局。这比 Solidity 的 proxy pattern 更安全但也更受限。


面试关联

Q: Move 的资源安全性如何防止 Token 被复制或凭空产生?

简短回答:Move 的 abilities 系统在编译期保证——没有 copy ability 的 struct 不能被复制,没有 drop ability 的 struct 不能被丢弃。Token 类型通常只有 key + store,任何复制或丢弃的尝试都会导致编译错误。

详细回答

  • 在 Solidity 中,Token 余额只是 mapping(address => uint256) 中的数字,合约 bug 可能导致凭空增减
  • 在 Move 中,Token 是一个 struct 对象,类型系统保证它只能被创建(mint)、转移(transfer)、合并(join)、拆分(split)或销毁(burn)
  • 编译器层面的保证比运行时检查更可靠——根本无法编写出复制 Token 的代码

Q: Sui 的对象模型比 EVM 的存储模型有什么优势?

回答

  1. 并行性:不同 owned object 可以在不同的验证者上并行处理,EVM 中整个合约状态串行访问
  2. 明确所有权:对象有明确 owner,无需在合约中维护 access control
  3. 组合性:对象可以嵌套(wrapping),形成层次结构
  4. 本地交易:只涉及 owned object 的交易不需要共识(Sui 的 fast path)

Q: 解释 Move 的 abilities 系统?

回答:Move 的四种 abilities(copy/drop/store/key)控制值的行为。copy 允许复制,drop 允许丢弃,store 允许存储在其他结构中,key 允许作为顶层存储对象。组合使用可实现不同安全级别——资产类型(key+store)不可复制不可丢弃,值类型(copy+drop+store)行为像普通数据,witness类型(仅drop)用于一次性能力证明。


参考资源