返回 SC 笔记
SC Day 71

Move 安全 — 溢出/权限/闪电贷在 Move 中的处理 + 审计案例

### 1. Move 安全模型概览

2026-06-20
第三阶段:安全审计 (71-72)
movesecurityauditoverflowaccesscontrolflashloan

日期: 2026-06-20 方向: Move / 安全审计 阶段: 第三阶段:安全审计 (71-72) 标签: #move #security #audit #overflow #accesscontrol #flashloan


今日目标

类型内容
学习Move 类型系统如何从语言层面防止常见漏洞、Move 剩余攻击面分析
实操对比 Move 安全模式 vs EVM 常见漏洞模式,分析真实 Move/Sui 审计报告
产出Move 安全模式速查表 + 审计报告分析笔记

核心概念

1. Move 安全模型概览

Move 语言从设计之初就以资源安全为核心目标。与 Solidity 对比,Move 在语言层面就消除了大量常见漏洞:

漏洞类型Solidity 状态Move 状态Move 如何防护
重入攻击常见,需 ReentrancyGuard语言层面消除线性类型 + 无动态调用
整数溢出0.8+ 默认检查语言层面消除所有算术运算自带溢出检查
未初始化存储偶发不存在无 storage slot 概念
delegatecall 注入高危不存在无 delegatecall
tx.origin 钓鱼常见不存在无 tx.origin 概念
闪电贷攻击常见部分缓解Hot Potato 模式约束
逻辑错误常见仍然存在语言无法防护业务逻辑
权限控制缺陷常见部分缓解Capability 模式
经济攻击常见仍然存在需要业务层防护

2. Move 线性类型系统与资源安全

Move 的核心安全特性是线性类型系统(Linear Type System),资源(Resource)具有以下不变量:

  • 不可复制(No Copy):资源不能被 copy,只能被 move
  • 不可丢弃(No Drop):资源不能被隐式丢弃,必须显式处理
  • 不可凭空创建:资源只能由定义它的模块创建
module example::token {
    /// 代币资源 - 没有 copy 和 drop ability
    struct Token has store, key {
        value: u64,
    }

    /// 错误:无法复制 Token
    // let token2 = copy token; // 编译错误!

    /// 错误:无法丢弃 Token
    // fun lose_token(token: Token) { } // 编译错误!token 未被使用

    /// 正确:必须显式处理 Token
    public fun destroy_token(token: Token): u64 {
        let Token { value } = token; // 解构消耗资源
        value
    }

    /// 转移 Token - 所有权从调用者转移到接收者
    public fun transfer(token: Token, recipient: address) {
        // token 的所有权被 move 到 recipient 的账户下
        transfer::public_transfer(token, recipient);
    }
}

为什么这能防止重入?

在 EVM 中,重入攻击利用外部调用期间合约状态未更新的窗口:

// Solidity - 经典重入漏洞
contract VulnerableVault {
    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount);
        // 危险:先转账,后更新状态
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
        balances[msg.sender] -= amount; // 攻击者在这行执行前重入
    }
}

在 Move 中,资源的所有权转移是原子性的,不存在"中间状态":

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

    struct Vault has key {
        id: UID,
        balance: Balance<SUI>,
    }

    /// Move 版本 - 天然防重入
    public fun withdraw(
        vault: &mut Vault,
        amount: u64,
        ctx: &mut TxContext
    ): Coin<SUI> {
        // balance::split 是原子操作
        // 资源被 move 出去后,vault.balance 已经减少
        // 即使存在回调(Move 中实际不存在),余额已经更新
        let withdrawn = balance::split(&mut vault.balance, amount);
        coin::from_balance(withdrawn, ctx)
    }
}

3. 整数溢出在 Move 中的处理

Move 在 VM 层面对所有算术运算进行溢出检查,溢出时直接 abort:

module example::math_safety {
    /// Move 中所有运算自动检查溢出
    public fun safe_add(a: u64, b: u64): u64 {
        a + b // 如果溢出,自动 abort,错误码 ARITHMETIC_ERROR
    }

