HIGH severity: When users borrow directly from Morpho price of the collateral will not be accurate. The vulnerability arises because the Morpho price oracle prices withdrawal requests based on the requested value when going through the lending router, but when users borrow directly from Morpho, the pricing falls back to the raw share value via super.convertToAssets(shares), which does not account...

Vulnerable code:
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import {DEFAULT_DECIMALS, DEFAULT_PRECISION, YEAR, ADDRESS_REGISTRY} from "./utils/Constants.sol";

import {
    Unauthorized,
    UnauthorizedLendingMarketTransfer,
    InsufficientSharesHeld,
    CannotLiquidate,
    CannotEnterPosition,
    CurrentAccountAlreadySet
} from "./interfaces/Errors.sol";
import {IYieldStrategy} from "./interfaces/IYieldStrategy.sol";
import {IOracle} from "./interfaces/Morpho/IOracle.sol";
import {TokenUtils} from "./utils/TokenUtils.sol";
import {Trade, TradeType, TRADING_MODULE, nProxy, TradeFailed} from "./interfaces/ITradingModule.sol";
import {IWithdrawRequestManager} from "./interfaces/IWithdrawRequestManager.sol";
import {Initializable} from "./proxy/Initializable.sol";
import {ADDRESS_REGISTRY} from "./utils/Constants.sol";
import {ILendingRouter} from "./interfaces/ILendingRouter.sol";

