2023年6月26日,在调查与使用abi.decode相关的安全报告期间,发现了 Solidity 编译器旧版代码生成管道中的一个 Bug,其中三元表达式(带有副作用)用作类型参数。abi.decode 作为类型参数。
旧版代码生成器没有评估复杂的表达式,例如赋值、函数调用或条件语句,这些表达式正在访问其.selector。这导致此类表达式的副作用未执行,因此可能导致使用旧版管道编译的合约出现错误行为。通过 IR 的代码生成器按预期工作并评估此类表达式。
此 Bug 自编译器 0.6.2 版本起就存在。但是,据我们所知,它从未被利用或外部报告过。任何实际受影响的编码模式都可能被用户视为反模式,并被更合理的替代方案所取代。
例如,在f().g.selector这样的表达式中,无论f()返回什么,结果都将始终相同,并且在合约类型上直接执行操作更自然,即C.g.selector。此外,f().selector之类的表达式不受影响。因此,我们将其严重性级别设置为“低”。
哪些合约受影响?
如果您仍在使用旧版代码生成管道,并且您的合约在具有副作用的表达式上访问.selector,则您的代码可能容易受到此 Bug 的影响。例如,以下代码如果通过 IR 编译,将导致x被赋值为42,并且合约D作为评估函数调用h()的副作用而创建。另一方面,如果使用旧版代码生成管道进行编译,则将导致x为0,并且D未部署。
contract D { function f() external {} } contract C { uint256 x; function f() public { h().f.selector; } function h() public returns (D) { x = 42; return new D(); } }
下面,您可以找到另一个示例,该示例演示了使用赋值表达式而不是函数调用表达式。在示例中,如果使用了旧版代码生成管道,则表达式x = true将不会被正确评估,导致x的值保持不变。
contract D { function g() external {} } contract C { bool x; function f() external { ((x = true) ? C.f : D.g).selector; } }
技术细节
此 Bug 在可以确定表达式的结果是哪个函数(在编译时)的情况下表现出来,这允许编译器在不实际评估表达式的条件下计算选择器。旧版代码生成器将利用此捷径生成更高效的代码,但错误地假设该表达式一定是无副作用的。
相反,新的基于 IR 的代码生成器主要关注生成正确的代码,并将使其更高效的任务推迟到 Yul 优化器。这意味着它不必依赖于此类假设。整个表达式始终在未优化的代码中被访问和评估。只有在确定它实际上是无副作用时,优化器才会将其删除。
由于类型检查器中存在相关的 Bug,因此该 Bug 还会影响某些在编译时无法确定其值的表达式。编译器在内部将函数类型与函数定义相关联,并且关联的定义将被传递到表达式的结果类型。如果表达式涉及多个函数,则结果类型不一定指向正确的函数。
例如,即使在(condition ? C.f : D.g).selector中,其中condition的值仅在运行时已知,编译器也能够确定C.f和D.g的通用类型,并使用与该类型关联的定义来确定要从中获取选择器的函数。此 Bug 也已在Solidity v0.8.21版本中修复。
总之,此 Bug 直到现在才被发现的事实表明,实际项目似乎没有使用这种模式,因为.selector的预期用法是在常量表达式中。