HIGH severity: Malicious user can change the TradeType to steal funds from the vault or withdraw request manager. The vulnerability exists in the _executeRedemptionTrades function of AbstractSingleSidedLP and the _preStakingTrade function of AbstractWithdrawRequestManager, where the trade type (TradeType) is not hardcoded and can be arbitrarily set by the caller. The intended behavior is to use TradeType.EXACT_...

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

import "../interfaces/IWithdrawRequestManager.sol";
import {Initializable} from "../proxy/Initializable.sol";
import {ClonedCoolDownHolder} from "./ClonedCoolDownHolder.sol";
import {
    Unauthorized,
    ExistingWithdrawRequest,
    NoWithdrawRequest,
    InvalidWithdrawRequestTokenization
} from "../interfaces/Errors.sol";
import {TypeConvert} from "../utils/TypeConvert.sol";
import {TokenUtils} from "../utils/TokenUtils.sol";
import {ADDRESS_REGISTRY, DEFAULT_PRECISION} from "../utils/Constants.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Trade, TradeType, TRADING_MODULE, nProxy, TradeFailed} from "../interfaces/ITradingModule.sol";


/**
 * Library to handle potentially illiquid withdraw requests of staking tokens where there
 * is some indeterminate lock up time before tokens can be redeemed. Examples would be withdraws
 * of staked or restaked ETH, tokens like sUSDe or stkAave which have cooldown periods before they
 * can be withdrawn.
 *
 * Primarily, this library tracks the withdraw request and an associated identifier for the withdraw
 * request. It also allows for the withdraw request to be "tokenized" so that shares of the withdraw
 * request can be liquidated.
 */
