Skip to content
On this page

Cracking the Naive Receiver: A Flash Loan Fee Fiasco

In the world of decentralized finance (DeFi) security, even the most seemingly innocuous design choices can hide critical vulnerabilities. The "Naive Receiver" challenge from Damn Vulnerable DeFi v4 is a prime example, demonstrating how a fixed fee, combined with a "naive" contract, can lead to a complete drain of funds.

This article delves into the "Naive Receiver" CTF challenge, exploring its setup, the subtle vulnerability, and the ingenious two-phase attack strategy required to rescue all funds.

The Challenge Setup: A Pool, A Receiver, and a Forwarder

Our scenario involves three core smart contracts:

  1. NaiveReceiverPool.sol:

    • This contract acts as a flash loan lender, holding an initial balance of 1000 WETH.
    • It charges a fixed fee of 1 WETH for every flash loan, regardless of the amount borrowed.
    • Crucially, it integrates with a BasicForwarder for meta-transactions and overrides the _msgSender() function: if msg.sender is the trustedForwarder, the actual sender is extracted from the end of msg.data.
    • The deployer is also designated as the feeReceiver.
  2. FlashLoanReceiver.sol:

    • A sample contract deployed by a "user," holding 10 WETH.
    • It implements the IERC3156FlashBorrower interface, meaning it can receive flash loans.
    • Its onFlashLoan function includes a critical check, ensuring only the NaiveReceiverPool can call it directly.
    • Most importantly, its _executeActionDuringFlashLoan() is empty. It performs no useful work with the borrowed funds.
  3. BasicForwarder.sol:

    • A permissionless meta-transaction forwarder. It allows users to submit signed requests (EIP-712) to execute transactions on their behalf, paying gas with a relayer.
    • It validates the request and the signature against the from address specified in the request.

The Goal: Rescue all 1010 WETH (1000 from the NaiveReceiverPool and 10 from the FlashLoanReceiver) and deposit them into a designated recovery account.

The Naive Vulnerability: Death by a Thousand Fees

The core vulnerability lies in the combination of the NaiveReceiverPool's fixed 1 WETH fee and the FlashLoanReceiver's empty _executeActionDuringFlashLoan() function.

When the FlashLoanReceiver is targeted for a flash loan:

  1. The NaiveReceiverPool sends the requested amount of WETH to the FlashLoanReceiver.
  2. The NaiveReceiverPool then calls FlashLoanReceiver.onFlashLoan().
  3. Inside onFlashLoan(), the FlashLoanReceiver's _executeActionDuringFlashLoan() is called, which, as noted, does nothing.
  4. The FlashLoanReceiver then approves and repays the borrowed amount + 1 WETH fixed fee back to the NaiveReceiverPool.

Since the FlashLoanReceiver starts with 10 WETH and performs no productive action with the loan, each flash loan simply drains 1 WETH from its own balance as a fee, transferring it to the NaiveReceiverPool (specifically, to deposits[feeReceiver], which is the deployer).

This means the FlashLoanReceiver can sustain exactly 10 flash loans before it runs out of its own WETH to pay the fees. After 10 such transactions, the FlashLoanReceiver will be completely drained, and the 10 WETH it initially held will now reside within the NaiveReceiverPool, attributed as deposits to the deployer (who is also the feeReceiver).

The Attack Strategy: A Two-Phased Assault

The solution leverages this fee-draining mechanism and the BasicForwarder's meta-transaction capabilities.

Phase 1: Drain the FlashLoanReceiver

The first step is to exploit the fixed fee vulnerability. We need to initiate 10 flash loans targeting the FlashLoanReceiver. The NaiveReceiverPool inherits a multicall function from Multicall.sol, which allows us to bundle multiple calls into a single transaction.

We construct an array of 10 identical calls, each invoking pool.flashLoan() with:

  • receiver: The FlashLoanReceiver contract.
  • token: WETH.
  • amount: 0 (since the FlashLoanReceiver doesn't use the loan, we don't need to borrow any significant amount; a 0-amount loan still incurs the 1 WETH fee).
  • data: Empty bytes.

Executing pool.multicall(calls) will trigger 10 flash loans. After this, the FlashLoanReceiver's balance will be 0 WETH, and the NaiveReceiverPool will have gained 10 WETH in fees, bringing its total WETH to 1010 WETH. These 10 WETH are added to the deployer's deposits because the deployer is the feeReceiver. So, deposits[deployer] now holds 1000 WETH (initial) + 10 WETH (fees) = 1010 WETH.

Phase 2: Withdraw All Funds from the Pool

Now that all WETH (1010 WETH) are consolidated within the NaiveReceiverPool and attributed to the deployer's deposits, we need to withdraw them to the recovery account. This is where the BasicForwarder and the _msgSender() override come into play.

  1. Craft the withdraw call: We create the abi.encodeWithSelector for pool.withdraw(1010 WETH, payable(recovery)).
  2. Construct a Meta-Transaction Request: We build a BasicForwarder.Request struct:
    • from: The deployer's address (because the deployer is the one with the 1010 WETH deposit).
    • target: The NaiveReceiverPool address.
    • value: 0 (no ETH sent with the forwarder call itself).
    • gas: Sufficient gas.
    • nonce: The current nonce of the deployer for the BasicForwarder (to prevent replay attacks).
    • data: Our crafted withdraw call.
    • deadline: A future timestamp.
  3. Sign the Request: We compute the EIP-712 typed data hash of this request and sign it using the deployer's private key (which is available to us in the CTF environment).
  4. Execute via Forwarder: Finally, we call forwarder.execute() with the crafted request and the deployer's signature.

Because msg.sender to the NaiveReceiverPool will be the BasicForwarder, the _msgSender() override will correctly interpret the deployer as the caller of withdraw. This allows the 1010 WETH to be withdrawn from the pool and sent directly to the recovery account, achieving our objective.

Conclusion and Key Takeaways

The "Naive Receiver" challenge beautifully illustrates several critical concepts in smart contract security:

  • Fixed Fees & Empty Logic: Contracts that pay fixed fees without a mechanism to generate equivalent value or perform a useful action are susceptible to being drained by repeated, low-value calls.
  • Flash Loan Receiver Design: Flash loan receivers must be meticulously designed. They should either perform economically sound actions with the borrowed funds or have safeguards against repeated, unproductive calls that drain their own capital.
  • Meta-Transactions and _msgSender(): Understanding how meta-transactions interact with _msgSender() overrides is crucial. Attackers can leverage these mechanisms to impersonate privileged addresses if they can obtain or forge signatures.
  • Consolidating Funds: A common attack pattern in CTFs and real-world exploits involves consolidating funds from various sources into a single, exploitable contract before performing a final withdrawal.

By combining the "naive" fee mechanism with the power of meta-transactions, the "Naive Receiver" challenge provides a brilliant lesson in the subtle dangers lurking in seemingly simple DeFi protocols.

Built with AiAda