/// @title AbstractYieldStrategy
/// @notice This is the base contract for all yield strategies, it implements the core logic for
/// minting, burning and the valuation of tokens.
abstract contract AbstractYieldStrategy is Initializable, ERC20, ReentrancyGuardTransient, IYieldStrategy {
    using TokenUtils for ERC20;
    using SafeERC20 for ERC20;

    uint256 internal constant VIRTUAL_SHARES = 1e6;
    uint256 internal constant VIRTUAL_YIELD_TOKENS = 1;
    uint256 internal constant SHARE_PRECISION = DEFAULT_PRECISION * VIRTUAL_SHARES;

    /// @inheritdoc IYieldStrategy
    address public immutable override asset;
    /// @inheritdoc IYieldStrategy
    address public immutable override yieldToken;
    /// @inheritdoc IYieldStrategy
    uint256 public immutable override feeRate;

    IWithdrawRequestManager internal immutable withdrawRequestManager;

    uint8 internal immutable _yieldTokenDecimals;
    uint8 internal immutable _assetDecimals;

    /********* Storage Variables *********/
    string private s_name;
    string private s_symbol;

    uint32 private s_lastFeeAccrualTime;
    uint256 private s_accruedFeesInYieldToken;
    uint256 private s_escrowedShares;
    /****** End Storage Variables ******/

    /********* Transient Variables *********/
    // Used to adjust the valuation call of price(), is set on some methods and
    // cleared by the lending router using clearCurrentAccount(). This is required to
    // ensure that the variable is set throughout the entire context of the lending router
    // call.
    address internal transient t_CurrentAccount;
    // Set and cleared on every call to a lending router authorized method
    address internal transient t_CurrentLendingRouter;
    // Used to authorize transfers off of the lending market
    address internal transient t_AllowTransfer_To;
    uint256 internal transient t_AllowTransfer_Amount;
    /****** End Transient Variables ******/

    constructor(
        address _asset,
        address _yieldToken,
        uint256 _feeRate,
        uint8 __yieldTokenDecimals
    ) ERC20("", "") {
        feeRate = _feeRate;
        asset = address(_asset);
        yieldToken = address(_yieldToken);
        // Not all yield tokens have a decimals() function (i.e. Convex staked tokens), so we
        // do have to pass in the decimals as a parameter.
        _yieldTokenDecimals = __yieldTokenDecimals;
        _assetDecimals = TokenUtils.getDecimals(_asset);
    }

    function name() public view override(ERC20, IERC20Metadata) returns (string memory) {
        return s_name;
    }

    function symbol() public view override(ERC20, IERC20Metadata) returns (string memory) {
        return s_symbol;
    }

    /*** Valuation and Conversion Functions ***/

    /// @inheritdoc IYieldStrategy
    function convertSharesToYieldToken(uint256 shares) public view override returns (uint256) {
        // NOTE: rounds down on division
        return (shares * (_yieldTokenBalance() - feesAccrued() + VIRTUAL_YIELD_TOKENS)) / (effectiveSupply());
    }

    /// @inheritdoc IYieldStrategy
    function convertYieldTokenToShares(uint256 yieldTokens) public view returns (uint256) {
        // NOTE: rounds down on division
        return (yieldTokens * effectiveSupply()) / (_yieldTokenBalance() - feesAccrued() + VIRTUAL_YIELD_TOKENS);
    }

    /// @inheritdoc IYieldStrategy
    function convertToShares(uint256 assets) public view override returns (uint256) {
        // NOTE: rounds down on division
        uint256 yieldTokens = assets * (10 ** (_yieldTokenDecimals + DEFAULT_DECIMALS)) / 
            (convertYieldTokenToAsset() * (10 ** _assetDecimals));
        return convertYieldTokenToShares(yieldTokens);
    }

    /// @inheritdoc IOracle
    function price() public view override returns (uint256) {
        return convertToAssets(SHARE_PRECISION) * (10 ** (36 - 24));
    }

    /// @inheritdoc IYieldStrategy
    function price(address borrower) external override returns (uint256) {
        // Do not change the current account in this method since this method is not
        // authenticated and we do not want to have any unexpected side effects.
        address prevCurrentAccount = t_CurrentAccount;

        t_CurrentAccount = borrower;
        uint256 p = convertToAssets(SHARE_PRECISION) * (10 ** (36 - 24));

        t_CurrentAccount = prevCurrentAccount;
        return p;
    }

    /// @inheritdoc IYieldStrategy
    function totalAssets() public view override returns (uint256) {
        return convertToAssets(totalSupply());
    }

    /// @inheritdoc IYieldStrategy
    function convertYieldTokenToAsset() public view returns (uint256) {
        // The trading module always returns a positive rate in 18 decimals so we can safely
        // cast to uint256
        (int256 rate , /* */) = TRADING_MODULE.getOraclePrice(yieldToken, asset);
        return uint256(rate);
    }

    /// @inheritdoc IYieldStrategy
    function effectiveSupply() public view returns (uint256) {
        return (totalSupply() - s_escrowedShares + VIRTUAL_SHARES);
    }

    /*** Fee Methods ***/

    /// @inheritdoc IYieldStrategy
    function feesAccrued() public view override returns (uint256 feesAccruedInYieldToken) {
        return s_accruedFeesInYieldToken + _calculateAdditionalFeesInYieldToken();
    }

    /// @inheritdoc IYieldStrategy
    function collectFees() external override {
        _accrueFees();
        _transferYieldTokenToOwner(ADDRESS_REGISTRY.feeReceiver(), s_accruedFeesInYieldToken);

        delete s_accruedFeesInYieldToken;
    }

    /*** Core Functions ***/
    modifier onlyLendingRouter() {
        if (ADDRESS_REGISTRY.isLendingRouter(msg.sender) == false) revert Unauthorized(msg.sender);
        t_CurrentLendingRouter = msg.sender;
        _;
        delete t_CurrentLendingRouter;
    }

    modifier setCurrentAccount(address onBehalf) {
        if (t_CurrentAccount == address(0)) {
            t_CurrentAccount = onBehalf;
        } else if (t_CurrentAccount != onBehalf) {
            revert CurrentAccountAlreadySet();
        }

        _;
    }

    /// @inheritdoc IYieldStrategy
    function clearCurrentAccount() external override onlyLendingRouter {
        delete t_CurrentAccount;
    }

    function mintShares(
        uint256 assetAmount,
        address receiver,
        bytes calldata depositData
    ) external override onlyLendingRouter setCurrentAccount(receiver) nonReentrant returns (uint256 sharesMinted) {
        // Cannot mint shares if the receiver has an active withdraw request
        if (_isWithdrawRequestPending(receiver)) revert CannotEnterPosition();
        ERC20(asset).safeTransferFrom(t_CurrentLendingRouter, address(this), assetAmount);
        sharesMinted = _mintSharesGivenAssets(assetAmount, depositData, receiver);

        t_AllowTransfer_To = t_CurrentLendingRouter;
        t_AllowTransfer_Amount = sharesMinted;
        // Transfer the shares to the lending router so it can supply collateral
        _transfer(receiver, t_CurrentLendingRouter, sharesMinted);
    }

    function burnShares(
        address sharesOwner,
        uint256 sharesToBurn,
        uint256 sharesHeld,
        bytes calldata redeemData
    ) external override onlyLendingRouter setCurrentAccount(sharesOwner) nonReentrant returns (uint256 assetsWithdrawn) {
        assetsWithdrawn = _burnShares(sharesToBurn, sharesHeld, redeemData, sharesOwner);

        // Send all the assets back to the lending router
        ERC20(asset).safeTransfer(t_CurrentLendingRouter, assetsWithdrawn);
    }

    function allowTransfer(
        address to, uint256 amount, address currentAccount
    ) external setCurrentAccount(currentAccount) onlyLendingRouter {
        // Sets the transient variables to allow the lending market to transfer shares on exit position
        // or liquidation.
        t_AllowTransfer_To = to;
        t_AllowTransfer_Amount = amount;
    }

    function preLiquidation(
        address liquidator,
        address liquidateAccount,
        uint256 sharesToLiquidate,
        uint256 accountSharesHeld
    ) external onlyLendingRouter {
        t_CurrentAccount = liquidateAccount;
        // Liquidator cannot liquidate if they have an active withdraw request, including a tokenized
        // withdraw request.
        if (_isWithdrawRequestPending(liquidator)) revert CannotEnterPosition();
        // Cannot receive a pending withdraw request if the liquidator has a balanceOf
        if (_isWithdrawRequestPending(liquidateAccount) && balanceOf(liquidator) > 0) {
            revert CannotEnterPosition();
        }
        _preLiquidation(liquidateAccount, liquidator, sharesToLiquidate, accountSharesHeld);

        // Allow transfers to the lending router which will proxy the call to liquidate.
        t_AllowTransfer_To = msg.sender;
        t_AllowTransfer_Amount = sharesToLiquidate;
    }

    function postLiquidation(
        address liquidator,
        address liquidateAccount,
        uint256 sharesToLiquidator
    ) external onlyLendingRouter {
        t_AllowTransfer_To = liquidator;
        t_AllowTransfer_Amount = sharesToLiquidator;
        // Transfer the shares to the liquidator from the lending router
        _transfer(t_CurrentLendingRouter, liquidator, sharesToLiquidator);

        _postLiquidation(liquidator, liquidateAccount, sharesToLiquidator);

        // Clear the transient variables to prevent re-use in a future call.
        delete t_CurrentAccount;
    }

    /// @inheritdoc IYieldStrategy
    /// @dev We do not set the current account here because valuation is not done in this method.
    /// A native balance does not require a collateral check.
    function redeemNative(
        uint256 sharesToRedeem,
        bytes memory redeemData
    ) external override nonReentrant returns (uint256 assetsWithdrawn) {
        uint256 sharesHeld = balanceOf(msg.sender);
        if (sharesHeld == 0) revert InsufficientSharesHeld();

        assetsWithdrawn = _burnShares(sharesToRedeem, sharesHeld, redeemData, msg.sender);
        ERC20(asset).safeTransfer(msg.sender, assetsWithdrawn);
    }

    /// @inheritdoc IYieldStrategy
    function initiateWithdraw(
        address account,
        uint256 sharesHeld,
        bytes calldata data
    ) external onlyLendingRouter setCurrentAccount(account) override returns (uint256 requestId) {
        requestId = _withdraw(account, sharesHeld, data);
    }

    /// @inheritdoc IYieldStrategy
    /// @dev We do not set the current account here because valuation is not done in this method. A
    /// native balance does not require a collateral check.
    function initiateWithdrawNative(
        bytes memory data
    ) external override returns (uint256 requestId) {
        requestId = _withdraw(msg.sender, balanceOf(msg.sender), data);
    }

    function _withdraw(address account, uint256 sharesHeld, bytes memory data) internal returns (uint256 requestId) {
        if (sharesHeld == 0) revert InsufficientSharesHeld();

        // Accrue fees before initiating a withdraw since it will change the effective supply
        _accrueFees();
        uint256 yieldTokenAmount = convertSharesToYieldToken(sharesHeld);
        requestId = _initiateWithdraw(account, yieldTokenAmount, sharesHeld, data);
        // Escrow the shares after the withdraw since it will change the effective supply
        // during reward claims when using the RewardManagerMixin.
        s_escrowedShares += sharesHeld;
    }

    /*** Private Functions ***/

    function _calculateAdditionalFeesInYieldToken() private view returns (uint256 additionalFeesInYieldToken) {
        uint256 timeSinceLastFeeAccrual = block.timestamp - s_lastFeeAccrualTime;
        // e ^ (feeRate * timeSinceLastFeeAccrual / YEAR)
        uint256 x = (feeRate * timeSinceLastFeeAccrual) / YEAR;
        if (x == 0) return 0;

        uint256 preFeeUserHeldYieldTokens = _yieldTokenBalance() - s_accruedFeesInYieldToken;
        // Taylor approximation of e ^ x = 1 + x + x^2 / 2! + x^3 / 3! + ...
        uint256 eToTheX = DEFAULT_PRECISION + x + (x * x) / (2 * DEFAULT_PRECISION) + (x * x * x) / (6 * DEFAULT_PRECISION * DEFAULT_PRECISION);
        // Decay the user's yield tokens by e ^ (feeRate * timeSinceLastFeeAccrual / YEAR)
        uint256 postFeeUserHeldYieldTokens = preFeeUserHeldYieldTokens * DEFAULT_PRECISION / eToTheX;

        additionalFeesInYieldToken = preFeeUserHeldYieldTokens - postFeeUserHeldYieldTokens;
    }

    function _accrueFees() private {
        if (s_lastFeeAccrualTime == block.timestamp) return;
        // NOTE: this has to be called before any mints or burns.
        s_accruedFeesInYieldToken += _calculateAdditionalFeesInYieldToken();
        s_lastFeeAccrualTime = uint32(block.timestamp);
    }

    function _update(address from, address to, uint256 value) internal override {
        if (from != address(0) && to != address(0)) {
            // Any transfers off of the lending market must be authorized here, this means that native balances
            // held cannot be transferred.
            if (t_AllowTransfer_To != to) revert UnauthorizedLendingMarketTransfer(from, to, value);
            if (t_AllowTransfer_Amount < value) revert UnauthorizedLendingMarketTransfer(from, to, value);

            delete t_AllowTransfer_To;
            delete t_AllowTransfer_Amount;
        }

        super._update(from, to, value);
    }

    /*** Internal Helper Functions ***/

    function _isWithdrawRequestPending(address account) virtual internal view returns (bool) {
        return address(withdrawRequestManager) != address(0)
            && withdrawRequestManager.isPendingWithdrawRequest(address(this), account);
    }

    function _yieldTokenBalance() internal view returns (uint256) {
        return ERC20(yieldToken).balanceOf(address(this));
    }

    /// @dev Can be used to delegate call to the TradingModule's implementation in order to execute a trade
    function _executeTrade(
        Trade memory trade,
        uint16 dexId
    ) internal returns (uint256 amountSold, uint256 amountBought) {
        if (trade.tradeType == TradeType.STAKE_TOKEN) {
            IWithdrawRequestManager wrm = ADDRESS_REGISTRY.getWithdrawRequestManager(trade.buyToken);
            ERC20(trade.sellToken).checkApprove(address(wrm), trade.amount);
            amountBought = wrm.stakeTokens(trade.sellToken, trade.amount, trade.exchangeData);
            return (trade.amount, amountBought);
        } else {
            address implementation = nProxy(payable(address(TRADING_MODULE))).getImplementation();
            bytes memory result = _delegateCall(
                implementation, abi.encodeWithSelector(TRADING_MODULE.executeTrade.selector, dexId, trade)
            );
            (amountSold, amountBought) = abi.decode(result, (uint256, uint256));
        }
    }

    function _delegateCall(address target, bytes memory data) internal returns (bytes memory result) {
        bool success;
        (success, result) = target.delegatecall(data);
        if (!success) {
            assembly {
                // Copy the return data to memory
                returndatacopy(0, 0, returndatasize())
                // Revert with the return data
                revert(0, returndatasize())
            }
        }
    }

    /*** Virtual Functions ***/

    function _initialize(bytes calldata data) internal override virtual {
        (string memory _name, string memory _symbol) = abi.decode(data, (string, string));
        s_name = _name;
        s_symbol = _symbol;

        s_lastFeeAccrualTime = uint32(block.timestamp);
        emit VaultCreated(address(this));
    }

    /// @dev Marked as virtual to allow for RewardManagerMixin to override
    function _mintSharesGivenAssets(uint256 assets, bytes memory depositData, address receiver) internal virtual returns (uint256 sharesMinted) {
        if (assets == 0) return 0;

        // First accrue fees on the yield token
        _accrueFees();
        uint256 initialYieldTokenBalance = _yieldTokenBalance();
        _mintYieldTokens(assets, receiver, depositData);
        uint256 yieldTokensMinted = _yieldTokenBalance() - initialYieldTokenBalance;

        sharesMinted = (yieldTokensMinted * effectiveSupply()) / (initialYieldTokenBalance - feesAccrued() + VIRTUAL_YIELD_TOKENS);
        _mint(receiver, sharesMinted);
    }

    /// @dev Marked as virtual to allow for RewardManagerMixin to override
    function _burnShares(
        uint256 sharesToBurn,
        uint256 /* sharesHeld */,
        bytes memory redeemData,
        address sharesOwner
    ) internal virtual returns (uint256 assetsWithdrawn) {
        if (sharesToBurn == 0) return 0;
        bool isEscrowed = _isWithdrawRequestPending(sharesOwner);

        uint256 initialAssetBalance = TokenUtils.tokenBalance(asset);

        // First accrue fees on the yield token
        _accrueFees();
        _redeemShares(sharesToBurn, sharesOwner, isEscrowed, redeemData);
        if (isEscrowed) s_escrowedShares -= sharesToBurn;

        uint256 finalAssetBalance = TokenUtils.tokenBalance(asset);
        assetsWithdrawn = finalAssetBalance - initialAssetBalance;

        // This burns the shares from the sharesOwner's balance
        _burn(sharesOwner, sharesToBurn);
    }

    /// @dev Some yield tokens (such as Convex staked tokens) cannot be transferred, so we may need
    /// to override this function.
    function _transferYieldTokenToOwner(address owner, uint256 yieldTokens) internal virtual {
        ERC20(yieldToken).safeTransfer(owner, yieldTokens);
    }

    /// @dev Returns the maximum number of shares that can be liquidated. Allows the strategy to override the
    /// underlying lending market's liquidation logic.
    function _preLiquidation(address liquidateAccount, address liquidator, uint256 sharesToLiquidate, uint256 accountSharesHeld) internal virtual;

    /// @dev Called after liquidation
    function _postLiquidation(address liquidator, address liquidateAccount, uint256 sharesToLiquidator) internal virtual returns (bool didTokenize);

    /// @dev Mints yield tokens given a number of assets.
    function _mintYieldTokens(uint256 assets, address receiver, bytes memory depositData) internal virtual;

    /// @dev Redeems shares
    function _redeemShares(
        uint256 sharesToRedeem,
        address sharesOwner,
        bool isEscrowed,
        bytes memory redeemData
    ) internal virtual;

    function _initiateWithdraw(
        address account,
        uint256 yieldTokenAmount,
        uint256 sharesHeld,
        bytes memory data
    ) internal virtual returns (uint256 requestId);

    /// @inheritdoc IYieldStrategy
    function convertToAssets(uint256 shares) public view virtual override returns (uint256) {
        uint256 yieldTokens = convertSharesToYieldToken(shares);
        // NOTE: rounds down on division
        return (yieldTokens * convertYieldTokenToAsset() * (10 ** _assetDecimals)) /
            (10 ** (_yieldTokenDecimals + DEFAULT_DECIMALS));
    }

}


// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.29;

import "../interfaces/ISingleSidedLP.sol";
import {AbstractYieldStrategy} from "../AbstractYieldStrategy.sol";
import {DEFAULT_PRECISION, ADDRESS_REGISTRY} from "../utils/Constants.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Trade, TradeType} from "../interfaces/ITradingModule.sol";
import {RewardManagerMixin} from "../rewards/RewardManagerMixin.sol";
import {IWithdrawRequestManager, WithdrawRequest, TokenizedWithdrawRequest} from "../interfaces/IWithdrawRequestManager.sol";
import {TokenUtils} from "../utils/TokenUtils.sol";
import {
    CannotEnterPosition,
    WithdrawRequestNotFinalized,
    PoolShareTooHigh,
    AssetRemaining
} from "../interfaces/Errors.sol";

/**
 * @notice Base contract for the SingleSidedLP strategy. This strategy deposits into an LP
 * pool given a single borrowed currency. Allows for users to trade via external exchanges
 * during entry and exit, but the general expected behavior is single sided entries and
 * exits. Inheriting contracts will fill in the implementation details for integration with
 * the external DEX pool.
 */
abstract contract AbstractSingleSidedLP is RewardManagerMixin {
    using TokenUtils for ERC20;

    uint256 immutable MAX_POOL_SHARE;
    address internal immutable LP_LIB;

    /************************************************************************
     * VIRTUAL FUNCTIONS                                                    *
     * These virtual functions are used to isolate implementation specific  *
     * behavior.                                                            *
     ************************************************************************/

    /// @notice Total number of tokens held by the LP token
    function NUM_TOKENS() internal view virtual returns (uint256);

    /// @notice Addresses of tokens held and decimal places of each token. ETH will always be
    /// recorded in this array as address(0)
    function TOKENS() internal view virtual returns (ERC20[] memory);

    /// @notice Index of the TOKENS() array that refers to the primary borrowed currency by the
    /// leveraged vault. All valuations are done in terms of this currency.
    function PRIMARY_INDEX() internal view virtual returns (uint256);

    /// @notice Returns the total supply of the pool token. Is a virtual function because
    /// ComposableStablePools use a "virtual supply" and a different method must be called
    /// to get the actual total supply.
    function _totalPoolSupply() internal view virtual returns (uint256);

    /// @dev Checks that the reentrancy context is valid
    function _checkReentrancyContext() internal virtual;

    /// @notice Called once during initialization to set the initial token approvals.
    function _initialApproveTokens() internal virtual {
        _delegateCall(LP_LIB, abi.encodeWithSelector(ILPLib.initialApproveTokens.selector));
    }

    /// @notice Implementation specific wrapper for joining a pool with the given amounts. Will also
    /// stake on the relevant booster protocol.
    function _joinPoolAndStake(
        uint256[] memory amounts, uint256 minPoolClaim
    ) internal virtual {
        _delegateCall(LP_LIB, abi.encodeWithSelector(ILPLib.joinPoolAndStake.selector, amounts, minPoolClaim));
    }

    /// @notice Implementation specific wrapper for unstaking from the booster protocol and withdrawing
    /// funds from the LP pool
    function _unstakeAndExitPool(
        uint256 poolClaim, uint256[] memory minAmounts, bool isSingleSided
    ) internal virtual returns (uint256[] memory exitBalances) {
        bytes memory result = _delegateCall(LP_LIB, abi.encodeWithSelector(
            ILPLib.unstakeAndExitPool.selector, poolClaim, minAmounts, isSingleSided
        ));
        exitBalances = abi.decode(result, (uint256[]));
    }

    /************************************************************************
     * CLASS FUNCTIONS                                                      *
     * Below are class functions that represent the base implementation     *
     * of the Single Sided LP strategy.                                     *
     ************************************************************************/

    constructor(
        uint256 _maxPoolShare,
        address _asset,
        address _yieldToken,
        uint256 _feeRate,
        address _rewardManager,
        uint8 _yieldTokenDecimals,
        IWithdrawRequestManager _withdrawRequestManager
    ) RewardManagerMixin( _asset, _yieldToken, _feeRate, _rewardManager, _yieldTokenDecimals) {
        MAX_POOL_SHARE = _maxPoolShare;
        // Although there will be multiple withdraw request managers, we only need to set one here
        // to check whether or not a withdraw request is pending. If any one of the withdraw requests
        // is not finalized then the entire withdraw will revert and the user will remain in a pending
        // withdraw state.
        withdrawRequestManager = _withdrawRequestManager;
    }

    function _initialize(bytes calldata data) internal override {
        super._initialize(data);
        _initialApproveTokens();
    }

    function _mintYieldTokens(
        uint256 assets,
        address /* receiver */,
        bytes memory depositData
    ) internal override {
        DepositParams memory params = abi.decode(depositData, (DepositParams));
        uint256[] memory amounts = new uint256[](NUM_TOKENS());

        // If depositTrades are specified, then parts of the initial deposit are traded
        // for corresponding amounts of the other pool tokens via external exchanges. If
        // these amounts are not specified then the pool will just be joined single sided.
        // Deposit trades are not automatically enabled on vaults since the trading module
        // requires explicit permission for every token that can be sold by an address.
        if (params.depositTrades.length > 0) {
            // NOTE: amounts is modified in place
            _executeDepositTrades(assets, amounts, params.depositTrades);
        } else {
            // This is a single sided entry, will revert if index is out of bounds
            amounts[PRIMARY_INDEX()] = assets;
        }

        _joinPoolAndStake(amounts, params.minPoolClaim);

        _checkPoolShare();
    }

    function _checkPoolShare() internal view virtual {
        // Checks that the vault does not own too large of a portion of the pool. If this is the case,
        // single sided exits may have a detrimental effect on the liquidity.
        uint256 maxSupplyThreshold = (_totalPoolSupply() * MAX_POOL_SHARE) / DEFAULT_PRECISION;
        // This is incumbent on a 1-1 ratio between the lpToken and the yieldToken, if that is not the
        // case then this function must be overridden.
        uint256 poolClaim = _yieldTokenBalance();
        if (maxSupplyThreshold < poolClaim) revert PoolShareTooHigh(poolClaim, maxSupplyThreshold);
    }

    function _redeemShares(
        uint256 sharesToRedeem,
        address sharesOwner,
        bool isEscrowed,
        bytes memory redeemData
    ) internal override {
        RedeemParams memory params = abi.decode(redeemData, (RedeemParams));

        // Stores the amount of each token that has been withdrawn from the pool.
        uint256[] memory exitBalances;
        bool isSingleSided;
        ERC20[] memory tokens;
        if (isEscrowed) {
            // Attempt to withdraw all pending requests, tokens may be different if there
            // is a withdraw request.
            (exitBalances, tokens) = _withdrawPendingRequests(sharesOwner, sharesToRedeem);
            // If there are pending requests, then we are not single sided by definition
            isSingleSided = false;
        } else {
            isSingleSided = params.redemptionTrades.length == 0;
            uint256 yieldTokensBurned = convertSharesToYieldToken(sharesToRedeem);
            exitBalances = _unstakeAndExitPool(yieldTokensBurned, params.minAmounts, isSingleSided);
            tokens = TOKENS();
        }

        if (!isSingleSided) {
            // If not a single sided trade, will execute trades back to the primary token on
            // external exchanges. This method will execute EXACT_IN trades to ensure that
            // all of the balance in the other tokens is sold for primary.
            // Redemption trades are not automatically enabled on vaults since the trading module
            // requires explicit permission for every token that can be sold by an address.
            _executeRedemptionTrades(tokens, exitBalances, params.redemptionTrades);
        }
    }

    /// @dev Trades the amount of primary token into other secondary tokens prior to entering a pool.
    function _executeDepositTrades(
        uint256 assets,
        uint256[] memory amounts,
        TradeParams[] memory depositTrades
    ) internal {
        ERC20[] memory tokens = TOKENS();
        Trade memory trade;
        uint256 assetRemaining = assets;

        for (uint256 i; i < amounts.length; i++) {
            if (i == PRIMARY_INDEX()) continue;
            TradeParams memory t = depositTrades[i];

            if (t.tradeAmount > 0) {
                trade = Trade({
                    tradeType: t.tradeType,
                    sellToken: address(asset),
                    buyToken: address(tokens[i]),
                    amount: t.tradeAmount,
                    limit: t.minPurchaseAmount,
                    deadline: block.timestamp,
                    exchangeData: t.exchangeData
                });
                // Always selling the primaryToken and buying the secondary token.
                (uint256 amountSold, uint256 amountBought) = _executeTrade(trade, t.dexId);

                amounts[i] = amountBought;
                // Will revert on underflow if over-selling the primary borrowed
                assetRemaining -= amountSold;
            }
        }

        if (PRIMARY_INDEX() < amounts.length) {
            amounts[PRIMARY_INDEX()] = assetRemaining;
        } else if (0 < assetRemaining) {
            // This can happen if the asset is not in the pool and we need to trade all
            // of the remaining asset for tokens in the pool.
            revert AssetRemaining(assetRemaining);
        }
    }

    /// @dev Trades the amount of secondary tokens into the primary token after exiting a pool.
    function _executeRedemptionTrades(
        ERC20[] memory tokens,
        uint256[] memory exitBalances,
        TradeParams[] memory redemptionTrades
    ) internal returns (uint256 finalPrimaryBalance) {
        for (uint256 i; i < exitBalances.length; i++) {
            if (address(tokens[i]) == address(asset)) {
                finalPrimaryBalance += exitBalances[i];
                continue;
            }

            TradeParams memory t = redemptionTrades[i];
            // Always sell the entire exit balance to the primary token
            if (exitBalances[i] > 0) {
                Trade memory trade = Trade({
                    tradeType: t.tradeType,
                    sellToken: address(tokens[i]),
                    buyToken: address(asset),
                    amount: exitBalances[i],
                    limit: t.minPurchaseAmount,
                    deadline: block.timestamp,
                    exchangeData: t.exchangeData
                });
                (/* */, uint256 amountBought) = _executeTrade(trade, t.dexId);

                finalPrimaryBalance += amountBought;
            }
        }
    }

    function _preLiquidation(address liquidateAccount, address liquidator, uint256 sharesToLiquidate, uint256 accountSharesHeld) internal override {
        _checkReentrancyContext();
        return super._preLiquidation(liquidateAccount, liquidator, sharesToLiquidate, accountSharesHeld);
    }

    function __postLiquidation(address liquidator, address liquidateAccount, uint256 sharesToLiquidator) internal override returns (bool didTokenize) {
        bytes memory result = _delegateCall(LP_LIB, abi.encodeWithSelector(
            ILPLib.tokenizeWithdrawRequest.selector, liquidateAccount, liquidator, sharesToLiquidator
        ));
        didTokenize = abi.decode(result, (bool));
    }

    function __initiateWithdraw(
        address account,
        uint256 yieldTokenAmount,
        uint256 sharesHeld,
        bytes memory data
    ) internal override returns (uint256 requestId) {
        WithdrawParams memory params = abi.decode(data, (WithdrawParams));

        uint256[] memory exitBalances = _unstakeAndExitPool({
            poolClaim: yieldTokenAmount,
            minAmounts: params.minAmounts,
            // When initiating a withdraw, we always exit proportionally
            isSingleSided: false
        });

        bytes memory result = _delegateCall(LP_LIB, abi.encodeWithSelector(
            ILPLib.initiateWithdraw.selector, account, sharesHeld, exitBalances, params.withdrawData
        ));
        uint256[] memory requestIds = abi.decode(result, (uint256[]));
        // Although we get multiple requests ids, we just return the first one here. The rest will be
        // observable off chain.
        requestId = requestIds[0];
    }

    function _withdrawPendingRequests(
        address sharesOwner,
        uint256 sharesToRedeem
    ) internal returns (uint256[] memory exitBalances, ERC20[] memory tokens) {
        bytes memory result = _delegateCall(LP_LIB, abi.encodeWithSelector(
            ILPLib.finalizeAndRedeemWithdrawRequest.selector, sharesOwner, sharesToRedeem
        ));
        (exitBalances, tokens) = abi.decode(result, (uint256[], ERC20[]));
    }

    /// @notice Returns the total value in terms of the borrowed token of the account's position
    function convertToAssets(uint256 shares) public view override returns (uint256) {
        if (t_CurrentAccount != address(0) && _isWithdrawRequestPending(t_CurrentAccount)) {
            return ILPLib(LP_LIB).getWithdrawRequestValue(t_CurrentAccount, asset, shares);
        }

        return super.convertToAssets(shares);
    }

    function _isWithdrawRequestPending(address account) internal view override returns (bool) {
        return ILPLib(LP_LIB).hasPendingWithdrawals(account);
    }
}