    /// 不需要像 Solidity 0.7 那样使用 SafeMath
    /// Move 没有 unchecked 块,无法绕过检查

    /// 但仍需注意精度损失
    public fun calculate_share(amount: u64, total: u64, supply: u64): u64 {
        // 危险:先除后乘可能丢失精度
        // let share = amount / total * supply; // 精度损失!

        // 正确:先乘后除,使用 u128 防止中间溢出
        let result = (amount as u128) * (supply as u128) / (total as u128);
        (result as u64)
    }

    /// 常见的精度处理模式
    const PRECISION: u64 = 1_000_000_000; // 1e9

    public fun mul_div(a: u64, b: u64, c: u64): u64 {
        assert!(c != 0, 1); // 除零检查仍需手动
        let result = (a as u128) * (b as u128) / (c as u128);
        assert!(result <= (18446744073709551615u128), 2); // u64 max
        (result as u64)
    }
}

EVM 对比 — Solidity 溢出历史

// Solidity 0.7 及以前 - 需要 SafeMath
// uint8 max = 255; max + 1 = 0 (静默溢出)

// Solidity 0.8+ - 默认检查,但可以用 unchecked 绕过
function unsafeAdd(uint256 a, uint256 b) public pure returns (uint256) {
    unchecked {
        return a + b; // 绕过溢出检查,节省 gas
    }
}
// Move 没有 unchecked 等价物,这是一个安全优势

4. 权限控制 — Capability 模式

Move 使用 Capability 模式 替代 Solidity 的 onlyOwner 修饰符:

module example::admin {
    /// AdminCap 是一个权限凭证资源
    /// 只有持有这个对象的地址才能执行管理操作
    struct AdminCap has key, store {
        id: UID,
    }

    /// 协议配置
    struct Config has key {
        id: UID,
        fee_rate: u64,
        paused: bool,
    }

    /// 初始化时创建 AdminCap 并转移给部署者
    fun init(ctx: &mut TxContext) {
        let admin_cap = AdminCap {
            id: object::new(ctx),
        };
        transfer::transfer(admin_cap, tx_context::sender(ctx));

        let config = Config {
            id: object::new(ctx),
            fee_rate: 30, // 0.3%
            paused: false,
        };
        transfer::share_object(config);
    }

    /// 只有持有 AdminCap 的地址才能调用
    /// AdminCap 作为参数传入,编译器保证调用者拥有它
    public fun update_fee(
        _admin: &AdminCap, // 引用即可,不需要消耗
        config: &mut Config,
        new_fee: u64,
    ) {
        assert!(new_fee <= 1000, 0); // 最大 10%
        config.fee_rate = new_fee;
    }

    /// 暂停协议
    public fun pause(
        _admin: &AdminCap,
        config: &mut Config,
    ) {
        config.paused = true;
    }

    /// 权限转移 - 将 AdminCap 转给新管理员
    public fun transfer_admin(
        admin_cap: AdminCap,
        new_admin: address,
    ) {
        transfer::transfer(admin_cap, new_admin);
    }
}

对比 Solidity 的权限控制

// Solidity - 常见权限控制漏洞
contract VulnerableAdmin {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    // 漏洞1:忘记加 onlyOwner
    function setFee(uint256 newFee) external {
        // 任何人都能调用!
        fee = newFee;
    }

    // 漏洞2:权限检查在错误位置
    function withdraw() external {
        uint256 balance = address(this).balance;
        payable(msg.sender).transfer(balance);
        require(msg.sender == owner); // 检查在转账之后!
    }
}

Move 的 Capability 模式优势:

  • 编译时检查:如果函数需要 AdminCap 参数,没有它就无法调用
  • 不可伪造AdminCap 只能由定义模块创建
  • 细粒度权限:可以创建多种 Cap(AdminCap, MinterCap, PauserCap)
  • 可组合:Cap 可以被包装、委托、销毁

5. 闪电贷在 Move 中的处理 — Hot Potato 模式

