返回 SC 笔记
SC Day 50

Move语言概述 — 资源模型与能力系统

### 1. Move 语言的诞生与定位

2026-05-30
第三阶段:安全审计
movesuiresourceabilities

日期: 2026-05-30 方向: Move 阶段: 第三阶段:安全审计 标签: #move #sui #resource #abilities


今日目标

类型内容
学习理解 Move 语言的核心设计哲学:资源导向编程、四种 Ability、模块系统、无动态分派
实操安装 Sui CLI,连接 devnet,编写并部署一个简单的 Move 模块(资源创建与能力演示)
产出Move vs Solidity 安全对比、能力系统详解、第一个 Move 模块代码

核心概念

1. Move 语言的诞生与定位

Move 最初由 Facebook(现 Meta)为 Diem(原 Libra)区块链设计,目标是创建一种安全第一的智能合约语言。当 Diem 项目终止后,Move 的核心团队分别创立了 SuiAptos 两条公链,都采用 Move 作为智能合约语言。

Move 的设计目标

目标解决的问题
资源安全代币/NFT 不能被凭空创建、意外复制或丢失
类型安全编译期捕获尽可能多的错误
形式化验证内置 Move Prover,可数学证明合约正确性
可预测的 Gas没有动态分派,执行路径在编译期可分析

2. 资源导向编程(Resource-Oriented Programming)

这是 Move 最核心的创新。在 Solidity 中,代币余额是一个 mapping 中的数字:

// Solidity: 代币只是一个数字
mapping(address => uint256) balances;

// 转账 = 修改两个数字
balances[from] -= amount;
balances[to] += amount;

// 问题:
// 1. 如果只执行第一行不执行第二行?代币凭空消失
// 2. 如果只执行第二行不执行第一行?代币凭空创建
// 3. 编译器无法检查这类逻辑错误

在 Move 中,代币是一个资源对象(Resource):

// Move: 代币是一个不可复制、不可丢弃的资源
module example::token {
    /// Coin 没有 copy 和 drop 能力
    /// 这意味着它不能被复制(防止双花)
    /// 也不能被丢弃(防止资产丢失)
    struct Coin has store, key {
        value: u64
    }

    /// 转账 = 移动资源的所有权
    /// 编译器保证:Coin 从 from 移走后,from 不再拥有它
    public fun transfer(coin: Coin, to: address) {
        // coin 被移动到 to 的账户
        // 原持有者自动失去访问权
        transfer::public_transfer(coin, to);
    }

    /// 拆分一个 Coin
    public fun split(coin: &mut Coin, amount: u64): Coin {
        assert!(coin.value >= amount, 0);
        coin.value = coin.value - amount;
        Coin { value: amount }
    }

    /// 合并两个 Coin
    public fun merge(coin1: &mut Coin, coin2: Coin) {
        let Coin { value } = coin2; // 解构 coin2(消耗它)
        coin1.value = coin1.value + value;
    }
}

资源模型的核心保证

┌─────────────────────────────────────────────────────┐
│               Move 资源的三大不变量                    │
│                                                      │
│  1. 不可凭空创建                                      │
│     → 只有定义该类型的模块才能创建新实例               │
│                                                      │
│  2. 不可意外复制(除非显式授予 copy 能力)              │
│     → 防止双花攻击                                    │
│                                                      │
│  3. 不可意外丢弃(除非显式授予 drop 能力)              │
│     → 防止资产丢失                                    │
│                                                      │
│  编译器在编译期强制执行这些规则!                       │
└─────────────────────────────────────────────────────┘

3. 能力系统(Abilities)— Move 的类型安全核心

Move 的每个 struct 可以拥有四种能力(Abilities),它们决定了该类型的值可以做什么:

能力含义没有该能力的限制
copy值可以被复制赋值和传参时必须移动(move),不能复制
drop值可以被丢弃/忽略必须被显式消耗(解构/转移/存储),否则编译错误
store值可以存储在全局存储中不能作为其他 struct 的字段(在 Sui 的对象模型中)
key值可以作为全局存储的顶层对象不能直接存储为链上对象

能力组合的含义

// 1. 没有任何能力 — "热土豆"(Hot Potato)模式
struct FlashLoanReceipt {
    amount: u64,
    borrower: address,
}
// 不能 copy(防止重复使用)
// 不能 drop(必须归还/消耗)
// 不能存储(必须在同一交易内处理)
// 用途:强制用户在同一交易中归还闪电贷

// 2. 只有 drop — 临时计算结果
struct TempResult has drop {
    value: u64,
}
// 可以丢弃(用完就扔)
// 不能复制/存储

