关卡链接: Ethernaut Level 15 - Naught Coin
攻击类型: ERC20approve/transferFrom漏洞
难度: ⭐⭐☆☆☆
作为 player,你初始拥有全部的 NaughtCoin 代币。然而,合约中的 transfer 函数被锁定,十年内无法转移代币。你的目标是在锁定期结束前,将你的全部代币从你的地址中转移出去。

让我们看一下 NaughtCoin 合约。它继承自 OpenZeppelin 的 ERC20 标准合约。
1 | contract NaughtCoin is ERC20 { |
合约通过 override 重写了 transfer 函数,并为其增加了一个 lockTokens 修饰符。这个修饰符会检查 msg.sender 是否为 player,如果是,则要求 block.timestamp 大于 timeLock(十年之后)。这意味着我们作为 player,无法直接调用 transfer 函数来转移代-笔。
然而,开发者只重写了 transfer 函数,却忽略了 ERC20 标准中的另一个重要的代币转移函数:transferFrom(address from, address to, uint256 amount)。
transferFrom 函数允许一个地址(spender)在得到 owner 授权(approve)后,从 owner 的账户中转移代币到任何地址。
由于 NaughtCoin 合约没有重写 transferFrom,它将直接使用 OpenZeppelin ERC20 合约中的原始实现,而这个原始实现是没有 lockTokens 修饰符的!
因此,攻击路径变得清晰:
player)调用 approve 函数,授权给另一个地址(可以是自己,也可以是任何其他地址)转移我们的全部代币。transferFrom 函数,将代币从我们的账户中转移出去。在 Foundry 测试中,我们可以直接模拟这个过程。我们甚至不需要一个单独的攻击合约,因为 player 可以授权给自己来执行 transferFrom。
1 | // SPDX-License-Identifier: Unlicense |
player 地址拥有的代币总量。approve): player 调用 instance.approve(spender, amount),其中 spender 是被授权的地址,amount 是授权额度。在这里,我们让 player 授权给自己全部余额。transferFrom): player 调用 instance.transferFrom(from, to, amount),其中 from 是 player 地址,to 是接收地址,amount 是要转移的数量。这个过程成功地绕过了 transfer 函数的 timeLock 限制。
完整地覆盖函数: 当继承一个标准(如ERC20)并打算修改其核心功能时,必须确保所有相关的函数都被一致地修改。在这个案例中,如果 transfer 被锁定,那么 transferFrom 也应该被同样的方式锁定。
1 | // 正确的修复方式 |
使用成熟的代币锁定合约: 与其自己实现时间锁,不如使用经过审计和广泛使用的解决方案,例如 OpenZeppelin 的 TokenTimelock 合约。这些合约已经考虑了各种边缘情况。
transfer, approve, transferFrom, balanceOf, allowance 等。override): 在Solidity中,当子合约需要修改父合约的行为时,使用 override 关键字。但必须小心,确保所有相关的行为都被覆盖,以避免产生漏洞。prank: vm.startPrank 是模拟特定地址(如 player)执行操作的强大工具,使得在测试中模拟多步攻击流程变得简单。核心概念:
transfer)是不够的。approve 和 transferFrom 的组合是ERC20的一个核心功能,允许第三方代为转移代币。攻击向量:
transfer 函数,而没有限制 transferFrom 函数。approve 和 transferFrom 的标准功能来绕过不完整的安全限制。防御策略:
在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