Move 中的闪电贷使用 Hot Potato 模式——创建一个没有 dropcopystorekey 的结构体,强制调用者在同一交易中归还:

module example::flash_loan {
    use sui::coin::{Self, Coin};
    use sui::balance::{Self, Balance};
    use sui::sui::SUI;

    struct LendingPool has key {
        id: UID,
        balance: Balance<SUI>,
        fee_rate: u64, // basis points
    }

    /// Hot Potato - 没有任何 ability!
    /// 不能 copy、不能 drop、不能 store、不能作为 key
    /// 必须在同一交易中被 repay_flash_loan 消耗
    struct FlashLoanReceipt {
        pool_id: ID,
        amount: u64,
        fee: u64,
    }

    /// 借出闪电贷
    public fun flash_loan(
        pool: &mut LendingPool,
        amount: u64,
        ctx: &mut TxContext,
    ): (Coin<SUI>, FlashLoanReceipt) {
        assert!(balance::value(&pool.balance) >= amount, 0);

        let fee = amount * pool.fee_rate / 10000;
        let loan_coin = coin::from_balance(
            balance::split(&mut pool.balance, amount),
            ctx
        );

        let receipt = FlashLoanReceipt {
            pool_id: object::id(pool),
            amount,
            fee,
        };

        (loan_coin, receipt)
        // receipt 必须在交易结束前被消耗
        // 否则交易会因为 Hot Potato 无法 drop 而失败
    }

    /// 归还闪电贷 - 消耗 receipt
    public fun repay_flash_loan(
        pool: &mut LendingPool,
        payment: Coin<SUI>,
        receipt: FlashLoanReceipt,
    ) {
        let FlashLoanReceipt { pool_id, amount, fee } = receipt; // 解构消耗

        assert!(pool_id == object::id(pool), 1);
        assert!(coin::value(&payment) >= amount + fee, 2);

        balance::join(&mut pool.balance, coin::into_balance(payment));
    }
}

为什么 Hot Potato 比 Solidity 的闪电贷更安全?

// Solidity 闪电贷 - 依赖运行时检查
contract FlashLender {
    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = token.balanceOf(address(this));
        token.transfer(msg.sender, amount);

        // 回调 - 这里可能发生任何事情(重入等)
        IFlashBorrower(msg.sender).onFlashLoan(amount);

        // 运行时检查归还
        require(
            token.balanceOf(address(this)) >= balanceBefore + fee,
            "Not repaid"
        );
    }
}

Move Hot Potato 的安全优势

  • 编译时保证FlashLoanReceipt 没有 drop,编译器强制交易中必须消耗它
  • 无回调风险:Move 没有动态调用/回调机制,不存在重入窗口
  • 不可绕过:无法通过任何方式丢弃 receipt(不像 Solidity 可以通过 selfdestruct 等方式)

6. Move 中仍然存在的攻击面

虽然 Move 消除了很多底层漏洞,但以下攻击面仍然存在:

6.1 逻辑错误

module example::vulnerable_swap {
    /// 漏洞:价格计算逻辑错误
    public fun swap(
        pool: &mut Pool,
        coin_in: Coin<A>,
        min_out: u64,
    ): Coin<B> {
        let amount_in = coin::value(&coin_in);
        let reserve_a = balance::value(&pool.reserve_a);
        let reserve_b = balance::value(&pool.reserve_b);

        // 逻辑错误:忘记扣除手续费
        let amount_out = amount_in * reserve_b / reserve_a;
        // 正确应该是:
        // let amount_in_with_fee = amount_in * 997;
        // let amount_out = amount_in_with_fee * reserve_b / (reserve_a * 1000 + amount_in_with_fee);

        assert!(amount_out >= min_out, 0);
        // ...
    }
}

6.2 经济攻击 / 预言机操纵

module example::vulnerable_oracle {
    /// 漏洞:使用 spot price 作为预言机
    public fun get_price(pool: &Pool): u64 {
        let reserve_a = balance::value(&pool.reserve_a);
        let reserve_b = balance::value(&pool.reserve_b);
        // 危险:spot price 可以被闪电贷操纵!
        reserve_b * PRECISION / reserve_a
    }