// 3. copy + drop — 普通值类型
struct Point has copy, drop {
    x: u64,
    y: u64,
}
// 行为类似整数/布尔值

// 4. key + store — 链上可存储的对象/资源
struct NFT has key, store {
    id: UID,       // Sui 要求 key 类型必须有 id: UID 字段
    name: String,
    url: String,
}
// 可以作为链上对象存储
// 不能复制(每个 NFT 独一无二)
// 不能丢弃(不会意外消失)

// 5. 所有能力 — 完全自由(慎用)
struct Wrapper has copy, drop, store, key {
    id: UID,
    value: u64,
}
// 很少使用,因为失去了资源安全保证

"Hot Potato" 模式深度解析

这是 Move 中最巧妙的设计模式之一。一个没有任何能力的 struct 就像一个"烫手的山芋" —— 你必须在同一个交易中处理它,不能存起来也不能丢掉。

module example::flash_loan {
    struct FlashLoanReceipt {
        // 无 copy, drop, store, key
        pool_id: ID,
        amount: u64,
    }

    /// 借出资产,同时返回一个 Receipt(烫手山芋)
    public fun borrow(pool: &mut Pool, amount: u64): (Coin<SUI>, FlashLoanReceipt) {
        let coin = coin::split(&mut pool.balance, amount);
        let receipt = FlashLoanReceipt {
            pool_id: object::id(pool),
            amount,
        };
        (coin, receipt)
    }

    /// 归还资产,消耗 Receipt
    public fun repay(pool: &mut Pool, payment: Coin<SUI>, receipt: FlashLoanReceipt) {
        let FlashLoanReceipt { pool_id, amount } = receipt; // 解构消耗
        assert!(object::id(pool) == pool_id, E_WRONG_POOL);
        assert!(coin::value(&payment) >= amount, E_INSUFFICIENT_REPAYMENT);
        coin::put(&mut pool.balance, payment);
    }

    // 如果用户借了钱但不调用 repay,
    // FlashLoanReceipt 无法被丢弃 → 编译错误!
    // Move 编译器从根本上防止了闪电贷不还款的可能
}

在 Solidity 中,闪电贷的还款检查是运行时的 require

// Solidity: 运行时检查(可能被遗忘)
function flashLoan(uint256 amount) external {
    uint256 balanceBefore = balance;
    // 转出
    token.transfer(msg.sender, amount);
    // 回调
    IFlashLoanReceiver(msg.sender).executeOperation(amount);
    // 运行时检查 — 如果开发者忘记写这行就完了
    require(balance >= balanceBefore, "Not repaid");
}

Move 的编译期保证 vs Solidity 的运行时检查,这是安全性的本质差异。

4. 模块系统与封装

Move 的模块(Module)类似于面向对象语言中的类,但有更强的封装性。

module example::treasury {
    use sui::coin::{Self, Coin};
    use sui::sui::SUI;

    /// Treasury 结构体 — 字段默认私有
    struct Treasury has key {
        id: UID,
        balance: Balance<SUI>,
        admin: address,
    }

    /// 只有本模块可以创建 Treasury
    fun init(ctx: &mut TxContext) {
        let treasury = Treasury {
            id: object::new(ctx),
            balance: balance::zero(),
            admin: tx_context::sender(ctx),
        };
        transfer::share_object(treasury);
    }

    /// 公开函数:任何人可以存款
    public fun deposit(treasury: &mut Treasury, coin: Coin<SUI>) {
        let balance = coin::into_balance(coin);
        balance::join(&mut treasury.balance, balance);
    }

    /// 公开函数:只有 admin 可以取款
    public fun withdraw(
        treasury: &mut Treasury,
        amount: u64,
        ctx: &mut TxContext
    ): Coin<SUI> {
        assert!(tx_context::sender(ctx) == treasury.admin, E_NOT_ADMIN);
        let balance = balance::split(&mut treasury.balance, amount);
        coin::from_balance(balance, ctx)
    }
}

关键规则

  1. struct 的字段只有定义它的模块可以直接访问 — 外部模块必须通过公开函数
  2. struct 只有定义它的模块可以创建和解构 — 防止外部模块伪造资源
  3. 没有继承 — Move 使用组合(composition)和泛型代替继承
  4. 没有动态分派 — 所有函数调用在编译期确定,消除了许多攻击面

5. Move vs Solidity 安全对比

