Zora的NFT空投系统中的一个漏洞允许攻击者通过利用薄弱的声明检查来窃取代币。攻击者利用0x Settler合约的basicSellToPool功能,使得ZoraTokenCommunityClaim合约将0x的ZORA代币分配转移到了攻击者的地址。这次攻击暴露了智能合约设计中安全性的重要性,并强调了严格的访问控制的必要性。
Zora的NFT空投系统中的一个漏洞允许攻击者通过利用薄弱的claim检查来窃取token。 这表明了安全智能合约设计至关重要的原因。
Zora是一个去中心化的内容创作者协议,专注于NFT和创作者驱动的市场。在Zora的模式中,创作者在一个开放的市场中铸造和销售带有内置版税的NFT。最近在Base上推出的“$ZORA”token被定位为社区的内容币:一个100亿token的“为了乐趣”的meme token(没有治理权),将追溯空投给Zora协议的早期用户。理论上,符合条件的用户(创作者、收藏家、开发者等)可以通过官方claim合约(ZoraTokenCommunityClaim
),地址为0x0000000002ba96C69b95E32CAAB8fc38bAB8B3F8,来claim他们分配到的ZORA。Zora团队表示,必须通过官方网站(claim.zora.co)与该合约进行claim,该合约会将用户分配到的ZORA token转移给他们。
ZoraTokenCommunityClaim
合约的部署是为了持有和分发ZORA供应量的10%,这部分token被预留用于追溯空投。在实践中,ZORA团队将分配的token转移到这个合约中,并在链下设置了每个符合条件的钱包的分配额。该合约的作用是强制只有符合条件的用户才能在claim处理启用后claim预先分配的数量。在内部,它跟踪诸如allocationSetupComplete
(一个布尔值,一旦所有分配加载完成,该值就会变为true),claimStart
(允许claim的时间戳或区块),以及一个映射accountClaims
来记录哪些账户已经claim过。(规模:空投总计约1,000,000,000 ZORA,分发给约240万个地址。)
关键功能包括:
setAllocations(...)
– 一个仅所有者可调用的函数,用于注册批量的接收者地址及其token数量(在设置期间使用)。completeAllocationSetup()
– 最终确定分配阶段(例如,设置allocationSetupComplete=true
和/或记录claimStart
时间),以便可以开始claim。claim()
– 供符合条件的用户认领其token;它检查分配是否已设置,发送者尚未认领(accountClaims[msg.sender] == false
),然后将他们标记为已认领并将token转移到指定地址。claimWithSignature(...)
– 类似于claim()
,但允许第三方通过提供用户的EIP-712签名来代表用户claim。此函数采用额外的_claimTo
地址参数,以便token可以发送到任何指定的接收者。basicSellToPool
0x 的“Settler”Basic流程是一种通用的一步式交换程序,用于许多链上流动性来源。 在这种设计中,Settler合约首先确保它可以转移用户的token,然后直接调用目标DEX/Pool:Pool合约提取卖方token并返回买方token。 在代码中,函数basicSellToPool(IERC20 sellToken, uint256 bps, address pool, uint256 offset, bytes memory data)
编排了这一点。 参数sellToken
是正在出售的token(为原生ETH使用特殊的ETH sentinel)。 bps
(基点)参数指示要出售的合约当前余额的百分比。 pool
是要调用的DEX合约的地址,data
是该pool的swap函数的编码调用数据。 offset
告诉 Settler 在 data
中覆盖金额字段的位置。
函数逻辑和安全检查:
_isRestrictedTarget(pool)
并触发还原(通过ConfusedDeputy()
错误)来阻止任何受限目标调用。这通过禁止某些地址来防止滥用。sellToken
是ETH,它计算value = (address(this).balance * bps) / BASIS
。如果提供的data
为空(即,没有calldata),它需要offset==0
,然后执行简单的pool.call{value: value}("")
,直接发送ETH。否则,它将32添加到offset,并将value
存储到该位置处的data
中,以便pool调用将使用正确的ETH金额。sellToken
是一个ERC-20 token(address != 0),它计算amount = sellToken.balanceOf(address(this)) * bps / BASIS
。然后,它将amount
写入offset+32
处的data
中。如果token的地址与pool不同,它会调用sellToken.safeApproveIfBelow(pool, amount)
,以便pool可以提取token(这是approval逻辑)。(success, returnData) = pool.call{value: value}(data)
。它在失败时还原。在调用返回后,它会检查是否没有返回数据(这意味着可能调用了非合约),然后它会还原并显示InvalidTarget()
。虽然Zora的claim网站的FAQ声明“合约是否经过审计?是的,由Zellic审计。”,但尚未发布公开的审计报告。Zellic自己的已发布审计列表没有Zora的条目,并且BaseScan上ZoraTokenCommunityClaim
的已验证源页面显示“未提交合约安全审计。”因此,社区无法审查审计的范围、发现或补救状态。
ZORA社区claim合约中的一个缺陷允许攻击者劫持分配给0x地址的token。攻击者构造了一个对0x Settler(execute()
)的调用,该调用触发了一个以ZORA claim合约为目标的basicSellToPool
操作。由于claim合约的内部_claimTo(address user, address to)
函数不要求msg.sender == user
,因此Settler调用导致合约将分配给0x的ZORA token发行给攻击者的地址。换句话说,攻击者向Settler的execute()
发送了一个交易,导致Settler将ZORA“出售”给claim合约,而claim合约又执行了隐藏的_claimTo(attacker, attacker)
。结果是,原本给0x的ZORA token最终进入了攻击者的钱包。
1) 攻击者调用Settler.execute(): 攻击者向0x Settler V1.10合约(地址0x5C9bdC80...
)发送了一个交易,调用了execute(...)
函数。该交易输入数据编码了一个操作:basicSellToPool
。在此payload中,sellToken
被设置为ZORA token地址,pool
被设置为ZoraTokenCommunityClaim合约地址,而data
字段是对ZoraTokenCommunityClaim._claimTo(attacker, attacker)
的ABI编码调用。
2) _dispatch 和 basicSellToPool: 当execute()运行时,Settler的内部_dispatch逻辑将此操作路由到basicSellToPool(...)。在Settler代码中,basicSellToPool检查pool地址是否允许,然后执行低级调用:
3) 在这里,pool是ZORA claim合约,而data是编码的_claimTo(attacker, attacker)。由于Settler合约不限制调用任意合约,因此它转发了该调用。
4) Claim合约执行_claimTo: 低级调用调用了ZoraTokenCommunityClaim._claimTo(attacker, attacker)。 在claim合约内部,_claimTo授权将用户的ZORA分配转移到指定地址。 因为_claimTo不检查msg.sender是否等于用户,所以它只是处理了claim。 结果是,ZORA token 从claim合约转移到了攻击者的地址。
核心问题是claim合约中缺乏访问控制。一个简单的修复方法是要求_claimTo
的调用者是受益于claim的帐户。例如,在_claimTo(user, to)
中添加require(msg.sender == _user, ...)
检查将确保只有实际接收者才能触发他们的claim。或者,claim合约可能只暴露一个用于发送者自己地址的claim()
函数,或者要求接收者签名的有效Merkle证明。正如它所说的那样,Settler可以作为任意调用者调用_claimTo
。
在0x Settler方面,该合约是一个通用的执行器,因此其行为本质上没有错误——它完全按照编程执行。(原则上,Settler可以将已知的“claim”合约列入受限目标黑名单,但这对于每次空投来说都难以维护。)简而言之,漏洞在于claim合约的设计中。在claim逻辑中严格执行msg.sender
将阻止漏洞利用,因为Settler的调用会失败。
0x 文档 明确警告:
攻击者有效地将 ZORA token从 0x 的社区分配转移到他们自己的钱包中(根据一些估计,大约 $128k)是从 0x 的分配中获得的。 除了经济损失之外,此事件也削弱了对空投的信心。 混乱的发布(在此期间甚至 BaseScan 都短暂关闭)。
随后的漏洞利用导致价格波动——ZORA 的市值在空投发布后的几个小时内暴跌超过 60%。
截至撰写本文时,Zora Labs 尚未发布正式的公开事后分析或补丁公告。
0x 团队在社交媒体上澄清说,Settler 合约本身没有被“黑客入侵”或存在缺陷——它只是在token存在时按设计执行。 实际上,0x 的人员指出,该漏洞是由于token分配造成的,而不是 Settler 代码错误。
_claimTo(...)
才有可能利用该漏洞。 换句话说,0x 的代码没有问题——问题在于空投合约接受了 Settler 的调用并转移了token,就好像它是有效的claim一样。execute()
调用中编码了一个 basicSellToPool
操作,其中 ZORA token和 claim 合约地址作为参数。 这导致 Settler 对 claim 合约执行了一个低级 pool.call(...)
,该合约执行了隐藏的 _claimTo(attacker, attacker)
调用。 因为合约没有检查调用者,所以不需要权限或签名。msg.sender
的智能合约都容易受到攻击。 事实上,空投合约通常应该_要求_只能claim接收者自己的地址。 项目应审查其claim逻辑以防止这种情况发生。msg.sender
== 所有者检查)。
- 原文链接: threesigma.xyz/blog/expl...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!