Appearance
深入揭秘:DVT 代币桥的“安全漏洞”与你的守护使命
想象一下,在一个去中心化金融(DeFi)的世界里,你扮演着一个至关重要的角色——代币桥的守护者。你的任务是保护一座连接 L2 和 L1 的数字桥梁,确保上面价值百万的 DVT(Damn Valuable Tokens)代币安全无虞。然而,一个潜在的危险正悄然逼近,一场针对这座桥梁的“盗窃”正在酝酿。
故事背景:一条看似安全的代币桥
我们拥有一座 L2 到 L1 的代币桥,它目前储备着高达一百万枚 DVT 代币。这座 L1 侧的桥梁设计允许任何人,在满足特定条件的情况下,可以完成代币的提现。这些条件包括:
- 延迟期已过: 提现请求必须经过 L2 的一个标准延迟期,在 L1 才能被最终确认。
- 有效的 Merkle 证明: 提现者需要提供一个有效的 Merkle 证明,这个证明必须对应着由桥梁所有者设定的、最新的提现根(root)。
潜藏的危机:一份可疑的提现记录
你手中掌握着一份包含 4 笔 L2 发起的提现事件的 JSON 文件。理论上,这些提现都可以在 7 天的延迟期后在 L1 执行。然而,你敏锐的直觉告诉你,其中一定隐藏着一个“可疑分子”。
你的特殊使命:成为桥梁的守护者
作为这座桥梁的桥梁操作员,你拥有一种特殊的权力。你的目标是:
- 保护所有提现: 确保所有合法的提现都能顺利完成。
- 阻止可疑提现: 识别并阻止那个“破坏者”的非法提现。
- 保护代币总量: 在完成以上目标的同时,避免耗尽桥梁的所有 DVT 代币。
代码解析:漏洞的蛛丝马迹
通过分析提供的智能合约代码,我们可以找到问题的关键点:
L1Gateway.sol-finalizeWithdrawal函数:- 这个函数负责最终确定提现。
- 它首先检查
timestamp + DELAY > block.timestamp,如果提现时间未到,则会 revert。 - 关键点:
if (!isOperator)块中的 Merkle 证明验证。如果调用者不是操作员(operator),就必须提供有效的 Merkle 证明。 - 漏洞猜测: 如果“可疑提现”者拥有一个伪造的 Merkle 证明,或者找到了绕过 Merkle 证明验证的方法,就可能导致非法提现。
L1Forwarder.sol-forwardMessage函数:- 这个函数负责将 L2 的消息转发到 L1。
- 它通过
gateway.xSender() == l2Handler来判断消息是否来自 L2 Handler。 - 关键点:
require(failedMessages[messageId]);和require(!failedMessages[messageId]);的不同条件。 - 漏洞猜测:
L1Gateway在finalizeWithdrawal函数中,当isOperator为true时,并未验证 Merkle 证明。这意味着,作为操作员,你可以直接调用finalizeWithdrawal,绕过 Merkle 证明的验证。
TokenBridge.sol-executeTokenWithdrawal函数:- 这个函数处理实际的代币转移。
- 它检查
msg.sender == address(l1Forwarder) || l1Forwarder.getSender() == otherBridge。 - 关键点:
l1Forwarder.getSender() == otherBridge这个条件。 - 漏洞猜测: 如果我们能够让
l1Forwarder.getSender()返回一个非otherBridge的值,那么TokenBridge.executeTokenWithdrawal就可以被调用,从而执行代币的转移。
策略制定:成为真正的守护者
既然你拥有操作员的特殊权限,就可以利用这一点来扭转乾坤。
- 利用操作员身份: 作为操作员,你可以绕过
L1Gateway中的 Merkle 证明验证。这意味着,你可以主动调用finalizeWithdrawal函数,为所有提现(包括那个可疑的)提供一个“最终确认”。 - 篡改 L2 Sender: 重点在于
L1Forwarder.forwardMessage函数中的gateway.xSender() == l2Handler逻辑。如果gateway.xSender()的值可以被控制,并且不等于address(l2Handler),那么L1Forwarder就会执行require(failedMessages[messageId]);。- 关键思路: 在
L1Gateway.finalizeWithdrawal中,xSender的值会被设置为l2Sender。如果我们将一个非l2Handler的l2Sender传递给finalizeWithdrawal,那么当L1Forwarder收到这个消息时,gateway.xSender()就会是这个非l2Handler的值。 - 攻击方式: 我们可以构造一个“可疑”的提现,其
l2Sender被设置为一个已知的值,但这个值不是l2Handler。当我们调用finalizeWithdrawal时,l1Gateway.xSender会被设置为这个可疑的l2Sender。 - 阻止可疑提现: 当
L1Forwarder.forwardMessage被调用时,它会看到gateway.xSender()(即我们设置的可疑l2Sender) 不等于l2Handler,因此会执行require(failedMessages[messageId]);。由于该消息 ID 尚未被标记为失败,这将导致forwardMessage失败,从而阻止了可疑提现的执行。
- 关键思路: 在
解决方案的核心:
- 跳过延迟期: 利用
vm.skip来模拟 7 天的延迟期过去。 - 模拟提现数据: 准备好所有 4 个提现的数据,包括
nonce,l2Sender,target,timestamp,message以及 空 Merkle 证明(因为我们是操作员)。 - 构造 Payload:
- 首先,你需要调用
TokenBridge.executeTokenWithdrawal,将代币转移到你指定的接收者。你需要构建message参数,指向TokenBridge.executeTokenWithdrawal函数,并包含接收者地址和金额。 - 然后,你需要构建
forwardMessage的message参数,它将包含上述的TokenBridge.executeTokenWithdrawal的调用。
- 首先,你需要调用
- 调用
finalizeWithdrawal:- 对于前 3 个合法的提现: 调用
l1Gateway.finalizeWithdrawal,并使用真实的l2Sender。 - 对于第 4 个可疑提现: 这是关键!调用
l1Gateway.finalizeWithdrawal,但将l2Sender设置为一个我们知道的、但不是l2Handler的地址。这样,当L1Forwarder尝试处理这个消息时,由于gateway.xSender()不匹配,forwardMessage函数将执行require(failedMessages[messageId]);,导致该消息被标记为失败,从而阻止了可疑提现。
- 对于前 3 个合法的提现: 调用
- 最后一步: 由于我们已经通过
finalizeWithdrawal模拟了所有提现的执行,并且阻止了可疑提现。最后,再执行一个简单的代币转移,确保代币总数不会被完全耗尽,满足“不耗尽所有资金”的要求。
总结:
这个挑战巧妙地利用了 L1Gateway 和 L1Forwarder 之间的交互逻辑。作为拥有特殊权限的操作员,你不仅可以确保所有合法提现的执行,更可以通过操纵 l2Sender 的值,制造一个“假象”,让 L1Forwarder 认为可疑提现来自一个未知的 L2 Handler,从而触发失败逻辑,成功阻止了这次潜在的资产盗窃。你成功地完成了守护者的使命,保护了桥梁的安全!