安全维度SolidityMove
重入攻击常见且致命(The DAO)不可能:没有动态分派,调用在编译期确定
整数溢出0.8+ 有检查,之前没有运行时自动检查,无需额外处理
未初始化变量可能导致零值 bug编译器要求所有变量初始化
双花攻击需要正确实现逻辑编译期防止:无 copy 能力的资源不可复制
资产丢失可以发送到错误地址编译期防止:无 drop 能力的资源必须被处理
存储冲突代理模式中常见不存在:没有代理模式,升级通过不同机制实现
权限控制需要手动实现 (Ownable)模块级封装 + 对象所有权模型
闪电贷安全运行时 require 检查编译期保证:Hot Potato 模式
形式化验证第三方工具(复杂)内置 Move Prover

Move 不能防止的问题

  • 业务逻辑错误(如价格计算错误)
  • Oracle 操纵
  • 经济模型设计缺陷
  • 权限管理逻辑错误(虽然工具更好)
  • 前端/后端漏洞

6. Sui 的对象模型

Sui 对 Move 做了重大改造,引入了对象导向的存储模型(不同于 Aptos 的账户导向模型)。

Sui 对象类型:
┌─────────────────┐
│  Owned Objects   │ ← 属于特定地址,只有所有者可以使用
│  (独占对象)       │   类似:你钱包里的 NFT
├─────────────────┤
│  Shared Objects  │ ← 所有人可以访问(需要共识排序)
│  (共享对象)       │   类似:DEX 的流动性池
├─────────────────┤
│  Immutable       │ ← 创建后不可修改
│  Objects         │   类似:已发布的 Package
│  (不可变对象)     │
└─────────────────┘

Sui 中的所有权与 Move Ability 的关系

// key 能力 → 可以成为 Sui 对象(有独立 ID)
struct MyObject has key {
    id: UID,  // Sui 要求 key 类型的第一个字段必须是 UID
    data: u64,
}

// key + store → 可以被转移、可以作为其他对象的字段
struct TransferableNFT has key, store {
    id: UID,
    name: String,
}

// 只有 store(没有 key)→ 可以嵌套在其他对象中,但不能独立存在
struct Metadata has store {
    description: String,
    created_at: u64,
}

代码实战

环境搭建:安装 Sui CLI

# 安装 Sui CLI(macOS/Linux)
# 方式1: Homebrew
brew install sui

# 方式2: Cargo(需要 Rust)
cargo install --locked --git https://github.com/MystenLabs/sui.git --branch devnet sui

# 方式3: 预编译二进制
# https://docs.sui.io/guides/developer/getting-started/sui-install

# 验证安装
sui --version

# 配置 devnet
sui client new-env --alias devnet --rpc https://fullnode.devnet.sui.io:443
sui client switch --env devnet

# 创建地址
sui client new-address ed25519

# 获取 devnet SUI
sui client faucet

# 查看余额
sui client gas

第一个 Move 模块:资源与能力演示

# 创建项目
sui move new hello_move
cd hello_move

sources/hello_move.move

/// 演示 Move 的资源模型和能力系统
module hello_move::greeting {
    use std::string::{Self, String};
    use sui::event;

    // ============ 类型定义(展示不同能力组合)============

    /// 问候卡片 — 具有 key + store,可以作为对象存在并转移
    struct GreetingCard has key, store {
        id: UID,
        sender_name: String,
        message: String,
        created_at: u64,
    }

    /// VIP 徽章 — 只有 key,没有 store
    /// 不能被转移给他人(soul-bound / 灵魂绑定)
    struct VIPBadge has key {
        id: UID,
        owner_name: String,
        level: u8,
    }

    /// 统计数据 — copy + drop,像普通值一样使用
    struct Stats has copy, drop {
        total_cards: u64,
        total_vip: u64,
    }

    /// 全局计数器 — 共享对象
    struct Counter has key {
        id: UID,
        cards_created: u64,
        vip_issued: u64,
    }

    // ============ 事件 ============

    struct CardCreated has copy, drop {
        card_id: ID,
        sender: address,
        recipient: address,
    }

    // ============ 错误码 ============

    const E_EMPTY_NAME: u64 = 0;
    const E_EMPTY_MESSAGE: u64 = 1;
    const E_INVALID_LEVEL: u64 = 2;

    // ============ 初始化(部署时执行一次)============

    fun init(ctx: &mut TxContext) {
        let counter = Counter {
            id: object::new(ctx),
            cards_created: 0,
            vip_issued: 0,
        };
        // 共享对象:所有人可以访问
        transfer::share_object(counter);
    }

    // ============ 公开函数 ============

