Skip to content
On this page

闪电贷的陷阱:Selfie 挑战揭秘

想象一下,一个全新的借贷池刚刚上线,不仅提供闪电贷,还附带了一个“高级”的治理机制。听起来是不是很稳妥?但事实是,这里隐藏着一个巨大的安全漏洞,等待你去发现并“拯救”所有资产!这就是 Damn Vulnerable DeFi v4 中的 Selfie 挑战。

挑战目标:

你的任务是窃取 SelfiePool 中所有 DVT 代币,并将它们存入指定的恢复账户。初始时,你没有任何 DVT 代币,而池子已经“身处险境”,拥有 150 万枚 DVT 代币。

漏洞的根源:

要理解这个漏洞,我们需要深入分析 SelfiePool.solSimpleGovernance.sol 这两个核心合约。

  1. SelfiePool 的闪电贷机制:

    • SelfiePool 实现了 IERC3156FlashLender 接口,允许其他合约在零费用的情况下借入代币。
    • 关键在于 flashLoan 函数:它将代币转移给借款人,然后调用借款合约的 onFlashLoan 回调函数。
    • 最致命的缺陷在于,flashLoan 函数在调用 onFlashLoan 回调之后,并没有检查借款人是否成功偿还了代币,而是直接从借款人那里尝试token.transferFrom)将借入的代币取回。
  2. SimpleGovernance 的权限管理漏洞:

    • SimpleGovernance 允许持有超过总供应量一半投票权的地址(通过 _hasEnoughVotes 函数检查)来 queueAction(排队一个治理提案)。
    • queueAction 函数允许指定一个 target 地址、一个 value(ETH 数量)和一个 data(执行的函数调用)。
    • executeAction 函数则可以在提案排队一定延迟时间(ACTION_DELAY_IN_SECONDS,2天)后执行。
    • 漏洞点: SelfiePool 合约在 SelfiePool.sol 中存在一个 emergencyExit 函数,该函数允许仅由治理合约调用onlyGovernance 修饰符)将池子中所有代币转移给指定的接收者。

如何利用这个漏洞?

聪明的攻击者可以通过以下步骤,将 SelfiePool 中的所有 DVT 代币转移到指定的恢复账户:

  1. 部署一个攻击合约(MyContract): 这个合约将是执行攻击的核心。它需要继承 IERC3156FlashBorrower 接口,以便接收闪电贷。
  2. 发起闪电贷: 攻击合约调用 SelfiePool.flashLoan(),借入池中所有的 DVT 代币。
  3. onFlashLoan 回调中执行恶意操作:
    • 重要:onFlashLoan 回调中,我们不会立即偿还闪电贷。
    • 而是将借入的 DVT 代币转移到攻击合约自身。
    • 然后,利用 SimpleGovernance 合约的漏洞,排队一个治理提案 (queueAction)。这个提案的目标是 SelfiePool 合约,执行的函数是 emergencyExit,并将指定的恢复账户作为参数。
    • 关键在于: queueAction 函数只检查调用者是否拥有足够的投票权,而并不检查执行的函数是否真的会消耗掉代币。我们知道 SelfiePool.emergencyExit 函数实际上是将代币转走,而不是返还给池子。
    • 最后,攻击合约批准 SelfiePool 合约能够从攻击合约转移 DVT 代币。
  4. 等待延迟时间: 攻击合约需要等待 SimpleGovernance 合约定义的 ACTION_DELAY_IN_SECONDS(2天)才能执行提案。
  5. 执行治理提案: 在延迟时间过后,攻击合约调用 SimpleGovernance.executeAction() 来执行之前排队的 emergencyExit 提案。
  6. 资产转移: emergencyExit 函数被调用,将 SelfiePool 中剩余的所有 DVT 代币(因为闪电贷的代币还在攻击合约,但 emergencyExit 函数只是从 SelfiePool 的余额中转走,而 SelfiePool 并未收到闪电贷的偿还)转移到预设的恢复账户。

代码解析:

在提供的 MyContract.sol 代码中,这个攻击流程被清晰地实现:

  • exploit() 函数发起闪电贷。
  • onFlashLoan() 函数在回调中执行了关键操作:
    • token.delegate(address(this));token.transfer(address(this), amount); 是为了让攻击合约拥有足够的 DVT 代币去“发送”给治理提案,虽然这部分在攻击 SelfiePool 时不是直接必需的,但它模拟了拥有代币的场景,并且可能与 SimpleGovernance_hasEnoughVotes 检查有关(虽然本题中 _hasEnoughVotes 检查的是 getVotes,而不是实际代币余额,但了解这一点有助于理解更复杂的场景)。
    • actionId = governance.queueAction(address(pool), 0, data); 巧妙地排队了 emergencyExit 提案。
    • token.approve(address(pool), amount); 这一步看起来是偿还闪电贷,但由于 SelfiePoolflashLoan 函数的逻辑,它实际上并不会阻止 emergencyExit 的执行。
  • execute() 函数在延迟后执行提案。

总结:

Selfie 挑战是一个经典的“闪电贷 + 治理漏洞”组合攻击。它揭示了以下几个重要的安全教训:

  • 闪电贷的风险: 即使闪电贷本身没有利息,其允许在同一交易中进行多次操作的能力,也可能被用于组合利用其他合约的漏洞。
  • 严格的权限控制: 治理合约的权限不应该被轻率地赋予,并且执行的函数需要仔细审查,防止被恶意滥用。
  • 状态检查的重要性: 在执行关键操作(如闪电贷的偿还)后,必须进行状态的确认,而不是仅仅依赖于操作是否能成功执行。

通过理解这些合约的设计和潜在的交互方式,你就能成功地“拯救” SelfiePool 中的所有 DVT 代币,并将其安全地转移到指定的恢复账户。

Built with AiAda