Move 安全 — 溢出/权限/闪电贷在 Move 中的处理 + 审计案例
### 1. Move 安全模型概览
日期: 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 模式——创建一个没有 drop、copy、store、key 的结构体,强制调用者在同一交易中归还:
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 混淆、版本竞争 |
常见误区
- "Move 是完全安全的" — 错误!Move 消除了底层漏洞,但逻辑错误和经济攻击仍然是主要风险
- "Move 不需要审计" — 错误!Capability 遗漏、精度错误、经济攻击都需要专业审计
- "Hot Potato = 无闪电贷风险" — 不完全对。Hot Potato 保证归还,但闪电贷仍可用于操纵价格、治理投票等
- "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 Book | Move 语言官方教程 |
| Sui Move 安全最佳实践 | Sui 官方安全指南 |
| MoveBit 审计报告集 | Move 生态审计公司 |
| OtterSec 审计报告 | 多链审计报告(含 Move) |
| Move Prover | Move 形式化验证工具 |
| Cetus Protocol 审计报告 | Sui DEX 审计案例 |