Security Audit Series (1): Research on Flash Loan Security Risks in Solidity

·

Flash loans have become a cornerstone of decentralized finance (DeFi), enabling users to borrow large amounts of assets without collateral—provided the loan is repaid within the same transaction. While this innovation unlocks powerful financial flexibility, it also introduces unique attack vectors, especially when implemented in Ethereum's primary smart contract language, Solidity.

This article dives into a common yet critical security flaw in Solidity-based flash loan implementations: reentrancy vulnerabilities triggered through balance manipulation. We'll examine a simplified but realistic contract design, demonstrate how an attacker can exploit it using a proof-of-concept (PoC), and provide actionable mitigation strategies to secure your DeFi projects.

Our focus is on flash loan safety, reentrancy protection, and secure balance validation—key concerns for developers building robust and resilient DeFi protocols.


Understanding the Flash Loan Mechanism in Solidity

In most Solidity flash loan designs, the core repayment check relies on comparing the contract’s ETH or token balance before and after the loan. The logic is straightforward:

  1. Record the contract’s initial balance.
  2. Send funds to the borrower.
  3. Execute the borrower’s callback function.
  4. Verify that the final balance is at least equal to the initial balance plus a fee.

At first glance, this method appears secure—it ensures no net loss of funds. However, problems arise when other functions in the same contract modify the balance, such as deposit, withdraw, or stake mechanisms.

👉 Discover how leading platforms prevent reentrancy attacks in real-time DeFi systems.


Case Study: A Vulnerable Flash Loan & Deposit Contract

Consider a simplified contract that combines two functionalities:

The contract issues "LP tokens" (liquidity provider tokens) to users who deposit ETH, allowing them to later withdraw their proportional share of the pool.

Here's a condensed version of the vulnerable code:

contract loan {
    uint256 public totalSupply;
    mapping(address => uint256) public _balance;

    function deposit() public payable returns (uint256) {
        uint256 value = address(this).balance - msg.value;
        uint256 mint_amount;
        if (totalSupply == 0) {
            mint_amount = msg.value;
        } else {
            mint_amount = (msg.value * totalSupply) / value;
        }
        _mint(mint_amount);
        return mint_amount;
    }

    function flash_loan(uint256 amountOut, address to, bytes calldata data) external {
        uint256 value = address(this).balance;
        require(amountOut <= value);

        // Send loan and trigger borrower's function
        payable(to).call{value: amountOut}(data);

        // Check repayment: balance >= initial + 1%
        value = value + value / 100;
        require(address(this).balance >= value);
    }
}

While the flash loan function checks that funds are repaid, it does not isolate external balance changes caused by other functions like deposit().

This opens the door for a devastating reentrancy attack.


Exploiting the Vulnerability: A Proof-of-Concept Attack

An attacker can craft a malicious contract that:

  1. Takes a flash loan close to the full pool balance.
  2. In the callback (back()), deposits part of the borrowed funds back into the same contract.
  3. This deposit increases the contract’s balance, falsely satisfying the repayment check.
  4. After the flash loan completes, the attacker redeems their newly minted LP tokens for real ETH.

How It Works Step-by-Step

  1. Attacker calls start() on their PoC contract.
  2. The contract requests a flash loan of poolBalance - 1 ETH.
  3. The loan contract sends funds and calls back() on the attacker’s contract.
  4. Inside back(), the attacker deposits 102% of the borrowed amount into the loan contract via deposit().

    • This inflates the contract’s balance.
    • The attacker receives LP tokens representing a large share.
  5. The loan contract checks: Is current balance ≥ original + fee?
    ✅ Yes—because of the deposit—but it's not actual repayment.
  6. The flash loan completes successfully.
  7. The attacker calls withdrew() to redeem their LP tokens and drain most of the pool.

Result: The attacker walks away with nearly all ETH from the liquidity pool—without ever repaying the loan in good faith.


Why This Attack Succeeds

The root cause lies in shared state dependency:

This is a classic case where business logic interference undermines security assumptions.

👉 Learn how top-tier DeFi protocols implement secure state isolation and reentrancy guards.


Mitigation Strategies for Secure Flash Loans

To prevent such exploits, developers must decouple balance validation from external manipulations. Here are three proven approaches:

1. Use Reentrancy Guards

Implement a mutex lock to prevent any function from being re-entered during a flash loan.

bool private locked;

modifier nonReentrant() {
    require(!locked, "No reentrancy");
    locked = true;
    _;
    locked = false;
}

function flash_loan(...) public nonReentrant {
    // ...
}

This stops attackers from calling deposit() inside the callback.

2. Maintain an Isolated Balance Ledger

Track "virtual" balances or use accounting snapshots that exclude temporary inflows like deposits during a loan.

For example:

This ensures only genuine repayments count toward validation.

3. Use Safe Token Transfers (for ERC-20 Loans)

When dealing with tokens instead of ETH:


Frequently Asked Questions (FAQ)

Q: Can flash loans be safe without reentrancy locks?
A: Only if all balance-affecting functions are isolated or if repayment is enforced through direct transfers rather than balance comparisons.

Q: Is checking contract balance inherently unsafe for flash loans?
A: Not inherently—but it becomes risky when other functions modify that balance. Always assume malicious actors will manipulate state.

Q: How do real-world protocols like Aave prevent this?
A: They use internal accounting, strict access control, and often separate liquidity management from flash loan logic.

Q: Are ERC-20 flash loans safer than ETH-based ones?
A: Often yes—because token transfers can be enforced via transferFrom, reducing reliance on post-call balance checks.

Q: What tools can detect such vulnerabilities?
A: Static analyzers like Slither, Mythril, and manual review using frameworks like Echidna help catch reentrancy and state interference issues.

👉 Explore advanced auditing techniques used by professional blockchain security teams.


Final Thoughts

Solidity’s flexibility is both its strength and its risk. Flash loans offer incredible utility, but when combined with other financial functions in the same contract, they require meticulous design to avoid catastrophic failures.

The key takeaway: never rely solely on balance checks in contracts with multiple interacting functions. Use reentrancy guards, isolate state changes, and prefer explicit repayment mechanisms over implicit assumptions.

As we continue this series with Move and Rust-based implementations, we’ll see how different languages enforce memory safety and concurrency controls—offering alternative paths to more secure DeFi primitives.

Stay vigilant, audit thoroughly, and build with defense in depth.