Move消除了EVM的一些旧痛点,也引入了独有的新安全模型和攻击面。比如,很多团队仍然误解对象所有权、泛型参数,甚至将看似无害的&mut引用传递给不受信任的代码,都可能酿成大错。对审计来说,不能是简单复刻Solidity那一套,必须深入理解其资源、能力和对象拓扑,并提前推导不变量。
Move 消除了 Solidity 一些最糟糕的故障模式,但它引入了一种不同的安全模型,许多 Aptos 团队仍然误读。本指南涵盖 Move 特定的漏洞、攻击路径和实际审计方法。

Move 比 Solidity 更安全,就像一辆刹车更好的赛车比没有刹车的赛车更安全一样。有用。重要。但仍然不等于安全。
这种区别很重要,因为 Move 消除了一些糟糕的 EVM 故障模式,同时引入了一套不同的安全假设。资源、能力、所有权和模块边界为审计师提供了比 Solidity 通常更强的保证。它们并未消除对能力设计、对象所有权、泛型类型绑定、升级控制和恶意组合进行推理的必要性。
本指南是这种现实的实用版本。它涵盖了 Move 的优点、团队仍然受损的地方、值得实际审计时间的漏洞类别,以及严肃的 Aptos 审查应遵循的方法。
本文面向两类人群:
如果你已经熟悉 EVM 审计,请关注 Move 改变安全边界的地方,而不是它仅仅感觉更简洁的地方。
Move 值得尊重的原因很简单:它将更多的安全保证推入语言本身。
其设计的核心是资源、能力、所有权和模块边界。
在 Solidity 中,稀缺性主要通过状态和约定来模拟。在 Move 中,稀缺性是类型系统的一部分。
资源不能被复制,除非它具有 copy 能力。它不能无声地消失,除非它具有 drop 能力。它不能存在于存储数据中,除非它具有 store 能力,并且不能直接用于全局存储操作,除非它具有 key 能力。
Move value model
Resource type
- no copy -> cannot be duplicated
- no drop -> cannot silently vanish
- key -> can participate in global storage ops
Capability / ordinary value
- copy/drop often allowed
- easier to pass around
- should never be confused with an owned asset
这四种能力并非无关紧要。它们是威胁模型的一部分。
| 能力 | 允许的操作 | 审计师关注的原因 |
|---|---|---|
copy |
复制一个值 | 对类似资产的状态很危险 |
drop |
丢弃一个值 | 对义务和收据很危险 |
store |
将一个值放入存储数据中 | 影响持久性和组合性 |
key |
在全局存储操作中使用一个值 | 影响对链上状态的权限 |
Aptos Move 能力文档值得重新审视,因为能力组合是看似微小的设计选择成为真实攻击面的地方。
与 Solidity 相比,Move 在几个重要领域为开发者提供了真正的帮助:
delegatecall 式危险。这就是为什么 Move 代码在审查时通常感觉更简洁。
但简洁不等于正确。错误转移到了能力边界、对象所有权、泛型、可变引用和操作控制中。
最常见的 Move 错误并非来自对 VM 操作码层面的误解。它们来自将语言保证视为比实际更广泛。
&signer 视为完整的授权接受一个签名者与证明该签名者应该被允许执行该操作是不同的。
Aptos Move 安全指南明确指出:对于敏感操作,你仍然需要验证签名者是预期的账户或所操作对象的合法所有者。
将 Object<T> 传递给函数并不能证明调用者拥有该对象所代表的资产或权限。
这种微妙之处产生了一种非常 Move 特有的错误类别:对象所有权检查失败。一个质押对象、订阅对象或抵押对象可以是有效的,但仍然属于其他人。
泛型是安全边界的一部分。
如果收据未与借入 Token 的资产类型进行参数化,则还款路径可能仅证明有某个 Token 被返还,而不是证明返还了正确的 Token。
如果你验证了一个不变量,然后将 &mut T 跨越信任边界传递,你就不再控制该不变量。
被调用者可能无法解包你的私有字段,但它仍然可以替换整个值、通过其他路径修改状态或使你刚刚检查的假设失效。
这是 Move 安全并非仅仅是“接受签名者然后继续”的最清晰例子之一。
entry fun execute_action_with_valid_subscription(
user: &signer,
obj: Object<Subscription>
) acquires Subscription {
let object_address = object::object_address(&obj);
let subscription = borrow_global<Subscription>(object_address);
assert!(subscription.end_subscription >= timestamp::now_seconds(), 1);
// action continues...
}
这检查了订阅是否存在且处于活动状态。它没有检查调用者是否拥有它。
缺失的防护措施在概念上很简单:
assert!(object::owner(&obj) == signer::address_of(user), ENOT_OWNER);
如果你跳过此检查,一个有效的对象可能会成为权限绕过。
一个有漏洞的闪电贷设计可能会返回 (Coin<T>, Receipt),然后接受 repay_flash_loan<T>(receipt: Receipt, coins: Coin<T>)。
问题在于 Receipt 实际上并未绑定到 T,因此协议仅证明存在还款金额,而不是还款资产与借入资产匹配。
struct Receipt<phantom T> has drop {
amount: u64,
}
public fun flash_loan<T>(amount: u64): (Coin<T>, Receipt<T>) { /* ... */ }
public fun repay_flash_loan<T>(receipt: Receipt<T>, coins: Coin<T>) { /* ... */ }
phantom 参数很重要,因为它将业务规则推入类型系统。
将 copy 赋予类似资产的类型,或将 drop 赋予类似义务的类型,是严重的设计错误。
copy 操作可能会导致通货膨胀或双重支付行为。drop 操作可能会让借款人丢弃他们本应履行的证明。这是 Move 安全故事只有在类型设计者做出正确选择时才有效的地方之一。
ConstructorRef 泄露在创建 Aptos 对象时,暴露 ConstructorRef 可能会泄露超出团队预期的更多控制权。根据流程,该引用稍后可能会被转换为更强的修改或转移能力。
对于 NFT、托管和以对象为主的协议,规则很简单:除非你已对每个派生能力的完整生命周期进行建模,否则不要返回或随意持久化 ConstructorRef。
Move 在结构上仍然比 Solidity 更能抵抗经典的重入攻击,但这并不意味着所有状态交接问题都消失了。
审计师仍然需要考虑:
&mut 引用跨越信任边界。正确的结论不是“Move 现在有重入问题了”。而是“Move 有比许多团队预期更多的状态假设失效方式”。
并非所有严重的 Move 错误都与类型有关。一次真正的审查还应包括:
如果升级密钥薄弱,即使是干净的 Move 代码也可能成为管理层“rug pull”的载体。
&mut 进行无价值资产替换此场景基于 Aptos 当前关于传递给不受信任代码的可变引用的指导。
一个协议接受一个 FungibleAsset,检查它是否是预期的资产,然后将 &mut FungibleAsset 传递给一个Hook。Hook返回后,协议假定资产身份未改变。
1. 用户存入合法资产。
2. 协议验证元数据一次。
3. 协议将 &mut 资产传递给攻击者控制的逻辑。
4. 攻击者用无价值资产替换该值。
5. 协议恢复并根据现在无价值的资产铸造信用。
6. 金库收到垃圾资产。攻击者保留真实价值。
&mut 传递给不受信任回调的公共 API。ConstructorRef 成为未来的追回路径这对于 NFT 市场、托管系统和以对象为主的 DeFi 设计很重要。
一个铸币函数为了方便返回一个 ConstructorRef。团队认为这无害,因为对象已经成功创建。
1. 铸币者收到或泄露一个 ConstructorRef。
2. ConstructorRef 被用于派生一个更强大的能力。
3. 对象稍后被出售或作为抵押品发布。
4. 原始行为者仍然持有一个隐藏的控制路径。
5. 资产在转移后被追回、重定向或修改。
ConstructorRef 的函数。如果你正在审计基于 Move 的协议,从第一天起,这个过程就应该与 Solidity 审查有所不同。
在深入阅读业务逻辑之前,请列举:
public entry 函数。public(friend) 函数。key、store、copy 或 drop 的类型。在 Aptos 上,这张地图不是内务管理。它是审计工作的一半。
对于每个协议,定义必须保持真实的“真理”:
如果团队无法清晰地陈述其不变量,审计就已经告诉你一些重要信息。
关注:
核心问题始终是:团队是否让类型系统强制执行业务规则,还是他们只是希望运行时逻辑能保持其完整性?
在 Move 中,信任边界不仅仅是明显的管理功能。它们还包括:
&mut 传递给另一个模块。如果一个有状态的假设跨越了这些边界之一,那么在证明其不会被破坏之前,请假定它可能会被破坏。
Move 通常通过中止而不是静默损坏来失败。这更安全,但如果协议依赖于活性,仍然很危险。
至少审查以下情况:
你仍然需要知道:
如果操作层薄弱,即使仓库中最干净的代码也可能无关紧要。
Move audit checklist
1. 列举每个 public 和 friend 入口点。
2. 列出每个资源及其能力。
3. 验证签名者检查和对象所有权检查。
4. 将所有资产敏感的收据和凭证绑定到正确的类型。
5. 审查每个能力,以评估其影响范围和最小权限。
6. 在任何不受信任的可变交接后重新检查不变量。
7. 审查回调、函数值和延迟执行路径。
8. 对算术、舍入和中止行为进行压力测试。
9. 审查预言机、桥接和升级假设。
10. 证明或测试在经济上真正重要的不变量。
正确的 Move 审计堆栈不是“仔细阅读代码然后祈祷”。
Move 证明器指南很有用,但更重要的问题是你选择证明什么。
将其用于在实际漏洞利用中很重要的属性:
功能正确性是不够的。好的测试套件应模拟:
Move 尚未拥有与 EVM 生态系统相同的成熟模糊测试文化,这使得不变量驱动的模糊测试更有价值。
有用的序列包括:
不要创建一个可以暂停系统、铸造供应、提取金库资金和管理升级的能力。分离权限,这样单个泄露的能力就不会造成灾难性后果。
如果收据属于某种资产类型,请将其编码到类型中。如果凭证代表固定的未来行动,请编码资产和金额,以便运行时逻辑减少猜测。
当设计允许时,直接从 signer::address_of(user) 借用或移动数据通常比接受任意对象并希望调用者提供了正确的对象更安全。
如果一个不变量在调用前很重要,那么在调用后它仍然很重要。每当可变或延迟执行路径可能改变身份、余额、权限和边界时,请重新检查它们。
共享对象账户不仅仅是一种组织技巧。它们影响转移和修改语义,因此它们属于威胁模型。
Move 在很多方面都做得很好。资源、能力、所有权和模块边界消除了多年来困扰 EVM 协议的真实错误类别。
但剩下的错误并非表面问题。它们存在于对象所有权、能力设计、泛型类型绑定、可变信任边界和升级控制中。这就是为什么一次真正的 Move 审计应该与一份回收的 Solidity 清单有所不同。它应该具备资源意识、对象意识、能力意识,并坚定不移地关注不变量。
- 原文链接: smartcontractshacking.co...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!