abstract contract BaseLPLib is ILPLib {
    using TokenUtils for ERC20;

    function TOKENS() internal view virtual returns (ERC20[] memory);

    /// @inheritdoc ILPLib
    function getWithdrawRequestValue(
        address account,
        address asset,
        uint256 shares
    ) external view returns (uint256 totalValue) {
        ERC20[] memory tokens = TOKENS();

        for (uint256 i; i < tokens.length; i++) {
            IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));
            // This is called as a view function, not a delegate call so use the msg.sender to get
            // the correct vault address
            (bool hasRequest, uint256 value) = manager.getWithdrawRequestValue(msg.sender, account, asset, shares);
            // Ensure that this is true so that we do not lose any value.
            require(hasRequest);
            totalValue += value;
        }
    }

    /// @inheritdoc ILPLib
    function hasPendingWithdrawals(address account) external view override returns (bool) {
        ERC20[] memory tokens = TOKENS();
        for (uint256 i; i < tokens.length; i++) {
            IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));
            if (address(manager) == address(0)) continue;
            // This is called as a view function, not a delegate call so use the msg.sender to get
            // the correct vault address
            (WithdrawRequest memory w, /* */) = manager.getWithdrawRequest(msg.sender, account);
            if (w.requestId != 0) return true;
        }

        return false;
    }

    /// @inheritdoc ILPLib
    function initiateWithdraw(
        address account,
        uint256 sharesHeld,
        uint256[] calldata exitBalances,
        bytes[] calldata withdrawData
    ) external override returns (uint256[] memory requestIds) {
        ERC20[] memory tokens = TOKENS();

        requestIds = new uint256[](exitBalances.length);
        for (uint256 i; i < exitBalances.length; i++) {
            if (exitBalances[i] == 0) continue;
            IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));

            tokens[i].checkApprove(address(manager), exitBalances[i]);
            // Will revert if there is already a pending withdraw
            requestIds[i] = manager.initiateWithdraw({
                account: account,
                yieldTokenAmount: exitBalances[i],
                sharesAmount: sharesHeld,
                data: withdrawData[i]
            });
        }
    }

    /// @inheritdoc ILPLib
    function finalizeAndRedeemWithdrawRequest(
        address sharesOwner,
        uint256 sharesToRedeem
    ) external override returns (uint256[] memory exitBalances, ERC20[] memory withdrawTokens) {
        ERC20[] memory tokens = TOKENS();

        exitBalances = new uint256[](tokens.length);
        withdrawTokens = new ERC20[](tokens.length);

        WithdrawRequest memory w;
        for (uint256 i; i < tokens.length; i++) {
            IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));
            (w, /* */) = manager.getWithdrawRequest(address(this), sharesOwner);

            uint256 yieldTokensBurned = uint256(w.yieldTokenAmount) * sharesToRedeem / w.sharesAmount;
            bool finalized;
            (exitBalances[i], finalized) = manager.finalizeAndRedeemWithdrawRequest({
                account: sharesOwner, withdrawYieldTokenAmount: yieldTokensBurned, sharesToBurn: sharesToRedeem
            });
            if (!finalized) revert WithdrawRequestNotFinalized(w.requestId);
            withdrawTokens[i] = ERC20(manager.WITHDRAW_TOKEN());
        }
    }

    /// @inheritdoc ILPLib
    function tokenizeWithdrawRequest(
        address liquidateAccount,
        address liquidator,
        uint256 sharesToLiquidator
    ) external override returns (bool didTokenize) {
        ERC20[] memory tokens = TOKENS();
        for (uint256 i; i < tokens.length; i++) {
            IWithdrawRequestManager manager = ADDRESS_REGISTRY.getWithdrawRequestManager(address(tokens[i]));
            if (address(manager) == address(0)) continue;
            // If there is no withdraw request then this will be a noop, make sure to OR with the previous result
            // to ensure that the result is always set but it is done after so the tokenizeWithdrawRequest call
            // is not short circuited.
            didTokenize = manager.tokenizeWithdrawRequest(liquidateAccount, liquidator, sharesToLiquidator) || didTokenize;
        }
    }
}


Vulnerability type: CWE-435|CWE-436|CWE-437