当重试、重试、再重试导致乱序执行漏洞 - The Trail of Bits Blog
Troy Sargent
2024年3月1日
blockchain, slither
你是否曾想过rollup与其基础链(rollup提交状态检查点的链)是如何通信和交互的?一个仅持有基础链资金的用户如何与rollup上的合约进行交互?
在Arbitrum Nitro中,从基础链调用部署在rollup上合约方法的一种方式是使用可重试交易(又称可重试票据)。虽然这一功能实现了这些交互,但它并非没有陷阱。在我们对Arbitrum及与其集成的合约进行审查时,我们发现了使用可重试票据时存在的一些不为人知的隐患,这些隐患在创建此类交易时应予以考虑。在本文中,我们将分享使用可重试票据如何可能导致意外的竞态条件,并引发乱序执行漏洞。更重要的是,我们为此问题创建了一个新的Slither检测器。现在,你将不仅能够在代码中识别这些隐患,还能对它们进行测试。
可重试票据
在Arbitrum Nitro中,可重试票据促进了以太坊主网(Layer 1,L1)与Arbitrum Nitro rollup(Layer 2,L2)之间的通信。要创建可重试票据,用户可以调用Arbitrum rollup的L1 Inbox合约上的createRetryableTicket,如下面的代码片段所示。当可重试票据被创建并排队时,ArbOS将尝试自动“兑换”它们,即在L2上依次执行。
|
|
createRetryableTicket函数接口
假设发送者覆盖了Gas成本且未发生任何故障,交易将按顺序执行,最终状态是交易B紧接交易A应用后的结果。
图1:理想情况是交易全部按顺序执行。
等等,“可重试”是什么意思?
因为任何交易都可能失败(例如,交易创建后L2 Gas价格大幅上涨,用户没有足够的Gas来覆盖新成本),Arbitrum创建了这些类型的交易,以便用户可以通过提供额外的Gas来“重试”它们。失败的可重试票据将持久保存在内存中,并且可以由任何手动调用ArbRetryableTx预编译合约的redeem方法的用户重新执行,赞助Gas成本。失败的可重试票据与回退的正常交易不同,因为它不需要签署新交易即可再次执行。
此外,内存中的可重试票据可以在创建后最多一周内兑换。通过支付额外存储费用,可重试票据的寿命可以再延长一周;否则,它将在到期后被丢弃。
问题出在哪里
虽然这些类型的交易很有用——它们促进了L2到L1的通信,并允许用户在发生故障时重试交易——但它们也带来了隐患,即用户和开发者可能未意识到的风险。具体来说,可重试票据预计按提交顺序执行,但这并不总是得到保证。
在场景1中,交易A和B都失败并进入内存区域。应用程序的状态保持不变。
图2:在同一交易中创建了两个可重试票据,但两者都失败并进入内存区域。
然而,任何人都可以在交易A之前手动兑换交易B,这意味着交易将意外地乱序执行。
图3:任何人都可以手动乱序兑换内存区域中的交易。
在场景2中,交易A失败并进入内存区域,但交易B成功。再次,交易乱序执行(即交易A根本没有执行),最终状态与预期不符。
图4:只有交易B包含在最终状态中。
在场景3中,交易A成功,但交易B没有。这意味着交易B必须手动重新执行。交易可以创建多次,这意味着在第一个交易B重新执行之前,可以提交第二组交易A和B。如果使用Arbitrum rollup系统的协议开发者没有考虑到协议可能在交易B成功之前收到第二个交易A的情况,协议可能无法正确处理这种情况。
图5:只有交易A包含在最终状态中。
乱序执行漏洞
鉴于这些场景,开发者应考虑交易可能乱序执行。例如,如果队列中的第二个交易依赖于第一个交易的完成,但由于Gas不足失败而在第一个交易执行之前执行,它可能会回退或无法正常工作。重要的是,rollup上的被调用者或消息接收者能够稳健地处理诸如按创建顺序不同的交易接收以及由于故障导致的交易子集较小等情况。如果协议没有预料到可重试票据的重新排序和故障情况,协议可能会崩溃或被黑客攻击。
让我们考虑以下L2合约,用户可以通过调用它来基于某些质押的代币领取奖励。当他们决定取消质押代币时,任何尚未领取的奖励都将丢失:
|
|
用户可以通过L1处理程序中的以下逻辑为此类操作提交可重试票据:
|
|
这里预期claim_rewards会在unstake之前调用。然而,正如我们所看到的,claim_rewards交易并不保证在unstake交易之前执行。如场景1和图3所示,如果两个交易都失败,攻击者可以使unstake在claim_rewards之前执行,导致用户失去奖励。也可能只有第二个交易unstake成功,如场景2所示。
为了减轻此类风险,必须设计协议,使可重试票据具有独立的顺序,其中每个交易的成功不依赖于其他交易的顺序或结果。如何实现独立顺序取决于协议和给定的操作。在此示例中,claim_rewards可以在unstake内部调用。
Slither来救援
作为安全研究人员,我们始终尝试找到方法来自动发现这类问题,并在开发周期早期(例如代码审查期间)标记它们。为此,我们编写了一个Slither检测器,它将标记通过Arbitrum Nitro Inbox合约创建多个可重试票据的函数,以提醒开发者注意此隐患。发布后,你可以通过安装Slither并在Solidity项目的根目录中运行以下命令来使用此检测器:python3 -m pip install slither-analzyer==0.10.1 && slither . –detect out-of-order-retryable。在我们的示例合约上,Slither提供以下诊断:
|
|
结论
如果你正在开发使用可重试票据的协议,请确保你的协议能够处理我们在此概述的场景。具体来说,可重试票据的使用不应依赖于它们的顺序或成功执行。你可以使用我们的新Slither检测器发现潜在的乱序执行漏洞!
如果你的应用程序与Arbitrum Nitro组件交互,或者你正在构建具有rollup-基础链通信功能的软件,请联系我们,了解我们如何提供帮助。
如果你喜欢这篇文章,请分享:
Twitter LinkedIn GitHub Mastodon Hacker News
页面内容
可重试票据
等等,“可重试”是什么意思?
问题出在哪里
乱序执行漏洞
Slither来救援
结论
近期文章
使用Deptective调查你的依赖项
系好安全带,Buttercup,AIxCC的评分回合正在进行中!
使你的智能合约超越私钥风险
Go解析器中意外的安全隐患
我们从审查Silence Laboratories的首批DKLs23库中学到了什么
© 2025 Trail of Bits.
使用Hugo和Mainroad主题生成。