Challenge:
Our first challenge is called ‘unstoppable’ and it comes with the following prompt:
There's a lending pool with a million DVT tokens in balance, offering flash loans for free.
If only there was a way to attack and stop the pool from offering flash loans ...
You start with 100 DVT tokens in balance.
Our goal therefore is to stop the pool from being able to offer flash loans and we have up to 100 DVT to play with.
The contracts for our first challenge are located in contracts/unstoppable/. The contract code can be found below:
UnstoppableLender.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
interface IReceiver {
function receiveTokens(address tokenAddress, uint256 amount) external;
}
/**
* @title UnstoppableLender
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract UnstoppableLender is ReentrancyGuard {
IERC20 public immutable damnValuableToken;
uint256 public poolBalance;
constructor(address tokenAddress) {
require(tokenAddress != address(0), "Token address cannot be zero");
damnValuableToken = IERC20(tokenAddress);
}
function depositTokens(uint256 amount) external nonReentrant {
require(amount > 0, "Must deposit at least one token");
// Transfer token from sender. Sender must have first approved them.
damnValuableToken.transferFrom(msg.sender, address(this), amount);
poolBalance = poolBalance + amount;
}
function flashLoan(uint256 borrowAmount) external nonReentrant {
require(borrowAmount > 0, "Must borrow at least one token");
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");
// Ensured by the protocol via the `depositTokens` function
assert(poolBalance == balanceBefore);
damnValuableToken.transfer(msg.sender, borrowAmount);
IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);
uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}
}
ReceiverUnstoppable.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../unstoppable/UnstoppableLender.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title ReceiverUnstoppable
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract ReceiverUnstoppable {
UnstoppableLender private immutable pool;
address private immutable owner;
constructor(address poolAddress) {
pool = UnstoppableLender(poolAddress);
owner = msg.sender;
}
// Pool will call this function during the flash loan
function receiveTokens(address tokenAddress, uint256 amount) external {
require(msg.sender == address(pool), "Sender must be pool");
// Return all tokens to the pool
require(IERC20(tokenAddress).transfer(msg.sender, amount), "Transfer of tokens failed");
}
function executeFlashLoan(uint256 amount) external {
require(msg.sender == owner, "Only owner can execute flash loan");
pool.flashLoan(amount);
}
}
The hints and solutions for this level can be found below:
Hint 1:
One way to prevent any transaction from going through
would be to trigger a revert each time it executes.
Where could this be achieved?
Solution:
If we want to stop the pool from ever performing flash loans again, we have a few options we could explore:
1. Drain the pool, so it does not have any funds to offer for flash loans
2. Ensure this check always fails: `require(balanceBefore >= borrowAmount)`
3. Ensure this check always fails: `assert(poolBalance == balanceBefore)`
4. Ensure this check always fails: `require(balanceAfter >= balanceBefore)`
Each of these boils down to triggering a revert each time the pool attempts to perform a flash loan. Looking through these, we can see that number 1 requires another exploit to drain the contract and number 2 makes a comparison to a user-controllable input that we cannot readily manipulate. The 4th option requires the balance of the contract to differ within the same transaction, which would be difficult to ensure for all transactions.
The 3rd one is interesting, because all we would need to do is get the poolBalance variable out of sync with the true DVT balance of the pool.
This can be done with a force feeding attack.
Ethers Hint:
In order to make transactions on behalf of a user, you can connect to the ERC20 contract.
Ethers Solution:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
await this.token.connect(attacker).transfer(this.pool.address,1);
});