HIGH severity: Incorrect Quote Calculation. The oracle system in sBOLD had a flaw in how it calculated quotes for collateral valuation. The cause was an incorrect scaling or precision handling in the getQuote() method of the ChainlinkLstOracle and PythOracle contracts. This led to inaccurate USD-to-BOLD conversions, which affected both swap s...

Vulnerable code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {AggregatorV3Interface} from "../../external/AggregatorV3Interface.sol";
import {BaseChainlinkOracle} from "./BaseChainlinkOracle.sol";
import {Decimals} from "../../libraries/helpers/Decimals.sol";
import {Common} from "../../libraries/Common.sol";
import {Constants} from "../../libraries/helpers/Constants.sol";

/// @title ChainlinkLstOracle
/// @notice PriceOracle adapter for Chainlink push-based price feeds, specific for liquid-staking tokens.
contract ChainlinkLstOracle is BaseChainlinkOracle {
    /// @notice The max staleness mapped to feed address.
    mapping(address => uint256) public feedToMaxStaleness;
    /// @notice Name of the oracle.
    string public constant name = "Chainlink LST sBold V1";
    /// @notice The address of the ETH to USD.
    Feed public ethUsdFeed;
    /// @notice The address of the LST to ETH feed.
    Feed public lstEthFeed;

    /// @notice Deploys a Chainlink price oracle for liquid-staking tokens.
    /// @param _base The address of the base asset.
    /// @param _ethUsdFeed The structure for the ETH to USD feed.
    /// @param _lstEthFeed The structure for the LST to ETH feed.
    constructor(address _base, Feed memory _ethUsdFeed, Feed memory _lstEthFeed) {
        Common.revertZeroAddress(_base);
        Common.revertZeroAddress(_ethUsdFeed.addr);
        Common.revertZeroAddress(_lstEthFeed.addr);

        if (
            _ethUsdFeed.maxStaleness < MAX_STALENESS_LOWER_BOUND || _ethUsdFeed.maxStaleness > MAX_STALENESS_UPPER_BOUND
        ) {
            revert InvalidMaxStaleness();
        }

        if (
            _lstEthFeed.maxStaleness < MAX_STALENESS_LOWER_BOUND || _lstEthFeed.maxStaleness > MAX_STALENESS_UPPER_BOUND
        ) {
            revert InvalidMaxStaleness();
        }

        base = _base;
        ethUsdFeed = _ethUsdFeed;
        lstEthFeed = _lstEthFeed;
    }

    /// @inheritdoc BaseChainlinkOracle
    function getQuote(uint256 inAmount, address) external view override returns (uint256) {
        uint256 lstUsdPrice = _fetchPrice();

        return (inAmount * lstUsdPrice) / 10 ** Decimals.getDecimals(base);
    }

    /// @inheritdoc BaseChainlinkOracle
    function isBaseSupported(address _base) external view override returns (bool) {
        return base == _base;
    }

    /// @notice Extracts price for ETH to USD and LST to ETH feeds.
    /// @dev The returned price is the denomination of the LST to USD.
    function _fetchPrice() internal view returns (uint256) {
        uint256 ethInUsd = _getLatestAnswer(ethUsdFeed);
        uint256 lstInEth = _getLatestAnswer(lstEthFeed);

        // Calculate the price for LST to USD
        return (ethInUsd * lstInEth) / 10 ** Constants.ORACLE_PRICE_PRECISION;
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IPyth} from "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
import {PythStructs} from "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
import {IPriceOracle} from "../../interfaces/IPriceOracle.sol";
import {Decimals} from "../../libraries/helpers/Decimals.sol";
import {Common} from "../../libraries/Common.sol";
import {Constants} from "../../libraries/helpers/Constants.sol";

/// @title PythOracle
/// @notice PriceOracle adapter for Pyth pull-based price feeds.
contract PythOracle is IPriceOracle, Ownable {
    /// @notice The maximum length of time that a price can be in the future.
    uint256 internal constant MAX_AHEADNESS = 1 minutes;
    /// @notice The maximum permitted value for `maxStaleness`.
    uint256 internal constant MAX_STALENESS_UPPER_BOUND = 15 minutes;
    /// @notice The minimum permitted value for `maxConfWidth`.
    /// @dev Equal to 0.1%.
    uint256 internal constant MAX_CONF_WIDTH_LOWER_BOUND = 10;
    /// @notice The maximum permitted value for `maxConfWidth`.
    /// @dev Equal to 5%.
    uint256 internal constant MAX_CONF_WIDTH_UPPER_BOUND = 500;
    /// @dev The smallest PythStruct exponent that the oracle can handle.
    int256 internal constant MIN_EXPONENT = -20;
    /// @dev The largest PythStruct exponent that the oracle can handle.
    int256 internal constant MAX_EXPONENT = 12;
    /// @notice Name of the oracle.
    string public constant name = "Pyth sBold V1";
    /// @notice The address of the Pyth oracle proxy.
    address public immutable pyth;
    /// @notice The address of the asset to convert from.
    address public immutable base;
    /// @notice The feedId of the base asset. The id of the feed in the Pyth network for the base asset address.
    /// @dev See https://pyth.network/developers/price-feed-ids.
    bytes32 public immutable feedId;
    /// @notice The decimals of the base asset.
    uint8 public immutable baseDecimals;
    /// @notice The max staleness of the feed id.
    uint256 public immutable maxStaleness;
    /// @notice The maximum allowed width of the confidence interval.
    uint256 public immutable maxConfWidth;

    /// @notice Deploys a PythOracle.
    /// @param _pyth The address of the Pyth oracle proxy.
    /// @param _base The base asset address.
    /// @param _feedId The id of the feed.
    /// @param _maxStaleness The maximum price staleness.
    /// @param _maxConfWidth The maximum width of the confidence interval in basis points.
    constructor(
        address _pyth,
        address _base,
        bytes32 _feedId,
        uint256 _maxStaleness,
        uint256 _maxConfWidth
    ) Ownable(_msgSender()) {
        Common.revertZeroAddress(_pyth);
        Common.revertZeroAddress(_base);

        if (_maxStaleness > MAX_STALENESS_UPPER_BOUND) {
            revert InvalidMaxStalenessUpperBound();
        }

        if (_maxConfWidth < MAX_CONF_WIDTH_LOWER_BOUND || _maxConfWidth > MAX_CONF_WIDTH_UPPER_BOUND) {
            revert InvalidMaxConfWidthLowerBound();
        }

        if (_feedId == bytes32(0)) {
            revert InvalidFeed();
        }

        pyth = _pyth;
        base = _base;
        maxConfWidth = _maxConfWidth;
        baseDecimals = Decimals.getDecimals(_base);
        feedId = _feedId;
        maxStaleness = _maxStaleness;
    }

    /// @inheritdoc IPriceOracle
    function getQuote(uint256 inAmount, address) external view override returns (uint256) {
        // Check if asset is configured
        if (baseDecimals == 0) revert InvalidBaseDecimals();
        // Fetch Pyth price struct and validated output
        PythStructs.Price memory priceStruct = _fetchPriceStruct(feedId);

        uint256 price = uint256(uint64(priceStruct.price));

        uint256 scale;
        if (priceStruct.expo < 0) {
            // scale down if exponent < 0
            scale = (price * 10 ** Constants.ORACLE_PRICE_PRECISION) / (10 ** uint8(int8(-priceStruct.expo)));
        } else {
            // scale up if exponent >= 0
            scale = (price * 10 ** Constants.ORACLE_PRICE_PRECISION) * (10 ** uint8(int8(priceStruct.expo)));
        }
        // Calculate amount out scaled to `ORACLE_PRICE_PRECISION`
        return (scale * inAmount) / 10 ** baseDecimals;
    }

    /// @inheritdoc IPriceOracle
    function isBaseSupported(address _base) external view returns (bool) {
        return base == _base;
    }

    /// @notice Get the latest Pyth price and perform sanity checks.
    /// @dev Revert conditions: update timestamp is too stale or too ahead, price is negative or zero,
    /// confidence interval is too wide, exponent is too large or too small.
    /// @return The Pyth price struct without modification.
    function _fetchPriceStruct(bytes32 _feedId) internal view returns (PythStructs.Price memory) {
        PythStructs.Price memory p = IPyth(pyth).getPriceUnsafe(_feedId);

        if (p.publishTime < block.timestamp) {
            // Verify that the price is not too stale
            uint256 staleness = block.timestamp - p.publishTime;
            if (staleness > maxStaleness) revert TooStalePrice();
        } else {
            // Verify that the price is not too ahead
            uint256 aheadness = p.publishTime - block.timestamp;
            if (aheadness > MAX_AHEADNESS) revert TooAheadPrice();
        }

        // Verify that the price is positive and within the confidence width.
        if (p.price <= 0 || p.conf > (uint64(p.price) * maxConfWidth) / Constants.BPS_DENOMINATOR) {
            revert InvalidPrice();
        }

        // Verify that the price exponent is within bounds.
        if (p.expo < MIN_EXPONENT || p.expo > MAX_EXPONENT) {
            revert InvalidPriceExponent();
        }

        return p;
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {ISBold} from "../../interfaces/ISBold.sol";
import {IPriceOracle} from "../../interfaces/IPriceOracle.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Constants} from "../helpers/Constants.sol";

/// @title QuoteLogic
/// @notice Logic for quotes derivation.
library QuoteLogic {
    using Math for uint256;

    /// @param oracle The oracle to use for getting the quote.
    /// @param balances The collateral balance structs.
    /// @param _revert If the getQuote should revert or not.
    /// @return success The success result of this function.
    /// @return amount The aggregated amount of collateral in USD.
    function getAggregatedQuote(
        IPriceOracle oracle,
        ISBold.CollBalance[] memory balances,
        bool _revert
    ) internal view returns (bool success, uint256 amount) {
        for (uint256 i = 0; i < balances.length; i++) {
            if (balances[i].balance == 0) continue;

            try oracle.getQuote(balances[i].balance, balances[i].addr) returns (uint256 amount_) {
                amount += amount_;
            } catch (bytes memory data) {
                if (_revert) revert(string(data));

                return (false, amount);
            }
        }

        return (true, amount);
    }

    /// @param oracle The oracle to use for getting the quote.
    /// @param bold The address of $BOLD.
    /// @param coll The address of the collateral.
    /// @param balance The balance to return quote for.
    /// @return amount The quote amount of collateral in $BOLD.
    function getInBoldQuote(
        IPriceOracle oracle,
        address bold,
        address coll,
        uint256 balance,
        uint8 decimals
    ) internal view returns (uint256) {
        // Get collateral value in USD
        uint256 collQuote = oracle.getQuote(balance, coll);
        // Get $BOLD value in USD
        uint256 boldUnitQuote = oracle.getQuote(10 ** decimals, bold);
        // Calculate 1 $BOLD * `n` collateral
        return collQuote.mulDiv(10 ** Constants.ORACLE_PRICE_PRECISION, boldUnitQuote);
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {BaseSBold} from "./base/BaseSBold.sol";
import {SpLogic} from "./libraries/logic/SpLogic.sol";
import {SwapLogic} from "./libraries/logic/SwapLogic.sol";
import {QuoteLogic} from "./libraries/logic/QuoteLogic.sol";
import {Constants} from "./libraries/helpers/Constants.sol";
import {Decimals} from "./libraries/helpers/Decimals.sol";
import {TransientStorage} from "./libraries/helpers/TransientStorage.sol";

/// @title sBold Protocol
/// @notice The $BOLD ERC4626 yield-bearing token.
contract sBold is BaseSBold {
    using Math for uint256;

    /// @notice Deploys sBold.
    /// @param _asset The address of the $BOLD instance.
    /// @param _name The name of `this` contract.
    /// @param _symbol The symbol of `this` contract.
    /// @param _sps The Stability Pools memory array.
    /// @param _priceOracle The address of the price oracle adapter.
    /// @param _vault The address of the vault for fee transfers.
    constructor(
        address _asset,
        string memory _name,
        string memory _symbol,
        SPConfig[] memory _sps,
        address _priceOracle,
        address _vault
    ) ERC4626(ERC20(_asset)) ERC20(_name, _symbol) BaseSBold(_sps, _priceOracle, _vault) {
        super.deposit(10 ** decimals(), address(this));
    }

    /*//////////////////////////////////////////////////////////////
                                 LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Deposits $BOLD in SP and mints corresponding $sBOLD.
    /// @param assets The amount of assets to deposit + fee to collect.
    /// @param receiver The address to mint the shares to.
    /// @return The amount of shares.
    function deposit(
        uint256 assets,
        address receiver
    ) public override whenNotPaused nonReentrant execCollateralOps returns (uint256) {
        uint256 maxAssets = _maxDeposit(receiver);
        if (assets > maxAssets) {
            revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);
        }

        uint256 fee = _feeOnTotal(assets, feeBps);

        uint256 shares = super.previewDeposit(assets - fee);

        _deposit(_msgSender(), receiver, assets, shares);

        if (fee > 0) SafeERC20.safeTransfer(IERC20(asset()), vault, fee);

        SpLogic.provideToSP(sps, assets - fee);

        return shares;
    }

    /// @notice Mints shares of $sBOLD and provides corresponding $BOLD to SP.
    /// @param shares The amount of shares to mint.
    /// @param receiver The address to send the shares to.
    /// @return The amount of assets.
    function mint(
        uint256 shares,
        address receiver
    ) public override whenNotPaused nonReentrant execCollateralOps returns (uint256) {
        uint256 maxShares = _maxMint(receiver);
        if (shares > maxShares) {
            revert ERC4626ExceededMaxMint(receiver, shares, maxShares);
        }

        uint256 assets = super.previewMint(shares);

        uint256 fee = _feeOnRaw(assets, feeBps);

        _deposit(_msgSender(), receiver, assets + fee, shares);

        if (fee > 0) SafeERC20.safeTransfer(IERC20(asset()), vault, fee);

        SpLogic.provideToSP(sps, assets);

        return assets + fee;
    }

    /// @notice Redeems shares of $sBOLD in $BOLD and burns $sBOLD.
    /// @param shares The amount of shares to redeem.
    /// @param receiver The address to send the assets to.
    /// @param owner The owner of the shares.
    /// @return The amount of assets.
    function redeem(
        uint256 shares,
        address receiver,
        address owner
    ) public override whenNotPaused nonReentrant execCollateralOps returns (uint256) {
        uint256 maxShares = _maxRedeem(owner);
        if (shares > maxShares) {
            revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
        }

        uint256 assets = super.previewRedeem(shares);

        SpLogic.withdrawFromSP(sps, IERC20(asset()), decimals(), assets, true);

        _withdraw(_msgSender(), receiver, owner, assets, shares);

        return assets;
    }

    /// @notice Withdraws assets $BOLD from the SP and burns $sBOLD.
    /// @param assets The amount of assets to withdraw.
    /// @param receiver The address to send the shares to.
    /// @param owner The owner of the shares.
    /// @return The amount of shares.
    function withdraw(
        uint256 assets,
        address receiver,
        address owner
    ) public override whenNotPaused nonReentrant execCollateralOps returns (uint256) {
        uint256 maxAssets = _maxWithdraw(owner);
        if (assets > maxAssets) {
            revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
        }

        uint256 shares = super.previewWithdraw(assets);

        SpLogic.withdrawFromSP(sps, IERC20(asset()), decimals(), assets, true);

        _withdraw(_msgSender(), receiver, owner, assets, shares);

        return shares;
    }

    /// @notice Swaps collateral balances to $BOLD.
    /// @param swapData The swap data.
    /// @param receiver The reward receiver.
    function swap(SwapData[] memory swapData, address receiver) public whenNotPaused nonReentrant {
        address bold = asset();

        // Prepare swap data and claim collateral.
        SwapDataWithColl[] memory swapDataWithColl = SwapLogic.prepareSwap(bold, priceOracle, sps, swapData);
        // Execute swaps for each collateral to $BOLD
        uint256 assets = SwapLogic.swap(bold, swapAdapter, swapDataWithColl, maxSlippage);

        (, uint256 swapFee, uint256 reward) = SwapLogic.applyFees(assets, swapFeeBps, rewardBps);

        IERC20 iBold = IERC20(bold);

        if (swapFee > 0) SafeERC20.safeTransfer(iBold, vault, swapFee);
        if (reward > 0) SafeERC20.safeTransfer(iBold, receiver, reward);

        uint256 assetsInternal = ERC20(bold).balanceOf(address(this));
        uint256 deadShareAmount = 10 ** decimals();

        if (assetsInternal > deadShareAmount) {
            SpLogic.provideToSP(sps, assetsInternal - deadShareAmount);
        }
    }

    /// @notice This function is able to both re-balance in terms of weights and change entirely current SPs.
    /// @param _sps The Stability Pools memory array.
    /// @param _swapData The swap data.
    function rebalanceSPs(SPConfig[] calldata _sps, SwapData[] memory _swapData) external onlyOwner nonReentrant {
        address bold = asset();

        // Prepare swap data and claim collateral.
        SwapDataWithColl[] memory swapDataWithColl = SwapLogic.prepareSwap(bold, priceOracle, sps, _swapData);
        // Execute swaps for each collateral to $BOLD.
        SwapLogic.swap(bold, swapAdapter, swapDataWithColl, maxSlippage);

        _checkCollHealth(true);

        uint256 boldAmount;
        for (uint256 i = 0; i < sps.length; i++) {
            // Add $BOLD compounded deposits from each SP
            boldAmount += SpLogic._getBoldAssetsSP(sps[i].sp);
        }

        // Withdraw all assets from current SPs.
        SpLogic.withdrawFromSP(sps, IERC20(bold), decimals(), boldAmount, false);

        // Sanitize
        delete sps;
        // Set new SPs.
        _setSPs(_sps);

        uint256 assetsInternal = ERC20(bold).balanceOf(address(this));
        uint256 deadShareAmount = 10 ** decimals();

        if (assetsInternal > deadShareAmount) {
            // Provide all assets to new SPs.
            SpLogic.provideToSP(sps, assetsInternal - deadShareAmount);
        }

        emit Rebalance(_sps);
    }

    /*//////////////////////////////////////////////////////////////
                                MAXIMUMS
    //////////////////////////////////////////////////////////////*/

    /// @dev Max deposit function returning result from `_maxDeposit`. See {IERC4626-maxDeposit} and {sBold-_maxDeposit}.
    function maxDeposit(address account) public view virtual override nonReentrantReadOnly returns (uint256) {
        return _maxDeposit(account);
    }

    /// @dev Max deposit function returning result from `_maxMint`. See {IERC4626-maxMint} and {sBold-_maxMint}.
    function maxMint(address account) public view virtual override nonReentrantReadOnly returns (uint256) {
        return _maxMint(account);
    }

    /// @dev Max withdraw function returning result from `_maxWithdraw`. See {IERC4626-maxWithdraw} and {sBold-maxWithdraw}.
    function maxWithdraw(address owner) public view virtual override nonReentrantReadOnly returns (uint256) {
        return _maxWithdraw(owner);
    }

    /// @dev Max redeem function returning result from `_maxRedeem`. See {IERC4626-maxRedeem} and {sBold-_maxRedeem}.
    function maxRedeem(address owner) public view virtual override nonReentrantReadOnly returns (uint256) {
        return _maxRedeem(owner);
    }

    /*//////////////////////////////////////////////////////////////
                               PREVIEWS
    //////////////////////////////////////////////////////////////*/

    /// @dev Preview deducting an entry fee on deposit. See {IERC4626-previewDeposit}.
    function previewDeposit(uint256 assets) public view virtual override nonReentrantReadOnly returns (uint256) {
        uint256 fee = _feeOnTotal(assets, feeBps);

        return super.previewDeposit(assets - fee);
    }

    /// @dev Preview adding an entry fee on mint. See {IERC4626-previewMint}.
    function previewMint(uint256 shares) public view virtual override nonReentrantReadOnly returns (uint256) {
        uint256 assets = super.previewMint(shares);

        return assets + _feeOnRaw(assets, feeBps);
    }

    /// @dev Preview withdraw add readOnly reentrancy check. See {IERC4626-previewWithdraw}.
    function previewWithdraw(uint256 assets) public view virtual override nonReentrantReadOnly returns (uint256) {
        return super.previewWithdraw(assets);
    }

    /// @dev Preview redeem add readOnly reentrancy check. See {IERC4626-previewRedeem}.
    function previewRedeem(uint256 shares) public view virtual override nonReentrantReadOnly returns (uint256) {
        return super.previewRedeem(shares);
    }

    /** @dev See {IERC4626-convertToShares}. */
    function convertToShares(uint256 assets) public view virtual override nonReentrantReadOnly returns (uint256) {
        return _convertToShares(assets, Math.Rounding.Floor);
    }

    /** @dev See {IERC4626-convertToAssets}. */
    function convertToAssets(uint256 shares) public view virtual override nonReentrantReadOnly returns (uint256) {
        return _convertToAssets(shares, Math.Rounding.Floor);
    }

    /// @dev Total underlying assets owned by the sBOLD contract which are utilized in stability pools.
    function totalAssets() public view virtual override nonReentrantReadOnly returns (uint256) {
        (uint256 totalBold, , , ) = _calcFragments();

        return totalBold;
    }

    /*//////////////////////////////////////////////////////////////
                                TRANSIENT
    //////////////////////////////////////////////////////////////*/

    /// @inheritdoc BaseSBold
    function _checkAndStoreCollValueInBold() internal virtual override {
        (, uint256 collValue, uint256 collInBold) = _checkCollHealth(true);
        // Transient store for collaterals and flag
        TransientStorage.storeCollValues(collValue, collInBold);
    }

    /*//////////////////////////////////////////////////////////////
                               GETTERS
    //////////////////////////////////////////////////////////////*/

    /// @notice Calculates the $sBOLD:BOLD rate.
    /// @return The $sBOLD:$BOLD rate.
    function getSBoldRate() public view nonReentrantReadOnly returns (uint256) {
        (uint256 totalBold, , , ) = _calcFragments();

        return (totalBold + 1).mulDiv(10 ** decimals(), totalSupply() + 10 ** _decimalsOffset());
    }

    function calcFragments() public view nonReentrantReadOnly returns (uint256, uint256, uint256, uint256) {
        return _calcFragments();
    }

    /// @dev Max deposit returns 0 if collateral is above max, the contract is paused or call to oracle has failed. See {IERC4626-maxDeposit}.
    function _maxDeposit(address account) private view returns (uint256) {
        (bool success, , ) = _checkCollHealth(false);

        if (!success || paused()) return 0;

        return super.maxDeposit(account);
    }

    /// @dev Max mint returns 0 if collateral is above max, the contract is paused or call to oracle has failed. See {IERC4626-maxMint}.
    function _maxMint(address account) private view returns (uint256) {
        (bool success, , ) = _checkCollHealth(false);

        if (!success || paused()) return 0;

        return super.maxMint(account);
    }

    /// @dev Max withdraw returns 0 if collateral is above max. See {IERC4626-maxWithdraw}.
    /// note: Returns an amount up to the one available in $BOLD.
    function _maxWithdraw(address owner) private view returns (uint256) {
        (bool success, , ) = _checkCollHealth(false);

        if (!success || paused()) return 0;

        uint256 maxWithdrawAssets = super.maxWithdraw(owner);

        uint256 boldAmount = SpLogic.getBoldAssets(sps, IERC20(asset()));

        if (maxWithdrawAssets > boldAmount) {
            uint256 deadShareAmount = 10 ** decimals();

            if (boldAmount < deadShareAmount) return 0;

            return boldAmount - deadShareAmount;
        }

        return maxWithdrawAssets;
    }

    /// @dev Max redeem returns 0 if collateral is above max. See {IERC4626-maxRedeem}.
    /// note: Returns an amount up to the one available in $BOLD, converted in shares.
    function _maxRedeem(address owner) private view returns (uint256) {
        (bool success, , ) = _checkCollHealth(false);

        if (!success || paused()) return 0;

        uint256 maxWithdrawAssets = super.maxWithdraw(owner);

        uint256 boldAmount = SpLogic.getBoldAssets(sps, IERC20(asset()));

        if (maxWithdrawAssets > boldAmount) {
            uint256 deadShareAmount = 10 ** decimals();

            if (boldAmount < deadShareAmount) return 0;

            return _convertToShares(boldAmount - deadShareAmount, Math.Rounding.Floor);
        }

        return super.maxRedeem(owner);
    }

    /// @notice Calculates the total value in $BOLD of the assets in the contract.
    /// @return The total value in USD, $BOLD amount and collateral in USD.
    function _calcFragments() private view returns (uint256, uint256, uint256, uint256) {
        address bold = asset();
        // Get compounded $BOLD amount
        uint256 boldAmount = SpLogic.getBoldAssets(sps, IERC20(bold));
        // Get collateral value in USD and $BOLD
        (, uint256 collValue, uint256 collInBold) = _calcCollValue(bold, true);
        // Calculate based on the minimum amount to be received after swap
        uint256 collToBoldMinOut = SwapLogic.calcMinOut(collInBold, maxSlippage);
        // Apply fees after swap
        (uint256 collInBoldNet, , ) = SwapLogic.applyFees(collToBoldMinOut, swapFeeBps, rewardBps);
        // Calculate total $BOLD value
        uint256 totalBold = boldAmount + collInBoldNet;

        return (totalBold, boldAmount, collValue, collInBold);
    }

    /// @notice Converts the $BOLD assets to shares based on $sBOLD exchange rate.
    /// @return The calculated $sBOLD share, based on the total value held.
    function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) {
        return
            assets.mulDiv(
                10 ** decimals(),
                _getSBoldRateWithRounding(rounding == Math.Rounding.Floor ? Math.Rounding.Ceil : Math.Rounding.Floor),
                rounding
            );
    }

    /// @notice Converts the $sBOLD shares to $BOLD assets based on $sBOLD exchange rate.
    /// @return The calculated $BOLD assets, based on the total value held.
    function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
        return shares.mulDiv(_getSBoldRateWithRounding(rounding), 10 ** decimals(), rounding);
    }

    /// @notice Calculates the $sBOLD:BOLD rate with input rounding.
    /// @param rounding Type of rounding on math calculations.
    /// @return The $sBOLD:$BOLD rate.
    function _getSBoldRateWithRounding(Math.Rounding rounding) private view returns (uint256) {
        (uint256 totalBold, , , ) = _calcFragments();

        return (totalBold + 1).mulDiv(10 ** decimals(), totalSupply() + 10 ** _decimalsOffset(), rounding);
    }

    /// @dev Calculates the fees that should be added to an amount `assets` that does not already include fees.
    /// Used in {IERC4626-mint} operations.
    function _feeOnRaw(uint256 assets, uint256 feeBasisPoints) internal pure returns (uint256) {
        return assets.mulDiv(feeBasisPoints, Constants.BPS_DENOMINATOR, Math.Rounding.Ceil);
    }

    /// @dev Calculates the fee part of an amount `assets` that already includes fees.
    /// Used in {IERC4626-deposit} operations.
    function _feeOnTotal(uint256 assets, uint256 feeBasisPoints) internal pure returns (uint256) {
        return assets.mulDiv(feeBasisPoints, feeBasisPoints + Constants.BPS_DENOMINATOR, Math.Rounding.Ceil);
    }

    /// @notice Calculates the collateral value in USD and $BOLD from all SPs.
    /// @param _bold Asset contract address.
    /// @param _revert Indication to revert on errors.
    /// @return success Success result from function.
    /// @return collValue The total collateral value.
    /// @return collInBold The collateral value denominated in $BOLD.
    function _calcCollValue(
        address _bold,
        bool _revert
    ) private view returns (bool success, uint256 collValue, uint256 collInBold) {
        // Return values from transient storage
        if (TransientStorage.loadCollsFlag())
            return (true, TransientStorage.loadCollValue(), TransientStorage.loadCollInBold());

        CollBalance[] memory collBalances = SpLogic.getCollBalances(sps, false);

        (success, collValue) = QuoteLogic.getAggregatedQuote(priceOracle, collBalances, _revert);

        if (success)
            try priceOracle.getQuote(10 ** decimals(), _bold) returns (uint256 boldUnitQuote) {
                collInBold = collValue.mulDiv(10 ** Constants.ORACLE_PRICE_PRECISION, boldUnitQuote);
            } catch (bytes memory data) {
                if (_revert) revert(string(data));

                return (false, collValue, 0);
            }
    }

    /*//////////////////////////////////////////////////////////////
                               VALIDATIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Checks if the collateral value in $BOLD is over the maximum allowed.
    function _checkCollHealth(bool _revert) private view returns (bool, uint256, uint256) {
        (bool success, uint256 collValue, uint256 collValueInBold) = _calcCollValue(asset(), _revert);

        if (!success) return (false, 0, 0);

        if (collValueInBold <= maxCollInBold) return (true, collValue, collValueInBold);

        if (_revert) revert CollOverLimit();

        return (false, collValue, collValueInBold);
    }
}


Vulnerability type: CWE-682