Appearance
闪电贷的陷阱:Selfie 挑战揭秘
想象一下,一个全新的借贷池刚刚上线,不仅提供闪电贷,还附带了一个“高级”的治理机制。听起来是不是很稳妥?但事实是,这里隐藏着一个巨大的安全漏洞,等待你去发现并“拯救”所有资产!这就是 Damn Vulnerable DeFi v4 中的 Selfie 挑战。
挑战目标:
你的任务是窃取 SelfiePool 中所有 DVT 代币,并将它们存入指定的恢复账户。初始时,你没有任何 DVT 代币,而池子已经“身处险境”,拥有 150 万枚 DVT 代币。
漏洞的根源:
要理解这个漏洞,我们需要深入分析 SelfiePool.sol 和 SimpleGovernance.sol 这两个核心合约。
SelfiePool 的闪电贷机制:
SelfiePool实现了IERC3156FlashLender接口,允许其他合约在零费用的情况下借入代币。- 关键在于
flashLoan函数:它将代币转移给借款人,然后调用借款合约的onFlashLoan回调函数。 - 最致命的缺陷在于,
flashLoan函数在调用onFlashLoan回调之后,并没有检查借款人是否成功偿还了代币,而是直接从借款人那里尝试(token.transferFrom)将借入的代币取回。
SimpleGovernance 的权限管理漏洞:
SimpleGovernance允许持有超过总供应量一半投票权的地址(通过_hasEnoughVotes函数检查)来queueAction(排队一个治理提案)。queueAction函数允许指定一个target地址、一个value(ETH 数量)和一个data(执行的函数调用)。executeAction函数则可以在提案排队一定延迟时间(ACTION_DELAY_IN_SECONDS,2天)后执行。- 漏洞点:
SelfiePool合约在SelfiePool.sol中存在一个emergencyExit函数,该函数允许仅由治理合约调用(onlyGovernance修饰符)将池子中所有代币转移给指定的接收者。
如何利用这个漏洞?
聪明的攻击者可以通过以下步骤,将 SelfiePool 中的所有 DVT 代币转移到指定的恢复账户:
- 部署一个攻击合约(
MyContract): 这个合约将是执行攻击的核心。它需要继承IERC3156FlashBorrower接口,以便接收闪电贷。 - 发起闪电贷: 攻击合约调用
SelfiePool.flashLoan(),借入池中所有的 DVT 代币。 - 在
onFlashLoan回调中执行恶意操作:- 重要: 在
onFlashLoan回调中,我们不会立即偿还闪电贷。 - 而是将借入的 DVT 代币转移到攻击合约自身。
- 然后,利用
SimpleGovernance合约的漏洞,排队一个治理提案 (queueAction)。这个提案的目标是SelfiePool合约,执行的函数是emergencyExit,并将指定的恢复账户作为参数。 - 关键在于:
queueAction函数只检查调用者是否拥有足够的投票权,而并不检查执行的函数是否真的会消耗掉代币。我们知道SelfiePool.emergencyExit函数实际上是将代币转走,而不是返还给池子。 - 最后,攻击合约批准
SelfiePool合约能够从攻击合约转移 DVT 代币。
- 重要: 在
- 等待延迟时间: 攻击合约需要等待
SimpleGovernance合约定义的ACTION_DELAY_IN_SECONDS(2天)才能执行提案。 - 执行治理提案: 在延迟时间过后,攻击合约调用
SimpleGovernance.executeAction()来执行之前排队的emergencyExit提案。 - 资产转移:
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);这一步看起来是偿还闪电贷,但由于SelfiePool的flashLoan函数的逻辑,它实际上并不会阻止emergencyExit的执行。
execute()函数在延迟后执行提案。
总结:
Selfie 挑战是一个经典的“闪电贷 + 治理漏洞”组合攻击。它揭示了以下几个重要的安全教训:
- 闪电贷的风险: 即使闪电贷本身没有利息,其允许在同一交易中进行多次操作的能力,也可能被用于组合利用其他合约的漏洞。
- 严格的权限控制: 治理合约的权限不应该被轻率地赋予,并且执行的函数需要仔细审查,防止被恶意滥用。
- 状态检查的重要性: 在执行关键操作(如闪电贷的偿还)后,必须进行状态的确认,而不是仅仅依赖于操作是否能成功执行。
通过理解这些合约的设计和潜在的交互方式,你就能成功地“拯救” SelfiePool 中的所有 DVT 代币,并将其安全地转移到指定的恢复账户。