干货分享:区块链运行时环境与智能合约安全建模

  • CertiK
  • 更新于 2024-10-25 14:51
  • 阅读 627

在Web3.0领域,智能合约的安全性也会被其部署区块链的设计和运行时环境影响。这有很多原因,例如:①开发者必须使用新的特定领域语言;②交易执行可能涉及异步函数最终性;③对于不同的区块链环境,并不总是具有相同的工具。在本文中,我们将探讨基于不同的运行时模型,智能合约安全性是如何变化的。

image.png

在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更新Alice的balance,并从Bob的balance中减掉x个token。

approve

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

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

transferFrom

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

b. 效果:合约用x个token更新Sally的balance,并从Bob的balance中减掉x个token,并从Bob对Alice的授权金额中减掉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的智能合约起作用。

image.png

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

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

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

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

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

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

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

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

image.png

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

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

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

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

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

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

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

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

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

image.png

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费用。

image.png

破产攻击步骤

① 假设流动性质押合约包含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和其他区块链通信系统之间,也因此非常关键,这一主题同样将在未来分享,敬请关注!

  • 原创
  • 学分: 73
  • 分类: 公链
  • 标签:
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
该文章收录于 CertiK 安全知识分享
4 订阅 15 篇文章

0 条评论

请先 登录 后评论
CertiK
CertiK
CertiK总部位于纽约,由耶鲁大学和哥伦比亚大学的两位教授创立。作为头部Web3安全机构,CertiK以守护Web3生态的安全为愿景,依托其核心技术和人才优势,为全球150个国家的4682个项目提供审计、安全评级、合规与反洗钱、投资和安全相关服务,致力于最大化客户利益,并持续为社区创造价值。