    /// 创建并发送一张问候卡片
    public fun create_card(
        counter: &mut Counter,
        sender_name: vector<u8>,
        message: vector<u8>,
        recipient: address,
        ctx: &mut TxContext,
    ) {
        let name = string::utf8(sender_name);
        let msg = string::utf8(message);

        assert!(string::length(&name) > 0, E_EMPTY_NAME);
        assert!(string::length(&msg) > 0, E_EMPTY_MESSAGE);

        let card = GreetingCard {
            id: object::new(ctx),
            sender_name: name,
            message: msg,
            created_at: tx_context::epoch(ctx),
        };

        let card_id = object::id(&card);

        // 更新计数器
        counter.cards_created = counter.cards_created + 1;

        // 发出事件
        event::emit(CardCreated {
            card_id,
            sender: tx_context::sender(ctx),
            recipient,
        });

        // 转移给接收者
        // GreetingCard 有 store,可以 public_transfer
        transfer::public_transfer(card, recipient);
    }

    /// 发行 VIP 徽章(只有部署者/管理员可以调用)
    public fun issue_vip_badge(
        counter: &mut Counter,
        owner_name: vector<u8>,
        level: u8,
        recipient: address,
        ctx: &mut TxContext,
    ) {
        assert!(level >= 1 && level <= 5, E_INVALID_LEVEL);

        let badge = VIPBadge {
            id: object::new(ctx),
            owner_name: string::utf8(owner_name),
            level,
        };

        counter.vip_issued = counter.vip_issued + 1;

        // VIPBadge 没有 store,只能用 transfer(不是 public_transfer)
        // 这意味着只有本模块可以转移它 → soul-bound!
        transfer::transfer(badge, recipient);
    }

    /// 查看计数器统计(返回可 copy + drop 的 Stats)
    public fun get_stats(counter: &Counter): Stats {
        Stats {
            total_cards: counter.cards_created,
            total_vip: counter.vip_issued,
        }
    }

    /// 销毁问候卡片(回收存储)
    /// GreetingCard 没有 drop,所以需要显式解构
    public fun burn_card(card: GreetingCard) {
        let GreetingCard {
            id,
            sender_name: _,
            message: _,
            created_at: _
        } = card;
        object::delete(id);
    }

    // ============ 展示编译期安全 ============

    /// 这个函数演示了 Move 的安全保证
    /// 如果取消注释下面的代码,会得到编译错误
    public fun safety_demo(card: &GreetingCard): String {
        // 以下代码如果取消注释会编译失败:

        // 错误 1: 尝试复制没有 copy 能力的资源
        // let card_copy = *card; // Error: GreetingCard does not have 'copy'

        // 错误 2: 尝试丢弃没有 drop 能力的资源
        // let temp_card = GreetingCard { ... };
        // // 函数结束时 temp_card 被丢弃 → Error: GreetingCard does not have 'drop'

        card.message
    }
}

编译与部署

# 编译
sui move build

# 运行测试
sui move test

# 部署到 devnet
sui client publish --gas-budget 100000000

# 记录 Package ID,用于后续交互
# 例如:0x1234...abcd

测试文件 tests/greeting_tests.move

#[test_only]
module hello_move::greeting_tests {
    use hello_move::greeting::{Self, Counter, GreetingCard, Stats};
    use sui::test_scenario::{Self as ts, Scenario};
    use sui::test_utils;

    const ADMIN: address = @0xAD;
    const ALICE: address = @0xA;
    const BOB: address = @0xB;

    fun setup(): Scenario {
        let mut scenario = ts::begin(ADMIN);
        {
            // init 会在 publish 时自动调用
            // 在测试中需要手动调用
            greeting::init_for_testing(ts::ctx(&mut scenario));
        };
        scenario
    }

    #[test]
    fun test_create_card() {
        let mut scenario = setup();

        // Alice 创建一张卡片给 Bob
        ts::next_tx(&mut scenario, ALICE);
        {
            let mut counter = ts::take_shared<Counter>(&scenario);
            greeting::create_card(
                &mut counter,
                b"Alice",
                b"Hello Bob!",
                BOB,
                ts::ctx(&mut scenario),
            );
            assert!(greeting::get_stats(&counter).total_cards == 1, 0);
            ts::return_shared(counter);
        };

        // Bob 收到了卡片
        ts::next_tx(&mut scenario, BOB);
        {
            let card = ts::take_from_sender<GreetingCard>(&scenario);
            // 验证卡片存在
            // 销毁卡片
            greeting::burn_card(card);
        };

        ts::end(scenario);
    }

    #[test]
    fun test_stats_is_copyable() {
        let stats = Stats { total_cards: 10, total_vip: 3 };
        // Stats 有 copy 能力,可以复制
        let stats_copy = stats;
        // 两个都可以使用
        assert!(stats.total_cards == stats_copy.total_cards, 0);
        // Stats 有 drop 能力,函数结束时自动丢弃
    }