    /// 修复:使用 TWAP 或外部预言机(如 Pyth)
    public fun get_price_safe(oracle: &PriceOracle): u64 {
        let price_info = pyth::get_price(oracle);
        let price = pyth::get_price_value(&price_info);
        let age = pyth::get_price_age(&price_info);
        // 检查价格新鲜度
        assert!(age < MAX_PRICE_AGE, E_STALE_PRICE);
        price
    }
}

6.3 访问控制遗漏

module example::vulnerable_access {
    /// 漏洞:public fun 但缺少权限检查
    /// 任何人都可以调用 mint!
    public fun mint(treasury: &mut Treasury, amount: u64, ctx: &mut TxContext): Coin<TOKEN> {
        // 缺少 AdminCap 参数!
        let minted = balance::increase_supply(&mut treasury.supply, amount);
        coin::from_balance(minted, ctx)
    }

    /// 修复:添加 Capability 参数
    public fun mint_safe(
        _admin: &AdminCap,  // 编译时强制权限
        treasury: &mut Treasury,
        amount: u64,
        ctx: &mut TxContext
    ): Coin<TOKEN> {
        let minted = balance::increase_supply(&mut treasury.supply, amount);
        coin::from_balance(minted, ctx)
    }
}

代码实战

Move 审计案例分析:Cetus Protocol (Sui DEX) 审计报告

以下基于公开的 Cetus Protocol 审计报告进行分析:

发现 1:精度损失导致的套利机会

// 审计发现:流动性计算中的精度损失
// 攻击者可以通过反复添加/移除小额流动性获利

// 有问题的实现
public fun calculate_liquidity(
    amount_a: u64,
    amount_b: u64,
    sqrt_price: u128,
): u128 {
    // 中间计算精度不足
    let liquidity_a = (amount_a as u128) * sqrt_price / PRECISION;
    let liquidity_b = (amount_b as u128) * PRECISION / sqrt_price;
    math::min(liquidity_a, liquidity_b)
}

// 修复后的实现
public fun calculate_liquidity_fixed(
    amount_a: u64,
    amount_b: u64,
    sqrt_price: u128,
): u128 {
    // 使用更高精度的中间变量
    let liquidity_a = full_math::mul_div_floor(
        (amount_a as u128),
        sqrt_price,
        PRECISION
    );
    let liquidity_b = full_math::mul_div_floor(
        (amount_b as u128),
        PRECISION,
        sqrt_price
    );
    math::min(liquidity_a, liquidity_b)
}

发现 2:共享对象竞争条件

// Sui 特有问题:共享对象(Shared Object)的并发访问
// 在 Sui 中,shared object 需要通过共识排序
// 但如果两个交易同时修改同一个 shared object,可能产生意外行为

struct GlobalConfig has key {
    id: UID,
    protocol_fee_rate: u64,
    // 审计建议:添加版本号防止竞争
    version: u64,
}

public fun update_config(
    admin: &AdminCap,
    config: &mut GlobalConfig,
    new_fee: u64,
) {
    // 添加版本检查
    assert!(config.version == CURRENT_VERSION, E_VERSION_MISMATCH);
    config.protocol_fee_rate = new_fee;
}

发现 3:对象所有权混淆

// Move/Sui 特有漏洞:对象所有权类型混淆
// Sui 有三种对象所有权:Owned, Shared, Immutable

// 漏洞:将应该是 Shared 的对象设为 Owned
// 导致只有 owner 能与之交互
fun init(ctx: &mut TxContext) {
    let pool = Pool {
        id: object::new(ctx),
        // ...
    };
    // 错误:pool 应该是 shared 的,这里变成了私有的
    transfer::transfer(pool, tx_context::sender(ctx));

    // 正确:
    // transfer::share_object(pool);
}

Move 审计检查清单

## Move 安全审计检查清单

### 资源安全
- [ ] 所有资源是否被正确消耗/转移?
- [ ] 是否存在资源泄漏(创建后未使用)?
- [ ] Hot Potato 模式是否正确实现(无 ability)?

