Damn Vulnerable DeFi is a serie of CTF games where the player must find a vulnerability to break or steal a protocol.These educational games are very interesting in a sense that they kind of mimic real apps (flashloans, pool, yield, …), allowing us to not only learn web3 security, but also what is DeFi.Link here : https://www.damnvulnerabledefi.xyz/index.html In this series of articles, I will present the challenges and their solution.
If you’re new in Solidity, I suggest you to first check the previous challenges as I sometimes quickly pass over some concepts we already saw before
A new cool 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. Your goal is to take them all
– See the contracts
– Complete the challenge
First thing, we need to understand how the protocol works. The protocol is made of 3 main contracts :
This pool is like the other ones, it gives flashloans to users (loans that must be returned at the end of the transaction, if not returned it trigger a revert). The particularity if this pool compared to the others is that it is controlled by a governance (next contract). The governance has the ability to drain all funds (if you see a protocol like this one, run !)
This is the governance contract, the one that control SelfiePool funds. To be part of this governance, users need to hold governance tokens, which are CVT tokens here, with snapshot capabilities added. Users can propose an action, and depending on some conditions the action will be executed.
The governance token used by the governance contract. An ERC20 token with snapshotting capabilities (capable of screening balances of each users at a particular time)
Quick notes after first read :
L1 — solidity ^0.8.0 → automatic overflow/underflow checks
L4&13 — Contract inherit OpenZeppelin Reentrancy Guard module
L6&15— Uses OpenZeppelin Address library. This library add checks when a
call is executed on an address. More details here, but in a nutshell address must be a contract, call must be successful, and if not error messages are better handled.
L17–18 & 27–30 — Governance token and governance contract are initialized at constructor, they cannot be modified afterward
L22 — Modifier to restrict usage of a function to the governance only
flashLoan uses reentrancy guard through its modifier.
L34 — The function check that its ETH balance is sufficient for the loan and then transfer the loan to the user.
L38 — Once the token transfer is done successfully, the function check that msg.sender is a contract (L38). In fact, this check could be removed, as
functionCall comes from the Address module which already make this check.
L39–45— This call trigger the
receiveTokens in the requester contract (but it will call its
fallback() function if it does not implement
L47–49 — Finally, the function verify the flashloan has been at least paid back (or more)
drainAllFunds is a special function that can be trigger by the governance only (modifier) used to transfer the whole token balance to a specific address.
This function is dangerous as a powerful actor or group of actors (with more than 50% of the voting power : check SimpleGovernance _hasEnoughVotes function) could chose to drain the funds.
(I will go quick over this contract, highlighting its main functionality)
This contract is what we usually call a (simple and imperfect) DAO (Decentralized Autonomous Organization). To be a member of this organization, users need to hold an amount of governance token.
Once you are a member of the gouvernance you can propose an action L38
queueAction , but to do this you need to have enoughVotes (L87
hasEnoughVotes ) which is equal to at least 50% of the supply.
An action is represented by a struct (L15
GovernanceAction), where the data field is basically used to store a abi encoded function to be executed afterward in the
Once an action is successfully queued, it can be executed (L56
executeAction) if the condition in
_canBeExecuted (L79) are valid, here the action can only be executed once, and a 2-day minimum delay must be respected.
Maybe you haven’t noticed it because it is defined in the challenge.js file, but the pool gives loans of governance token. The same governance token that control the governance… Now you remember that a bad actor with enough token share can propose an action. This action consist in executing an arbitrary call. And this call could for example be
drainAllFunds , why not after all ?
In fact, this happened multiple time, here an example of what we could call a governance flashloan attack (the attack is a bit different)
Let’s check how do do that with my attack contract.
My contract is composed of 3 functions, corresponding to the 3 phases of the attack :
- L41 —
attackRewardPool()which call the
flashLoanfunction, requesting a loan equal to the whole balance of the pool.
- L47 —
receiveTokenswhich is called by the pool during the flashloan.
This function trigger a
snapshotof the governance token, as this is what is accounted by the governance to check if a user has the right to queue an action.
L56–60 — Then after making sure the snapshot allow me to queue the action, I queue the
drainAllFunds(address)function to the governance list of actions, where the address is myself, the owner of the contract
L62 — Finally, I pay back the loan as it is requested by the lending pool to not revert
- After the 2 day-period I can call the
finalAttack()function of my contract, which will execute the action, and send my all the funds !
To end this, let’s now implement this in the challenge.js file by writing this in the Exploit section :
Recommended Mitigation Steps
The governance did think to implement a delay period for the action to be executable, which is a really good idea.
Also, an event is emmited when an action is queued, the event can be catched on-chain and for example forwarded to members of the DAO by e-mail, giving them 2-days to protect themselve from what is coming.
The pool shouldn’t flashloan the governance tokens that controls it !.. Overall, a “drainAllFunds” function is clearly undesirable and source of danger, and should be seen as redflag.
If you are interested in flashloan attacks, there’s multiple mitigation methods that exists, but none of them is perfect. It will usually increase centralization (for example allowing a list of privileged address to unqueue an action, or less intrusive, to pause the governance contract)
Hope you enjoyed this article and see you next time for a new challenge !
New to trading? Try crypto trading bots or copy trading on best crypto exchanges