在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的智能合约起作用。
同步函数调用的重入攻击流程:
① 假设地址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可以在同一笔交易中调用depositAndStake和withdraw。这是因为 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和其他区块链通信系统之间,也因此非常关键,这一主题同样将在未来分享,敬请关注!