Appearance
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:
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
BasicForwarderfor meta-transactions and overrides the_msgSender()function: ifmsg.senderis thetrustedForwarder, the actual sender is extracted from the end ofmsg.data. - The
deployeris also designated as thefeeReceiver.
FlashLoanReceiver.sol:- A sample contract deployed by a "user," holding 10 WETH.
- It implements the
IERC3156FlashBorrowerinterface, meaning it can receive flash loans. - Its
onFlashLoanfunction includes a critical check, ensuring only theNaiveReceiverPoolcan call it directly. - Most importantly, its
_executeActionDuringFlashLoan()is empty. It performs no useful work with the borrowed funds.
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
fromaddress 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:
- The
NaiveReceiverPoolsends the requested amount of WETH to theFlashLoanReceiver. - The
NaiveReceiverPoolthen callsFlashLoanReceiver.onFlashLoan(). - Inside
onFlashLoan(), theFlashLoanReceiver's_executeActionDuringFlashLoan()is called, which, as noted, does nothing. - The
FlashLoanReceiverthen approves and repays the borrowed amount + 1 WETH fixed fee back to theNaiveReceiverPool.
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: TheFlashLoanReceivercontract.token: WETH.amount: 0 (since theFlashLoanReceiverdoesn'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.
- Craft the
withdrawcall: We create theabi.encodeWithSelectorforpool.withdraw(1010 WETH, payable(recovery)). - Construct a Meta-Transaction Request: We build a
BasicForwarder.Requeststruct:from: Thedeployer's address (because thedeployeris the one with the1010 WETHdeposit).target: TheNaiveReceiverPooladdress.value: 0 (no ETH sent with the forwarder call itself).gas: Sufficient gas.nonce: The current nonce of thedeployerfor theBasicForwarder(to prevent replay attacks).data: Our craftedwithdrawcall.deadline: A future timestamp.
- 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). - Execute via Forwarder: Finally, we call
forwarder.execute()with the crafted request and thedeployer'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.