Damn Vulnerable DeFi - Unstoppable

The contract

The goal is to stop the flash loan functionality. The vault has a function called "flashLoan" that will revert in four different scenarios. Among these, the one that we can control that will make the function fail for all users is the third one, that checks whether "totalSupply" is different than "balanceBefore".
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();

"totalSupply" represents the number of existing tokens: if we analyze ERC20.sol, we can see it is increased when tokens are minted, and decreased when tokens are burnt.

    function _mint(address to, uint256 amount) internal virtual {
        totalSupply += amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) internal virtual {
        balanceOf[from] -= amount;

        // Cannot underflow because a user's balance
        // will never be larger than the total supply.
        unchecked {
            totalSupply -= amount;
        }

        emit Transfer(from, address(0), amount);

"balanceBefore" keeps track of the number of tokens owned by an address, instead. Analyzing ERC20.sol, we can see it is updated when using transfer, transferFrom, mint and burn.

    function transfer(address to, uint256 amount) public virtual returns (bool) {
        balanceOf[msg.sender] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(msg.sender, to, amount);

        return true;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual returns (bool) {
        uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.

        if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;

        balanceOf[from] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(from, to, amount);

        return true;
    }

The solution

We can see that functions "transfer" and "transferFrom" will modify the value of "balanceBefore" without modifying "totalSupply". This will create an imbalance that will cause the revert, DoS-ing the flash loan functionality.
We can do that in ethers by adding the following code to the challenge solution
await token.connect(player).transfer(vault.address, 1);
This will call the transfer function. After the execution, totalSupply will be different than transferFrom, meaning the flash loan function will always revert, making it impossible to anyone to loan and use the contract.

Merry hacking ;)


You'll only receive email when they publish something new.

More from emacab98
All posts