    #[test]
    #[expected_failure(abort_code = greeting::E_EMPTY_NAME)]
    fun test_empty_name_fails() {
        let mut scenario = setup();
        ts::next_tx(&mut scenario, ALICE);
        {
            let mut counter = ts::take_shared<Counter>(&scenario);
            greeting::create_card(
                &mut counter,
                b"",  // 空名字 → 应该失败
                b"Hello",
                BOB,
                ts::ctx(&mut scenario),
            );
            ts::return_shared(counter);
        };
        ts::end(scenario);
    }
}

关键要点总结

  1. 资源 = 线性类型:Move 的资源(无 copy + 无 drop)是线性类型的实际应用。编译器保证每个资源恰好被使用一次 —— 不多不少
  2. 能力系统是 Move 安全性的核心:四种能力(copy/drop/store/key)的组合决定了类型的行为。缺少某种能力不是缺陷,而是安全限制
  3. Hot Potato 模式利用无能力 struct 实现编译期约束:闪电贷、多步操作等场景可以在编译期保证正确性
  4. 模块封装比 Solidity 更严格:struct 的字段、创建和解构都只在定义模块内可操作
  5. 没有动态分派 = 没有重入:Move 的所有函数调用在编译期确定,不存在 EVM 中 delegatecall/callback 导致的重入问题
  6. Sui 的对象模型扩展了 Move 的能力:Owned/Shared/Immutable 三种对象类型对应不同的并发访问模式
  7. key + store vs 只有 keystore 决定了对象是否可以被外部模块转移,缺少 store 实现了 Soul-bound Token(SBT)

常见误区

误区 1: "Move 比 Solidity 更好"

Move 在类型安全上确实优于 Solidity,但它也有不足:

  • 生态系统远不如 EVM 成熟
  • 开发工具和文档相对匮乏
  • 学习曲线更陡峭
  • 不支持动态分派,某些设计模式无法实现

选择语言应该基于项目需求和生态,而非纯粹的语言特性。

误区 2: "Move 不需要审计"

虽然 Move 从编译期消除了很多漏洞类型,但业务逻辑错误、经济模型缺陷、权限管理漏洞等仍然可能存在。Move 合约同样需要审计,只是审计的重点从低级安全漏洞转向了高级业务逻辑。

误区 3: "没有 copy 能力的类型完全不能复制"

通过 & 引用(borrow)可以读取资源的数据。不能所有权复制,但可以数据引用读取。开发者需要正确区分"移动所有权"和"借用引用"。

误区 4: "Sui Move 和 Aptos Move 是一样的"

虽然都基于 Move,但 Sui 和 Aptos 对 Move 做了不同的扩展:

  • Sui:对象导向模型,UID 系统,transfer 机制
  • Aptos:账户导向模型,更接近原始 Move 规范
  • 两者的标准库、部署方式、交易模型都不同
  • 代码不能直接互相迁移

面试关联

Q1: "Move 和 Solidity 最大的安全区别是什么?"

核心答案:Move 通过线性类型系统编译期防止资产的意外复制和丢失,而 Solidity 依赖运行时检查

具体来说:

  • Move 的资源(无 copy/drop)在编译期保证不会双花或丢失
  • Move 没有动态分派,从根本上消除了重入攻击
  • Move 的 Hot Potato 模式在编译期保证操作完整性
  • Solidity 的所有安全检查都在运行时(require/assert),开发者可能遗忘

Q2: "为什么 Sui/Aptos 选择 Move 而不是 Solidity?"

  1. 安全优先:金融应用需要最高级别的安全保证,Move 的类型系统提供编译期安全
  2. 并行执行:Move 的对象模型(尤其是 Sui)允许独占对象的交易并行处理,提升吞吐量
  3. 形式化验证:Move Prover 可以数学证明合约正确性,这在 Solidity 生态中很难做到
  4. 资源语义:资产作为一等公民,而不是 mapping 中的数字

Q3: "如果你设计一个新的 DeFi 协议,会选择 EVM/Solidity 还是 Sui/Move?"

产品经理视角

选 EVM/Solidity 如果:

  • 需要最大的用户基数和流动性
  • 需要与现有 DeFi 协议组合
  • 团队有丰富的 Solidity 经验
  • 市场验证优先

选 Sui/Move 如果:

  • 安全性是首要考量(管理大量资产)
  • 需要高吞吐量/低延迟
  • 项目有独特的资源管理需求
  • 可以接受较小的初始生态

参考资源