abstract contract AbstractWithdrawRequestManager is IWithdrawRequestManager, Initializable {
    using SafeERC20 for ERC20;
    using TypeConvert for uint256;

    /// @inheritdoc IWithdrawRequestManager
    address public immutable override YIELD_TOKEN;
    /// @inheritdoc IWithdrawRequestManager
    address public immutable override WITHDRAW_TOKEN;
    /// @inheritdoc IWithdrawRequestManager
    address public immutable override STAKING_TOKEN;

    mapping(address => bool) public override isApprovedVault;
    mapping(address vault => mapping(address account => WithdrawRequest)) private s_accountWithdrawRequest;
    mapping(uint256 requestId => TokenizedWithdrawRequest) private s_tokenizedWithdrawRequest;

    constructor(address _withdrawToken, address _yieldToken, address _stakingToken) Initializable() {
        WITHDRAW_TOKEN = _withdrawToken;
        YIELD_TOKEN = _yieldToken;
        STAKING_TOKEN = _stakingToken;
    }

    modifier onlyOwner() {
        if (msg.sender != ADDRESS_REGISTRY.upgradeAdmin()) revert Unauthorized(msg.sender);
        _;
    }

    /// @dev Ensures that only approved vaults can initiate withdraw requests.
    modifier onlyApprovedVault() {
        if (!isApprovedVault[msg.sender]) revert Unauthorized(msg.sender);
        _;
    }

    /// @inheritdoc IWithdrawRequestManager
    function getWithdrawRequest(address vault, address account) public view override
        returns (WithdrawRequest memory w, TokenizedWithdrawRequest memory s) {
        w = s_accountWithdrawRequest[vault][account];
        s = s_tokenizedWithdrawRequest[w.requestId];
    }

    /// @inheritdoc IWithdrawRequestManager
    function isPendingWithdrawRequest(address vault, address account) public view override returns (bool) {
        return s_accountWithdrawRequest[vault][account].requestId != 0;
    }

    /// @inheritdoc IWithdrawRequestManager
    function setApprovedVault(address vault, bool isApproved) external override onlyOwner {
        isApprovedVault[vault] = isApproved;
        emit ApprovedVault(vault, isApproved);
    }

    /// @inheritdoc IWithdrawRequestManager
    function stakeTokens(
        address depositToken,
        uint256 amount,
        bytes calldata data
    ) external override onlyApprovedVault returns (uint256 yieldTokensMinted) {
        uint256 initialYieldTokenBalance = ERC20(YIELD_TOKEN).balanceOf(address(this));
        ERC20(depositToken).safeTransferFrom(msg.sender, address(this), amount);
        (uint256 stakeTokenAmount, bytes memory stakeData) = _preStakingTrade(depositToken, amount, data);
        _stakeTokens(stakeTokenAmount, stakeData);

        yieldTokensMinted = ERC20(YIELD_TOKEN).balanceOf(address(this)) - initialYieldTokenBalance;
        ERC20(YIELD_TOKEN).safeTransfer(msg.sender, yieldTokensMinted);
    }

    /// @inheritdoc IWithdrawRequestManager
    function initiateWithdraw(
        address account,
        uint256 yieldTokenAmount,
        uint256 sharesAmount,
        bytes calldata data
    ) external override onlyApprovedVault returns (uint256 requestId) {
        WithdrawRequest storage accountWithdraw = s_accountWithdrawRequest[msg.sender][account];
        if (accountWithdraw.requestId != 0) revert ExistingWithdrawRequest(msg.sender, account, accountWithdraw.requestId);

        // Receive the requested amount of yield tokens from the approved vault.
        ERC20(YIELD_TOKEN).safeTransferFrom(msg.sender, address(this), yieldTokenAmount);

        requestId = _initiateWithdrawImpl(account, yieldTokenAmount, data);
        accountWithdraw.requestId = requestId;
        accountWithdraw.yieldTokenAmount = yieldTokenAmount.toUint120();
        accountWithdraw.sharesAmount = sharesAmount.toUint120();
        s_tokenizedWithdrawRequest[requestId] = TokenizedWithdrawRequest({
            totalYieldTokenAmount: yieldTokenAmount.toUint120(),
            totalWithdraw: 0,
            finalized: false
        });

        emit InitiateWithdrawRequest(account, msg.sender, yieldTokenAmount, sharesAmount, requestId);
    }

    /// @inheritdoc IWithdrawRequestManager
    function finalizeAndRedeemWithdrawRequest(
        address account,
        uint256 withdrawYieldTokenAmount,
        uint256 sharesToBurn
    ) external override onlyApprovedVault returns (uint256 tokensWithdrawn, bool finalized) {
        WithdrawRequest storage s_withdraw = s_accountWithdrawRequest[msg.sender][account];
        if (s_withdraw.requestId == 0) return (0, false);

        (tokensWithdrawn, finalized) = _finalizeWithdraw(account, s_withdraw);

        if (finalized) {
            // Allows for partial withdrawal of yield tokens
            if (withdrawYieldTokenAmount < s_withdraw.yieldTokenAmount) {
                tokensWithdrawn = tokensWithdrawn * withdrawYieldTokenAmount / s_withdraw.yieldTokenAmount;
                s_withdraw.sharesAmount -= sharesToBurn.toUint120();
                s_withdraw.yieldTokenAmount -= withdrawYieldTokenAmount.toUint120();
            } else {
                require(s_withdraw.yieldTokenAmount == withdrawYieldTokenAmount);
                delete s_accountWithdrawRequest[msg.sender][account];
            }

            ERC20(WITHDRAW_TOKEN).safeTransfer(msg.sender, tokensWithdrawn);
        }
    }

    /// @inheritdoc IWithdrawRequestManager
    function finalizeRequestManual(
        address vault,
        address account
    ) external override returns (uint256 tokensWithdrawn, bool finalized) {
        WithdrawRequest storage s_withdraw = s_accountWithdrawRequest[vault][account];
        if (s_withdraw.requestId == 0) revert NoWithdrawRequest(vault, account);

        // Do not transfer any tokens off of this method here. Withdrawn tokens will be held in the
        // tokenized withdraw request until the vault calls this contract to withdraw the tokens.
        (tokensWithdrawn, finalized) = _finalizeWithdraw(account, s_withdraw);
    }

    /// @inheritdoc IWithdrawRequestManager
    function tokenizeWithdrawRequest(
        address _from,
        address _to,
        uint256 sharesAmount
    ) external override onlyApprovedVault returns (bool didTokenize) {
        if (_from == _to) revert();

        WithdrawRequest storage s_withdraw = s_accountWithdrawRequest[msg.sender][_from];
        uint256 requestId = s_withdraw.requestId;
        if (requestId == 0 || sharesAmount == 0) return false;

        // Ensure that no withdraw request gets overridden, the _to account always receives their withdraw
        // request in the account withdraw slot. All storage is updated prior to changes to the `w` storage
        // variable below.
        WithdrawRequest storage toWithdraw = s_accountWithdrawRequest[msg.sender][_to];
        if (toWithdraw.requestId != 0 && toWithdraw.requestId != requestId) {
            revert ExistingWithdrawRequest(msg.sender, _to, toWithdraw.requestId);
        }

        toWithdraw.requestId = requestId;

        if (s_withdraw.sharesAmount < sharesAmount) {
            // This should never occur given the checks below.
            revert InvalidWithdrawRequestTokenization();
        } else if (s_withdraw.sharesAmount == sharesAmount) {
            // If the resulting vault shares is zero, then delete the request. The _from account's
            // withdraw request is fully transferred to _to. In this case, the _to account receives
            // the full amount of the _from account's withdraw request.
            toWithdraw.yieldTokenAmount = toWithdraw.yieldTokenAmount + s_withdraw.yieldTokenAmount;
            toWithdraw.sharesAmount = toWithdraw.sharesAmount + s_withdraw.sharesAmount;
            delete s_accountWithdrawRequest[msg.sender][_from];
        } else {
            // In this case, the amount of yield tokens is transferred from one account to the other.
            uint256 yieldTokenAmount = s_withdraw.yieldTokenAmount * sharesAmount / s_withdraw.sharesAmount;
            toWithdraw.yieldTokenAmount = (toWithdraw.yieldTokenAmount + yieldTokenAmount).toUint120();
            toWithdraw.sharesAmount = (toWithdraw.sharesAmount + sharesAmount).toUint120();
            s_withdraw.yieldTokenAmount = (s_withdraw.yieldTokenAmount - yieldTokenAmount).toUint120();
            s_withdraw.sharesAmount = (s_withdraw.sharesAmount - sharesAmount).toUint120();
        }

        emit WithdrawRequestTokenized(_from, _to, requestId, sharesAmount);
        return true;
    }

    /// @inheritdoc IWithdrawRequestManager
    function rescueTokens(
        address cooldownHolder, address token, address receiver, uint256 amount
    ) external override onlyOwner {
        ClonedCoolDownHolder(cooldownHolder).rescueTokens(ERC20(token), receiver, amount);
    }

    /// @notice Finalizes a withdraw request and updates the account required to determine how many
    /// tokens the account has a claim over.
    function _finalizeWithdraw(
        address account,
        WithdrawRequest memory w
    ) internal returns (uint256 tokensWithdrawn, bool finalized) {
        TokenizedWithdrawRequest storage s = s_tokenizedWithdrawRequest[w.requestId];

        // If the tokenized request was already finalized in a different transaction
        // then return the values here and we can short circuit the withdraw impl
        if (s.finalized) {
            return (
                uint256(s.totalWithdraw) * uint256(w.yieldTokenAmount) / uint256(s.totalYieldTokenAmount),
                true
            );
        }

        // These values are the total tokens claimed from the withdraw request, does not
        // account for potential tokenization.
        (tokensWithdrawn, finalized) = _finalizeWithdrawImpl(account, w.requestId);

        if (finalized) {
            s.totalWithdraw = tokensWithdrawn.toUint120();
            // Safety check to ensure that we do not override a finalized tokenized withdraw request
            require(s.finalized == false);
            s.finalized = true;

            tokensWithdrawn = uint256(s.totalWithdraw) * uint256(w.yieldTokenAmount) / uint256(s.totalYieldTokenAmount);
        } else {
            // No tokens claimed if not finalized
            require(tokensWithdrawn == 0);
        }
    }


    /// @notice Required implementation to begin the withdraw request
    /// @return requestId some identifier of the withdraw request
    function _initiateWithdrawImpl(
        address account,
        uint256 yieldTokenAmount,
        bytes calldata data
    ) internal virtual returns (uint256 requestId);

    /// @notice Required implementation to finalize the withdraw
    /// @return tokensWithdrawn total tokens claimed as a result of the withdraw, does not
    /// necessarily represent the tokens that go to the account if the request has been
    /// tokenized due to liquidation
    /// @return finalized returns true if the withdraw has been finalized
    function _finalizeWithdrawImpl(address account, uint256 requestId) internal virtual returns (uint256 tokensWithdrawn, bool finalized);

    /// @notice Required implementation to stake the deposit token to the yield token
    function _stakeTokens(uint256 amount, bytes memory stakeData) internal virtual;

    /// @dev Allows for the deposit token to be traded into the staking token prior to staking, i.e.
    /// enables USDC to USDe trades before staking into sUSDe.
    function _preStakingTrade(address depositToken, uint256 depositAmount, bytes calldata data) internal returns (uint256 amountBought, bytes memory stakeData) {
        if (depositToken == STAKING_TOKEN) {
            amountBought = depositAmount;
            stakeData = data;
        } else {
            StakingTradeParams memory params = abi.decode(data, (StakingTradeParams));
            stakeData = params.stakeData;

            (/* */, amountBought) = _executeTrade(Trade({
                tradeType: params.tradeType,
                sellToken: depositToken,
                buyToken: STAKING_TOKEN,
                amount: depositAmount,
                exchangeData: params.exchangeData,
                limit: params.minPurchaseAmount,
                deadline: block.timestamp
            }), params.dexId);
        }
    }

    function _executeTrade(
        Trade memory trade,
        uint16 dexId
    ) internal returns (uint256 amountSold, uint256 amountBought) {
        (bool success, bytes memory result) = nProxy(payable(address(TRADING_MODULE))).getImplementation()
            .delegatecall(abi.encodeWithSelector(TRADING_MODULE.executeTrade.selector, dexId, trade));
        if (!success) {
            assembly {
                // Copy the return data to memory
                returndatacopy(0, 0, returndatasize())
                // Revert with the return data
                revert(0, returndatasize())
            }
        }

        (amountSold, amountBought) = abi.decode(result, (uint256, uint256));
    }

    /// @inheritdoc IWithdrawRequestManager
    function getWithdrawRequestValue(
        address vault,
        address account,
        address asset,
        uint256 shares
    ) external view override returns (bool hasRequest, uint256 valueInAsset) {
        WithdrawRequest memory w = s_accountWithdrawRequest[vault][account];
        if (w.requestId == 0) return (false, 0);

        TokenizedWithdrawRequest memory s = s_tokenizedWithdrawRequest[w.requestId];

        int256 tokenRate;
        uint256 tokenAmount;
        uint256 tokenDecimals;
        uint256 assetDecimals = TokenUtils.getDecimals(asset);
        if (s.finalized) {
            // If finalized the withdraw request is locked to the tokens withdrawn
            (tokenRate, /* */) = TRADING_MODULE.getOraclePrice(WITHDRAW_TOKEN, asset);
            tokenDecimals = TokenUtils.getDecimals(WITHDRAW_TOKEN);
            tokenAmount = (uint256(w.yieldTokenAmount) * uint256(s.totalWithdraw)) / uint256(s.totalYieldTokenAmount);
        } else {
            // Otherwise we use the yield token rate
            (tokenRate, /* */) = TRADING_MODULE.getOraclePrice(YIELD_TOKEN, asset);
            tokenDecimals = TokenUtils.getDecimals(YIELD_TOKEN);
            tokenAmount = w.yieldTokenAmount;
        }

        // The trading module always returns a positive rate in 18 decimals so we can safely
        // cast to uint256
        uint256 totalValue = (uint256(tokenRate) * tokenAmount * (10 ** assetDecimals)) /
            ((10 ** tokenDecimals) * DEFAULT_PRECISION);
        // NOTE: returns the normalized value given the shares input
        return (true, totalValue * shares / w.sharesAmount);
    }

}
// 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-707|CWE-20|CWE-1287