Damn Vulnerable Defi Walkthrough Part One: Challenge 1–6.
You can find part two here and the repository here.
I started learning solidity in June by participating in Chainlink’s Smart Contract Developer Bootcamp. I liked it a lot so started digging further in the rabbit hole.
Later this year in September I got selected to participate in Secureum’s Smart Contract Security Bootcamp. From the first RACE (Readiness Assessment for CARE) phase with 1024 initial participants, I made it to the top 128 that progressed to the Audit Readiness Phase, where I had the privilege to create the first CARE (Comprehensive Audit Readiness Evaluation)audit report on my own. You can read about that in my next blog post.
During the learning phase, I had quite a lot of fun and learned new audit techniques on a daily basis but the biggest impact was the hands-on capture the flag challenges from @tinchoabbate the Damn Vulnerable Defi.
While I was solving the challenges I was constantly torn between the I want to solve it all immediately and the let’s keep some for tomorrow otherwise I will finish it too quickly feelings. So to preserve something from this enjoyment I would like to present my solutions and the thoughts behind them.
If you would like to go straight to the code here is the repository.
Challenge #1 — Unstoppable
There’s a lending pool
UnstoppableLender
with a millionDamnValuableToken
(DVT) tokens in balance, offering flash loans for free.
If only there was a way to attack and stop the pool from offering flash loans …You start with 100 DVT tokens in balance.
So all we have to do is to break the pool. Cool, I was always good at breaking things :)
To understand better I always started the challenges by actually using the service the particular smart contract offers. Once that works I tried to solve the challenge itself.ReceiverUnstoppable
is an example contract showing how to take a flash loan. We will use this functionality many times in other challenges so worth taking a look at it :
- implement the
receiveTokens(address tokenAddress, uint256 amount) external
function, once this is called by the pool it means that the loan is on the contract’s balance and we have to repay it at the end of the current transaction. - call the pool’s
flashLoan
function to initiate the flash loan.
Okay, we can take a flash loan and we can pay it back but how will we stop the pool from offering it? After examining the loaner contract’s flashLoan
function one thing should immediately catch our attention: the strict equality check between two integers wrapped in the assert statement.
assert(poolBalance == balanceBefore);
Secureum has a pretty nice collection of pitfalls and best practices where the following item can be found:
Dangerous strict equalities: Use of strict equalities with tokens/Ether can accidentally/maliciously cause unexpected behavior. Consider using >= or <= instead of == for such variables depending on the contract logic. (see here)
So the internal accounting checks thatpoolBalance == balanceBefore
.poolBalance
is calculated only in thedepositToken
function by adding the deposited amount to the old balance. And the balanceBefore
is the current balance of the contract before the flash loan. So adding tokens to the contract’s balance by not using the deposit
function will break the internal accounting and next time someone executes the flashLoan
function the equality check will fail and finally we get an answer to the decade-old question: What happens when an unstoppable force meets an immovable object?
The solution is the following:
1. Transfer some DVT tokens from the attacker to the pool.
Challenge #2 — Naive receiver
There’s a lending pool
NaiveReceiverLenderPool
offering quite expensive (1 ETH) flash loans of Ether, which has 1000 ETH in balance.
You also see that a user has deployed a contractFlashLoanReceiver
with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.
Drain all ETH funds from the user’s contract. Doing it in a single transaction is a big plus ;)
Examining the pool contract’s flashLoan
function we can immediately see that the loanee’s address borrower
passed as a parameter and the transaction can be initiated by anyone since there is no restriction on that in the function body. Combine this with the outrageous 1 ETH fee which is deducted from the borrower
and the solution is self-explanatory:
1. Create a contract that calls the pool’s flashLoan
function passing FlashLoanReceiver
‘s address as the borrower.
2. Do it ten times and the borrower
will run out of funds.
Deploy and call the attack function:
Challenge #3 — Truster
More and more lending pools are offering flash loans. In this case, a new pool
TrusterLenderPool
has launched that is offering flash loans of DVT tokens for free.
Currently the pool has 1 million DVT tokens in balance. And you have nothing.
But don’t worry, you might be able to take them all from the pool. In a single transaction.
The lender pool’s flashLoan
function takes four parameters: borrowAmount
the amount of the flash loan, borrower
the one whom the amount will be transferred, target
an address to perform a function call into with a payload defined as the data
parameter. From the function body, it turns out there is no pre-defined function that the loanee contract must implement to repay the loan. Rather the lending pool trusts the loanee contract to provide an arbitrary contract address whose arbitrary function will be blindly called by the lending pool with arbitrary parameters hoping that it does what is expected: repays the loan. Well no luck with us. How about passing in the ERC20’s approve
function with the attacker’s address as the spender
and the total pool supply as the amount
and thanks to the functionCall
the pool will be the derived owner
. And what about the loan itself? How it will be paid back? Well if there is no loan it should not be paid back.
The solution is the following:
1. Create a contract that calls the pool’s flashLoan function
2. Pass theamount
as 0, borrower
as the current contract, target
as the DVT token address, data
as the encoded approve
function where the attacker is the spender
and the pool’s balance is the amount
3. Transfer the approved allowance from the pool to the attacker.
The TrusterAttacker
:
Deploy it, call the attack function and transfer the tokens:
Challenge #4 — Side entrance
A surprisingly simple lending pool
SideEntranceLenderPool
allows anyone to deposit ETH, and withdraw it at any point in time.
This very simple lending pool has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.
You must take all ETH from the lending pool.
To take a flash loan the loanee contract must implement the execute
payable function which will be called by the pool sending the requested loan amount as the msg.value
. Okay, let’s say we implemented the callback, called the pool’sflashLoan
function the loan is sent and now it’s our turn to repay it…OR dodge somehow the following check :
require(address(this).balance >= balanceBefore, "Flash loan hasn't been paid back");
Wish there would be some side entrance besides paying the loan back to modify the lender pool’s balance… You guessed it right it is the deposit
function.
Having this the receipt is easy:
1. Take a flash loan
2. Deposit back into the pool
3. Pass the balance check, withdraw the money and upon receive
-ing, transfer it to the attacker’s address and live a happy life.
The SideEntranceAttacker
:
Deploy and attack:
Challenge #5 — The rewarder
There’s a pool
TheRewarderPool
offering rewards in tokensRewardToken
every 5 days for those who deposit their DVT tokens into it.
Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!
You don’t have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.
Oh, by the way, rumours say a new poolFlashLoanerPool
has just landed on mainnet. Isn’t it offering DVT tokens in flash loans?
Things are getting a bit complex from here, but the more complex is the logic and the infrastructure the more frequent is the likeliness of security vulnerabilities. And again, lots of hints in the description. Most likely we will need the flash loan so let’s implement that. Calling FlashLoanerPool
‘s flashLoan
function will provide us the desired loan and receiveFlashLoan
call back function must be implemented by our contract to repay the loan. Having that we have access now to 1 million DVT tokens for free even just for a timespan of a transaction.
We can only deposit DVT tokens to RewarderPool
. The AccountingToken
is an ERC20Snapshot
token used to track individual deposited DVT balances by minting upon deposit
and burning upon withdraw
.
Let’s focus on the RewardToken
since the goal is to claim the most in the next round. There is only one code block whereRewardToken
is minted and that is in the distributeRewards
function:
if(rewards > 0 && !_hasRetrievedReward(msg.sender)) {
rewardToken.mint(msg.sender, rewards);
lastRewardTimestamps[msg.sender] = block.timestamp;
}
Let’s try to pass the statements: rewards
is greater than zero if the caller has a positive balance of AccountingToken
at a given snapshot in time. To record such a snapshot we have to pass:
if(isNewRewardsRound()) {
_recordSnapshot();
}
isNewRewardsRound
evaluates true if at least 5 days passed since the last recorded snapshot.
_hasRetrievedReward
will evaluate false (which we need because of the !
negation) if we never received a reward before which is true in our case. If these two statements pass RewardToken
will be minted for the caller matching the amount of the rewards
.
So all we have to do is hold a lot of money and pose for a quick photo, and they will believe we are rich.
It could be someone else’s money borrowed just for a second…
You guessed it by now.
The solution is the following:
1. Wait 5 days and take a flash loan of DVT from the FlashLoanerPool
,
2. Deposit to the RewarderPool
,
3. Call distributeRewards
to get RewardToken
,
4. Withdraw DVT
5. Send RewardToken
to the attacker
6. Repay the loan.
The RewarderAttacker
:
Deploy the attacker contract wait 5 days and execute the attack:
Challenge #6 — Selfie
A new cool lending pool
SelfiePool
has launched! It’s now offering flash loans of DVT tokens.
Wow, and it even includes a really fancy governanceSimpleGovernance
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 objective: take them all.
Another pool with flash loans to be rekt. To take a loan our contract has to implement the pool’s receiveTokens
callback function. The loaned token DamnValueableTokenSnapshot
is an ERC20Snapshot
token introduced in the previous challenge. The pool also has a drainAllFunds
function that transfers all funds to the address defined as a parameter. This one looks promising. The function’s onlyGovernance
modifier only permits executions coming from the governance
address which points to the SimpleGovernance
contract. We can most likely find a way to execute transactions on behalf of SimpleGovernance
because that’s what governance contracts for to create proposals, vote for them, and execute them. After examining the governance contract’s executeAction
function we find exactly this execute functionality.
GovernanceAction storage actionToExecute = actions[actionId];
actionToExecute.executedAt = block.timestamp;
actionToExecute.receiver.functionCallWithValue(
actionToExecute.data,
actionToExecute.weiAmount
);
To reach this block we have to pass the _canBeExecuted
function in the require statement that checks if the action was never executed before and the action was registered at least 2 days ago. Okay, how to register a GovernanceAction
? The queAction
function does that for us. We have to pass two require statements here:
1. the receiver
(the called contract) cannot be the governance itself, that is a pass since our target is the pool.
2. the caller must pass the _hasEnoughVotes
check which we can if the caller address owns more than half of the supply of the governanceToken
at the latest snapshot of the token. Yes, SimpleGovernance
contract uses DamnValueableTokenSnapshot
as a governance token and SelfiePool
offers flash loans in the same token while usingSimpleGovernance
as its governor.
And this is where the circle closes and the pool takes a selfie in the mirror revealing some unwanted details.
The solution is the following:
1. Take the max amount of flash loan from the pool.
2. Take a token snapshot and take over the governance.
3. Queue an action that drains all funds from the pool.
4. Repay the flash loan.
5. Advance two days in time and execute the action.
The SelfieAttacker
:
Deploy it, execute theattack
, read the actionId
, advance 2 days in time and execute the action by the actionId
.
Thanks for reading along continue with part two here.
Cheers and happy (responsible) hacking!