### 权限控制
- [ ] 所有管理函数是否需要 AdminCap?
- [ ] Capability 的创建是否在 init 中受限?
- [ ] 是否存在 public fun 缺少权限参数?
- [ ] 权限是否可以被不当转移?

### 数学安全
- [ ] 除法是否检查除零?
- [ ] 乘法是否可能超过 u128 范围?
- [ ] 精度损失是否在可接受范围内(round down vs round up)?
- [ ] 是否使用了 full_math 库处理高精度计算?

### 对象模型(Sui 特有)
- [ ] Shared Object vs Owned Object 是否正确选择?
- [ ] 是否存在对象所有权混淆?
- [ ] 动态字段的使用是否安全?
- [ ] 对象版本控制是否到位?

### 经济安全
- [ ] 预言机价格是否可被操纵?
- [ ] 是否存在闪电贷攻击向量?
- [ ] 手续费计算是否正确?
- [ ] 滑点保护是否生效?

### 升级安全
- [ ] 模块升级策略是否明确(Sui package upgrade)?
- [ ] 升级后数据迁移是否安全?
- [ ] 是否存在不可升级的关键逻辑?

关键要点总结

要点说明
Move 消除了重入线性类型 + 无动态调用,从语言层面消除
溢出自动检查VM 层面检查,无 unchecked 绕过
Capability > Modifier编译时权限检查,不可伪造
Hot Potato 闪电贷比 Solidity 更安全,编译器强制归还
逻辑错误仍存在精度损失、经济攻击、业务逻辑错误
Sui 对象模型有新攻击面Shared/Owned 混淆、版本竞争

常见误区

  1. "Move 是完全安全的" — 错误!Move 消除了底层漏洞,但逻辑错误和经济攻击仍然是主要风险
  2. "Move 不需要审计" — 错误!Capability 遗漏、精度错误、经济攻击都需要专业审计
  3. "Hot Potato = 无闪电贷风险" — 不完全对。Hot Potato 保证归还,但闪电贷仍可用于操纵价格、治理投票等
  4. "Move 的安全优势意味着 Solidity 不行" — Solidity 生态的安全工具链(Slither/Mythril/Certora)非常成熟,Move 的工具链还在追赶

面试关联

面试题:Move 相比 Solidity 有哪些安全优势?

30 秒回答: Move 通过线性类型系统从语言层面消除了重入攻击、整数溢出等常见漏洞,使用 Capability 模式实现编译时权限检查,使用 Hot Potato 模式保证闪电贷归还。但逻辑错误和经济攻击仍需要审计。

2 分钟回答: Move 的安全优势主要体现在三个层面:一是类型系统——线性类型保证资源不可复制、不可丢弃,从根本上消除了重入和资产复制漏洞;二是权限模型——Capability 模式将权限凭证化,编译器在编译时就能检查权限,不像 Solidity 的 modifier 容易遗漏;三是VM 层面——所有算术运算自带溢出检查,没有 unchecked 逃逸口。但 Move 并非万能,精度计算错误、预言机操纵、经济攻击等仍然存在。此外 Sui 的对象模型引入了新的攻击面,如 Shared/Owned 对象混淆、并发竞争等。

追问准备

  • Q: Move 适合所有 DeFi 场景吗? A: 基本适合,但 Move 生态的预言机、跨链桥等基础设施不如 EVM 成熟,某些复杂的组合性协议可能受限于 Move 不支持动态调用。
  • Q: 如何审计 Move 合约? A: 重点关注业务逻辑正确性、精度计算、Capability 权限完整性、对象模型使用是否得当。工具方面可以用 Move Prover 做形式化验证。

参考资源

资源说明
Move BookMove 语言官方教程
Sui Move 安全最佳实践Sui 官方安全指南
MoveBit 审计报告集Move 生态审计公司
OtterSec 审计报告多链审计报告(含 Move)
Move ProverMove 形式化验证工具
Cetus Protocol 审计报告Sui DEX 审计案例