[ad_1]
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 :
- SelfiePool.sol
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 !) - SimpleGovernance.sol
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. - DamnValuableTokenSnapshot.sol
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)
Selfie Pool
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
L32 — 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 receiveTokens
)
L47–49 — Finally, the function verify the flashloan has been at least paid back (or more)
L52 — 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.
SimpleGovernance
(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 executeAction
function.
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 theflashLoan
function, requesting a loan equal to the whole balance of the pool. - L47 —
receiveTokens
which is called by the pool during the flashloan.
This function trigger asnapshot
of 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 thedrainAllFunds(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
[ad_2]
Source link