Appearance
The Selfie Exploit: How a Flash Loan Can Hijack Your DeFi Governance
Imagine a brand-new DeFi lending pool, gleaming with the promise of innovative financial services. It offers instant, zero-collateral flash loans of its native DVT tokens, and boasts a "fancy governance mechanism" to ensure community control. What could possibly go wrong?
This is the premise of the "Selfie" challenge from Damn Vulnerable DeFi v4. You start with nothing, but 1.5 million DVT tokens are locked in the pool, waiting to be rescued and deposited into a designated recovery account. This isn't just about stealing funds; it's about cleverly manipulating the very governance system designed to protect them.
The Setup: A Triad of Contracts
At the heart of this challenge lies a triad of interconnected smart contracts:
SelfiePool.sol: This is the lending pool itself. It allows users to take flash loans of DVT tokens. Critically, it also has anemergencyExit(address receiver)function. However, this function is protected by anonlyGovernancemodifier, meaning only theSimpleGovernancecontract can call it.SimpleGovernance.sol: This contract holds the keys to theemergencyExit. It allows actions to bequeueActiond and laterexecuteActiond. ToqueueAction, the caller needs to have "enough votes" – specifically, more than half of the total supply of the governance token. Once an action is queued, there's a mandatoryACTION_DELAY_IN_SECONDS(2 days) before it can be executed.DamnValuableVotes.sol: This is the DVT token. Crucially, it's not just a standard ERC20; it's also the voting token for theSimpleGovernancecontract. It includes adelegatefunction, allowing token holders to assign their voting power to another address.
The challenge's apparent security lies in the governance mechanism: you can't just call emergencyExit directly. You need the governance to approve it, and that requires a majority vote. How can an attacker with no DVT tokens possibly achieve that?
The Vulnerability Revealed: A Fleeting Majority
Here's where the seemingly secure system takes a dramatic turn. The SimpleGovernance contract determines voting power based on the getVotes(who) function of the DamnValuableVotes token. To queueAction, you need _votingToken.getVotes(msg.sender) > _votingToken.totalSupply() / 2.
The key insight is the combination of flash loans and the dual nature of the DVT token as both liquidity and governance power.
What if you could temporarily acquire a dominant share of these voting tokens? A flash loan provides exactly that ephemeral opportunity. You can borrow all the DVT tokens from the SelfiePool, hold them for a single transaction, and then return them. This brief moment of immense liquidity gives the attacker unprecedented voting power.
The Attack in Action: A Daring Two-Step Heist
Our attacker, represented by the MyContract exploit contract, executes the heist in two phases:
Phase 1: The Instant Coup (Inside the Flash Loan Callback)
- Flash Loan Initiation:
MyContractinitiates a flash loan fromSelfiePool, requesting all of the DVT tokens held within the pool (1.5 million). - The Ephemeral Window: When the
SelfiePoolgrants the flash loan, it callsMyContract'sonFlashLoanfunction. This is the attacker's golden moment. - Delegate and Dominate: While holding all 1.5 million DVT tokens,
MyContractimmediately callstoken.delegate(address(this)). This instantly grantsMyContractmajority voting power within theSimpleGovernancesystem. - Queue the Heist: Capitalizing on this fleeting dominance,
MyContractswiftly callsgovernance.queueAction(address(pool), 0, data), wheredataencodes a call toemergencyExit(recovery). SinceMyContractnow satisfies the_hasEnoughVotescondition (it controls >50% of the total DVT supply), the action is successfully queued! - Repay and Retreat: After queueing the action,
MyContractapproves theSelfiePoolto take back its tokens to satisfy the flash loan repayment. The DVT tokens return to the pool, and the system appears to revert to its normal state. But the seed of destruction has been planted.
Phase 2: The Delayed Execution (After the Waiting Game)
- The Waiting Game: The
SimpleGovernancecontract requires a 2-day delay (ACTION_DELAY_IN_SECONDS) before a queued action can be executed. The attacker must wait patiently. - Final Strike: After the 2 days have passed,
MyContractcallsgovernance.executeAction(actionId), targeting theemergencyExitaction it had previously queued. - Mission Accomplished: The
executeActioncall triggers theemergencyExitfunction inSelfiePool, which then transfers all 1.5 million DVT tokens from the pool directly to the designatedrecoveryaccount.
The Takeaway: Governance Requires Robust Design
The "Selfie" challenge serves as a stark reminder of the profound impact flash loans can have on DeFi protocols, especially those with governance mechanisms tied to liquid pool tokens. The apparent security of a multi-day voting delay was completely circumvented by the ability to temporarily amass overwhelming voting power, even if only for the duration of a single transaction.
The lesson is clear: Governance mechanisms, especially those tied directly to the liquidity of a protocol's core assets, must be rigorously designed to withstand such "temporary majority" attacks. Relying solely on a vote count derived from liquid, flash-loanable tokens, without additional safeguards (like time-weighted average voting, minimum lock-up periods, or differentiated governance tokens), leaves a critical vulnerability open.
The "Selfie" exploit isn't just a clever hack; it's a critical case study in DeFi security, showcasing how unexpected interactions between seemingly innocuous features can lead to devastating consequences. It's a "selfie" of a system caught off guard by its own reflection.