干货分享:区块链运行时环境与智能合约安全建模
2023-03-11 01:00
CertiK
2023-03-11 01:00
订阅此专栏
收藏此文章

在 Web3.0 领域,智能合约的安全性也会被其部署区块链的设计和运行时环境影响。

这有很多原因,例如:

① 开发者必须使用新的特定领域语言;

② 交易执行可能涉及异步函数最终性;

对于不同的区块链环境,并不总是具有相同的工具。

在本文中,我们将探讨基于不同的运行时模型,智能合约安全性是如何变化的。

本文也将特意比较 EVM 智能合约的运行时环境假设和 NEAR 智能合约的一组类似假设。然后我们将研究这些元素将如何影响一个安全智能合约的设计。

除此之外,本文也将分享我们在审计 NEAR 合约时发现的一个攻击载体,我们将其命名为“insolvency attack”(破产攻击)——这一攻击可以影响那些需要额外支付链上存储费用的区块链。虽然这种攻击已被 NEAR 社区中的部分成员所知,但它仍然不是一种广为人知的攻击方式。


区块链运行时环境

如何影响智能合约安全性?

我们先定义一个设想的流动性质押合约

通过分析该合约,我们将了解区块链运行时环境是如何影响智能合约安全性的。

这一质押合约具备 ERC20 类型合约中的一些常见功能,共同的功能将是追踪给定用户的 token 余额并允许在用户之间转移 token。

改变状态的函数有 3 个:transfer; transferFrom; approve。

本例中,假设有两个用户 Bob 和 Alice,他们两个通常能够按如下方式更改状态:

transfer

a. 调用:Bob使用如下参数值 (amount=x, to=Alice) 调用transfer

b. 效果:合约用x个 token 更新Alicebalance,并从Bobbalance中减掉x个 token。

approve

a. 调用Bob使用如下参数值 (allowance=x, to=Alice) 调用approve,并向 Alice 发送address

b. 效果合约根据Bobbalance更新Alice授权金额x,以允许 Alice 调用 transferfrom转移 Bob 不大于 x 个 token。

transferFrom

a. 调用Alice 使用如下参数值 (amount=x, from=Bob, to=Sallys) 调用 transferfrom

b. 效果合约用x个 token 更新Sallybalance,并从Bobbalance中减掉x个 token,并从BobAlice的授权金额中减掉 x。

流动性质押合约的其余函数如下。

depositAndStake

a. 调用Alice使用如下参数值 (amount=x) 调用depositAndStake。

b. 效果合约检查当参数值 amount=x时,是否有足够的附加 GAS。如果检查通过,合约将为 Alice 铸造stGAS token 并将 GAS 存入节点。然后调用depositandStakeCallback

depositandStakeCallback

a. 调用此函数是一个无参数的合约内部函数,且仅能被合约本身调用。

b. 效果检查 Gas 存款是否成功。如果成功,它将返回 true,否则会回滚已更新的合约状态并返还用户的 GAS token。

withdraw

a. 调用:Alice使用如下参数值 (amount=x) 调用 withdraw。

b. 效果合约从节点上取消Alice抵押的 GAS token,并在Alice拥有的等量 stGAS token 上调用transferFrom。如果Alice没有持有足够数量的 stGAS,那么交易将被回滚。


EVM 运行时模型和异步函数的调用

EVM 的Runtime Model(运行时模型)广为人知,Solidity 开发人员假定的一组通用公理如下所示:

① 如果有足够数量的 gas,那么交易可能包含大量复杂的函数调用;

② 如果执行交易,则函数调用是同步的;

③ 如果函数调用增加了现有合约的存储使用量,则交易的 gas 成本没有差异。

这个模型导致了一个有趣的攻击类型「家族」——重入攻击。虽然大家对重入攻击的解决方案已经不陌生,但仍有许多不同的重入仍然并且更难以检测,例如多功能重入和只读重入。

通过我们示例的流动性质押合约,让我们回顾一下简单的重入攻击如何使用下面的伪代码对 EVM-style 的智能合约起作用。

同步函数调用的重入攻击流程:

假设地址 Bob 对应一个智能合约,我们的流动性质押合约控制着 100 个 GAS token。

② 如果Bob持有至少 1个 stGAS token,那么 Bob 调用 withdraw 函数并通过回调函数重新进入 withdraw 函数。

③ 最后的 withdraw 将完成函数调用。

check-effect-interact模式是这种类型重入的一种解决方案。

在本例中,这是通过将负责传输的代码与更新余额的代码切换来完成的。但是,如果函数调用不再同步,这个解决方案是否仍然安全?

不,Near 智能合约中就可以找到这样的一个例子。

让我们看看下图中用于流动性质押合约的伪代码。我们假设所有外部函数调用都在下一个块中完成(注意,以下内容基于 NEAR 的文档,仅用于模拟重入攻击)。

在上述合约中,withdraw函数遵循check-effect-interact模式,但是它仍然容易受到重入的影响:

○ 假设地址 Bob 对应的是 Near 上的智能合约,并假设我们的流动性质押合约控制 100 个 Near token。

