#46982 [SC-Insight] Spread calculation discrepancy allows wildly divergent prices to be accepted

Submitted on Jun 7th 2025 at 08:38:29 UTC by @dawn for Audit Comp | Flare | FAssets

  • Report ID: #46982

  • Report Type: Smart Contract

  • Report severity: Insight

  • Target: https://github.com/flare-foundation/fassets/blob/main/contracts/assetManager/implementation/FtsoV2PriceStore.sol

  • Impacts:

    • Protocol insolvency

Description

Brief/Intro

A significant flaw exists in the _calculateMedian function within FtsoV2PriceStore.sol that compromises its ability to accurately assess price stability. The issue stems from an inconsistent calculation of the spread (how dispersed prices are) depending on whether there's an odd or even number of price inputs. When there's an even number of prices, the system only considers the two middle values to determine the spread, effectively ignoring extreme price fluctuations. This allows a bypass of the crucial spread check, potentially leading to a misleading median price that doesn't reflect the true market value, especially during volatile periods. This inaccurate price can then be exploited, either by manipulation or error, to artificially inflate or deflate collateral ratios for agents. This could prevent necessary liquidations of undercollateralized agents, risking F-Asset holders, or trigger unwarranted liquidations.

Vulnerability Details

Below is a snippet of the median and spread calculation of the _calculateMedian function in FtsoV2PriceStore.sol:

   function _calculateMedian(bytes memory _prices) internal view returns (uint256 _medianPrice, bool _priceOk) {
        //...
        uint256 spread = 0;
        uint256 middleIndex = length / 2;
        if (length % 2 == 1) {
            _medianPrice = prices[middleIndex];
            if (length >= 3) {
                spread = prices[middleIndex + 1] - prices[middleIndex - 1];
            }
        } else {
            // if median is "in the middle", take the average price of the two consecutive prices
            _medianPrice = (prices[middleIndex - 1] + prices[middleIndex]) / 2;
            spread = prices[middleIndex] - prices[middleIndex - 1];
        }
        // check if spread is within the limit
        _priceOk = spread <= maxSpreadBIPS * _medianPrice / MAX_BIPS; // no overflow
    }

For odd length it sets the _medianPrice to the middle element (prices[middleIndex]) and calculate the spread as the difference between the price just above and below the median (prices[middleIndex + 1] - prices[middleIndex - 1]);

For even length it calculates the _medianPrice as the average of the two middle prices ((prices[middleIndex - 1] + prices[middleIndex]) / 2) and calculate the spread as their difference (prices[middleIndex] - prices[middleIndex - 1]);

This inconsistency allows the check to be bypassed. The even-length case is significantly weaker, as it completely ignores the values at the extremes of the dataset. For even lengths, _priceOk may result to true when prices are widely dispersed, as long as the two middle prices are close, undermining the spread check’s purpose.

Impact Details

The result of the _calculateMedian function could be based on a median of widely divergent prices. This median itself might not be representative of the true market price during high market volatility, or it could be easily manipulated if a few trusted providers collude or submit erroneous data. This result is used in the publishPrices function, it will be stored on the priceStore.trustedValue variable. This variable is accessed by the _getPriceFromTrustedProviders function which is relied heavily by an AssetManager for accurate price feeds (both FTSO scaling and trusted prices) to calculate agent collateral ratios (CR). The function Liquidation.getCollateralRatioBIPS uses the maximum of the FTSO-derived CR and the trusted-price-derived CR. This could make an agent's collateral appear more valuable than it is, or the F-Assets less valuable, their calculated CR could be artificially inflated. This might prevent necessary liquidations or CCB triggers, allowing undercollateralized agents to continue operating, putting F-Asset holders at risk. Or vice versa which makes collateral seem less valuable or F-Assets more valuable, an agent's CR could be artificially deflated, this could trigger unwarranted liquidations.

Proof of Concept

For brevity, the variable initializations and sort logic step by step calculations are skipped.

_prices: [0, 100, 101, 1000]

length: 4

maxSpreadBIPS: 100 (1%)

Median and spread calculation:

        uint256 spread = 0;
        uint256 middleIndex = length / 2;
        if (length % 2 == 1) {
            _medianPrice = prices[middleIndex];
            if (length >= 3) {
                spread = prices[middleIndex + 1] - prices[middleIndex - 1];
            }
        } else {
            // if median is "in the middle", take the average price of the two consecutive prices
            _medianPrice = (prices[middleIndex - 1] + prices[middleIndex]) / 2;
            spread = prices[middleIndex] - prices[middleIndex - 1];
        }

middleIndex = length / 2 = 4 / 2 = 2

length is 4 so the else block is entered:

_medianPrice = (prices[middleIndex - 1] + prices[middleIndex]) / 2

  • _medianPrice = (100 + 101) / 2 = 201 / 2 = 100 (integer division)

spread = prices[middleIndex] - prices[middleIndex - 1]

  • spread = 101 - 100 = 1

_priceOk = spread <= maxSpreadBIPS * _medianPrice / MAX_BIPS;

  • _priceOk = 1 <= 100 * 100 / 10000;

  • _priceOk = 1 <= 10000 / 10000;

  • _priceOk = 1 <= 1;

  • _priceOk = true;

Final Output:

_medianPrice = 100

_priceOk = true

The spread is only 1 which is very small and barely pass the _priceOk check, while actually the prices contain 0 and 1000 and the range of the dataset is 1000 which is extremely large compared to the median of 100.

Was this helpful?