关卡链接: 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
的标准功能来绕过不完整的安全限制。防御策略:
在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