在第 0 个区块中,Bob调用depositAndStake函数并附加 49 个 Near。

Bob 在区块 0 调用 withdraw 函数,然后他将收到49 个 Near

但是,回调函数depositAndStakeCallback在第一个区块执行,Bob将收到另一份49 个 Near

请注意,Bob可以在同一笔交易中调用depositAndStakewithdraw。这是因为 NEAR 允许批量函数的调用。这里的关键是withdraw函数是在depositAndStake完成外部函数调用之前完成的。

当函数调用是异步的,重入攻击依然存在,然而攻击形式略有不同。

一种防止此类攻击的解决方案是在某些情况下结合使用Check-Interact-Effect 和本地互斥锁。

请参阅下文了解 Check-Interact-Effect 如何在此处工作。要进一步查看完整示例,请查看 NEAR 文档中的详细解释。


NEAR 运行时模型和 Storage Staking

在本文的上一部分中,我们了解到如果函数调用不是同步执行,会导致我们的智能合约安全模型发生怎样的变化。

而在本章节中,我们可以看看如果随着智能合约状态的增加而增加 gas,安全性将如何变化。

Near 智能合约对应的账户需要持有足够的 NEAR token 用于支付 NEAR 链上数据的存储。这种机制称为Storage Staking

所有数据都需要存储,包括:账户元数据、智能合约字节码、智能合约上的函数调用生成的数据。Storage Staking 所需的 Near 数量被定义为存储的数据长度乘以字节成本。

因此,我们可以更新Near 开发者的通用公理,如下所示:

① 如果有足够数量的 gas,那么交易可能包含大量复杂的函数调用;

② 如果执行交易,则外部函数调用是异步的;

③ 交易的 gas 成本被定义为函数调用的 gas 成本与Storage Staking费用的总和。如果没有足够的Storage Staking 费用,那么所有函数调用都将还原,并且合约中存入的所有其他资金都将被 Near 链冻结。

同时,我们将介绍一个在编写 Near 智能合约时没有发现的新问题

如果普通用户可以调用一个在链上存储新数据的函数,那么智能合约逻辑必须验证是否有足够的资金用于Storage Staking。否则,普通用户就可能会对智能合约发起拒绝服务攻击。这种攻击也被称为Million Small Deposits attack

来自 Near Social 团队的 Evgeny Kuzyakov 为 NEP-145 中的 Storage Staking 提供了解决方案。然而,即使使用这种解决方案,如果未正确计算合约之间的 Storage Staking,这也会导致一个新的攻击向量,我们将其称为 insolvency attack(破产攻击)。

关于 NEP-145 的详情,请参见此链接:https://github.com/near/NEPs/discussions/145


针对流动性质押合约的破产攻击

为了尽量减少复杂程度,假设我们的流动性质押合约不在函数depositAndStake中强制执行Storage Staking费用,withdraw函数会退还Storage Staking费用。

破产攻击步骤

① 假设流动性质押合约包含 100 个 Near,那么 50 个 Near 是 200 个账户的必要存储费。剩下的 50 则是通过质押获得的奖励。

② 进一步假设,一个新账户需要 0.5 个 Near 用于Storage Staking调用 depositAndStake

③ Bob 输入金额 0.01,调用depositAndStake

④ 然后 Bob 批量调用withdraw函数,输入金额 0.0001。

⑤ Bob能够从流动性质押中抽取 50 个 NEAR,在这之后,任何在链上存储新数据的函数会被回滚。

尽管以上示例攻击看起来只是微末功夫,但在实践中,检测大型智能合约系统却是个非常困难的事情。

而且这种情况看起来完全不合逻辑。如果balance[user]甚至不为 0,那为什么在调用withdraw时合约会退还 NEAR?

这种情况曾经在审计 NEAR 智能合约时出现过

例如,跨链桥项目可以在其两侧设置托管合约,以便跨链桥的一侧收取存储费用,而另一侧则释放存储费用。

但是如果跨链桥两侧没有正确的存储记账,用户可以滥用此漏洞获取合约中的存储费用。这是 Calimero 桥的一个风险问题,我们将会在 CertiK 官方公众号接下来发布的内容中详细探讨,如果有现在想要了解的小伙伴,可以访问 certik.com 搜索该项目并点击查看审计报告。


写在最后

智能合约的安全性会受到底层区块链运行时环境,以及定义编程语言的虚拟机的影响。因此不同的区块链需要不同的安全模型

虽然已经有了大量针对不同区块链的著名安全模型,但随着更多区块链的出现,安全模型的数量也会随之增加。

我们已经看到智能合约安全性在不同模型之间无法组合。在本文中,我们也提供了一些示例来说明 NEAR 和 EVM 智能合约的不同之处。

限于篇幅原因,本文仍未能探讨一些相对重要的方面,例如不同的智能合约安全模型是如何相互交互的。这些内容存在于 bridge 和其他区块链通信系统之间,也因此非常关键,这一主题同样将在未来分享,敬请关注!

相关Wiki

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

在 App 打开
空投
rwa
稳定币
wct
hyperliquid
uniswap
initia
fo
以太坊
om
crv
香港