说明
基本信息
Attacker: 0xa0959536560776ef8627da14c6e8c91e2c743a0a
Vulnerable Contract:
- 0x35dD16dFA4ea1522c29DdD087E8F076Cad0AE5E8
- 0x4DEcA517D6817B6510798b7328F2314d3003AbAC
Attack Tx:
LOSS: 704ETH
漏洞函数
在合约Initializable (0x35dd16dfa4ea1522c29ddd087e8f076cad0ae5e8)中的变量initialized和initializing都是
1 | contract Initializable { |
在Initializable合约中的initialized和initializing都是bool类型,由于 initialized 和 initializing 都是 bool 类型变量,因此他们各自都只占据一字节,所以说它们俩实际上是被打包放在了 slot 0 中。
通过实际的检测分析也得出的是一样的结论:

上面是在实现合约中的分析结果。
下面继续分析在代理合约0x4DEcA517D6817B6510798b7328F2314d3003AbAC中的结果。分析在代理合约中的storage layout。
发现在代理合约中slot 0的结果是proxyAdmin。
代理合约和实现合约在同一个Slot中对应的是不同的变量,这样就造成了storage collision类型的漏洞。
接下来就是看看在代理合约 0x4DEcA517D6817B6510798b7328F2314d3003AbAC 中的slot 0的结果,通过cast storage的方式查看得到如下结论:
1 | cast storage 0x18aAA7115705e8be94bfFEBDE57Af9BFc265B998 0 --rpc-url=https://rpc.ankr.com/eth |
代理合约中的proxyAdmin的0x0000000000000000000000004deca517d6817b6510798b7328f2314d3003abac 如何对应到实现合约中的initialized和initializing上呢?
对应关系如下所示:
可以看到最终initialized和initializing分别映射到了0xab和0xac中,存在冲突,与预期的不一致。
initialized 和 initializing 这两个变量的值使用了 ProxyAdmin 实际值的最后两个字节!而恰好最后两个字节(0xAB, 0xAC)都是非零值,这也就造成在实际可升级合约的数据读取中,initialized 和 initializing 的值总是 true。
这个冲突具体会造成什么样的后果呢?分析initializer 这个修饰符。
1 | modifier initializer() { |
require(initializing || isConstructor() || !initialized, "Contract instance has already been initialized");因为initializing为true,所以require的结果是True。bool isTopLevelCall = !initializing;, 所以isTopLevelCall为False,那么后面的代码都不会执行。
所以initialized 和 initializing 的值总是 true,就导致了修饰符initializer返回的永远都是True,即没有任何的作用。
攻击分析
84号提案
governanceAddress

通过initialize()函数将governanceAddress设置为攻击合约的地址。
evaluateProposalOutcome

提议84号提案。
按照audius官方的说明
Governance proposal #84 was created to transfer the entirety of the Audius community pool to the attacker’s wallet (0xa62c3ced6906b188a4d4a3c981b79f2aabf2107f), but did not pass quorum, so failed during execution.
https://dashboard.audius.org/#/governance/proposal/84
没有通过法定人数,因此在执行过程中失败。
85号提案
submitProposal
1 | uint256 audioBalance_gov = IERC20(AUDIO).balanceOf(governance); |
submitVote
1 | IGovernence(governance).submitVote(85, IGovernence.Vote(2)); |
对应的tx是 0x3c09c6306b67737227edc24c663462d870e7c2bf39e9ab66877a980c900dd5d5
evaluateProposalOutcome
1 | IGovernence(governance).evaluateProposalOutcome(85); |
对应的tx是:0x4227bca8ed4b8915c7eec0e14ad3748a88c4371d4176e716e8007249b9980dc9

创建治理提案 #85 以将整个 Audius 社区池转移到攻击者的钱包(0xbdbb5945f252bc3466a319cdcc3ee8056bf2e569)并成功执行。
最红获利代币
SWAP
将所有获得代币全部转换为ETH
对应的tx:0x82fc23992c7433fffad0e28a1b8d11211dc4377de83e88088d79f24f4a3f28b3
攻击者将攻击获得的18564497819999999999735541代币全部转换为ETH,获得704177543861243828018
获利
根据最终的SWAP交易,最终攻击者获利约为704个ETH。按照当时的价格换算是1.08M。
总结
连续分了两个和Storage Collision的漏洞。从分析过程也可以发现,智能合约的开发人员如果对代理模式不够了解,很容易就写出的Storage Collision漏洞,而且这种类型的漏洞很难被发现。
但是即使存在Storage Collision类型的问题,也不一定会形成漏洞,一般是需要合约中的其他的条件。
参考
https://mirror.xyz/xyyme.eth/IQ8uMgQ11S7YK_Tt4sR3E1iPq6MBrdu5WqHwdFPwWuw
https://twitter.com/BeosinAlert/status/1551041795735408641
https://etherscan.io/address/0x35dd16dfa4ea1522c29ddd087e8f076cad0ae5e8#code