Move 语言的设计,更难以写出 bug,并且 Move 确实相当程度上做到了,有一些类型 bug 是不会发生的。
但 Bug 是无法根除的,正如所有事情的情况一样 – 而且智能合约的 bug 几乎总是有可能造成负面的财务影响。
在经过许多 Move 审计之后,我们观察到一些 bug 的出现模式。因此,这篇文章的目的是记录我们发现并报告给客户的最常见的 bug 类型。
💡 我们创建了一个虚构的、易受攻击的自动做市商(AMM)协议,名为 “DonkeySwap”,以展示每个 bug 类型,并描述我们将如何在这篇文章中修复每个问题。
DonkeySwap 源代码在这里。
请注意,大多数 bug 的影响通常取决于上下文。
对于 Move 中的公共函数而言,泛型类型是另一种形式的用户输入,必须进行有效性检查。我们经常发现一些函数在接受一个泛型类型时没有
例如,DonkeySwap(我们举例的易受攻击的协议)在以下方面易受攻击:
cancel_order 函数没有断言输入的 BaseCoinType 泛型与存储在 Order 资源中的 base_type 类型信息相匹配。
这个函数为给定的 BaseCoinType 解锁流动资金,并将存储的 Coin 数量返回给用户:
public fun cancel_order(
user: &signer,
order_id: u64
) acquires OrderStore, CoinStore {
// [...]
deposit_funds(order_store, address_of(user), order.base);
// [...]
}
攻击者有可能通过限价兑换订单和取消订单 – 传递不正确的 Coin 类型,从 AMM 中抽干流动性。
下面的概念证明证明了攻击者有能力窃取其他用户的锁定流动性:
##[test(admin=@donkeyswap, user=@0x2222)]
fun WHEN_exploit_lack_of_type_checking(admin: &signer, user: &signer) acquires CoinCapability {
let (my_usdc, order_id) = setup_with_limit_swap(admin, user, 1000000000000000);
// let's say the admin deposits some ZEL
mint(my_usdc, address_of(admin));
let _admin_order_id = market::limit_swap(admin, my_usdc, 1000000000000000);
// now, let's try stealing from the admin
assert!(coin::balance(address_of(user)) == 0, ERR_UNEXPECTED_BALANCE);
assert!(coin::balance(address_of(user)) == 0, ERR_UNEXPECTED_BALANCE);
market::cancel_order(user, order_id); // ZEL is not the right coin type!
assert!(coin::balance(address_of(user)) == 0, ERR_UNEXPECTED_BALANCE);
assert!(coin::balance(address_of(user)) == my_usdc, ERR_UNEXPECTED_BALANCE); // received ZEL?
}
在 cancel_order 函数中添加以下类型检查断言:
assert!(order.base_type == type_info::type_of(), ERR_ORDER_WRONG_COIN_TYPE);
无限制执行(Unbounded execution),也被称为 gas griefing/loop bombing,是一种拒绝服务的攻击,当用户可以无限制的向多个用户共享的循环代码(即可以被许多用户执行)添加迭代时,就会存在这种攻击。
攻击者有可能引起循环迭代足够多的次数,使其耗尽能量而中止。这可能会阻止应用程序的关键功能。
下面的循环遍历每一个开放的订单,有可能因为注册了许多订单而被阻塞:
fun get_order_by_id(
order_store: &OrderStore,
order_id: u64
): (Option) {
let i = 0;
let len = vector::length(&order_store.orders);
while (i < len) {
let order = vector::borrow(&order_store.orders