MEDIUM severity: Incompatibility of ERC20::approve with USDT tokens on Ethereum. The vulnerability arises from the use of high-level Solidity calls (e.g., ERC20(token).approve) that expect a boolean return value, which is not returned by certain ERC-20 tokens like USDT on Ethereum. The root cause is the violation of the ERC-20 standard by USDT, which omits the return statement i...

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

import {ILendingRouter, VaultPosition} from "../interfaces/ILendingRouter.sol";
import {
    NotAuthorized,
    CannotExitPositionWithinCooldownPeriod,
    CannotInitiateWithdraw,
    CannotForceWithdraw,
    InvalidLendingRouter,
    NoExistingPosition,
    LiquidatorHasPosition,
    CannotEnterPosition,
    CannotLiquidateZeroShares,
    InsufficientSharesHeld
} from "../interfaces/Errors.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {TokenUtils} from "../utils/TokenUtils.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IYieldStrategy} from "../interfaces/IYieldStrategy.sol";
import {RewardManagerMixin} from "../rewards/RewardManagerMixin.sol";
import {ILendingRouter} from "../interfaces/ILendingRouter.sol";
import {ADDRESS_REGISTRY, COOLDOWN_PERIOD} from "../utils/Constants.sol";

abstract contract AbstractLendingRouter is ILendingRouter {
    using SafeERC20 for ERC20;
    using TokenUtils for ERC20;

    mapping(address user => mapping(address operator => bool approved)) private s_isApproved;

    /*** Authorization Methods ***/
    modifier isAuthorized(address onBehalf, address vault) {
        // In this case msg.sender is the operator
        if (msg.sender != onBehalf && !isApproved(onBehalf, msg.sender)) {
            revert NotAuthorized(msg.sender, onBehalf);
        }

        _;

        // Clear the current account after the transaction is finished
        IYieldStrategy(vault).clearCurrentAccount();
    }

    /// @inheritdoc ILendingRouter
    function setApproval(address operator, bool approved) external override {
        if (operator == msg.sender) revert NotAuthorized(msg.sender, operator);
        s_isApproved[msg.sender][operator] = approved;
    }

    /// @inheritdoc ILendingRouter
    function isApproved(address user, address operator) public view override returns (bool) {
        return s_isApproved[user][operator];
    }

    /// @inheritdoc ILendingRouter
    function enterPosition(
        address onBehalf,
        address vault,
        uint256 depositAssetAmount,
        uint256 borrowAmount,
        bytes calldata depositData
    ) public override isAuthorized(onBehalf, vault) {
        _enterPosition(onBehalf, vault, depositAssetAmount, borrowAmount, depositData, address(0));
    }

    /// @inheritdoc ILendingRouter
    function migratePosition(
        address onBehalf,
        address vault,
        address migrateFrom
    ) public override isAuthorized(onBehalf, vault) {
        if (!ADDRESS_REGISTRY.isLendingRouter(migrateFrom)) revert InvalidLendingRouter();
        // Borrow amount is set to the amount of debt owed to the previous lending router
        (uint256 borrowAmount, /* */, /* */) = ILendingRouter(migrateFrom).healthFactor(onBehalf, vault);

        _enterPosition(onBehalf, vault, 0, borrowAmount, bytes(""), migrateFrom);
    }

    function _enterPosition(
        address onBehalf,
        address vault,
        uint256 depositAssetAmount,
        uint256 borrowAmount,
        bytes memory depositData,
        address migrateFrom
    ) internal {
        address asset = IYieldStrategy(vault).asset();
        // Cannot enter a position if the account already has a native share balance
        if (IYieldStrategy(vault).balanceOf(onBehalf) > 0) revert CannotEnterPosition();

        if (depositAssetAmount > 0) {
            // Take any margin deposit from the sender initially
            ERC20(asset).safeTransferFrom(msg.sender, address(this), depositAssetAmount);
        }

        if (borrowAmount > 0) {
            _flashBorrowAndEnter(
                onBehalf, vault, asset, depositAssetAmount, borrowAmount, depositData, migrateFrom
            );
        } else {
            _enterOrMigrate(onBehalf, vault, asset, depositAssetAmount, depositData, migrateFrom);
        }

        ADDRESS_REGISTRY.setPosition(onBehalf, vault);
    }

    /// @inheritdoc ILendingRouter
    function exitPosition(
        address onBehalf,
        address vault,
        address receiver,
        uint256 sharesToRedeem,
        uint256 assetToRepay,
        bytes calldata redeemData
    ) external override isAuthorized(onBehalf, vault) {
        _checkExit(onBehalf, vault);

        address asset = IYieldStrategy(vault).asset();
        if (0 < assetToRepay) {
            _exitWithRepay(onBehalf, vault, asset, receiver, sharesToRedeem, assetToRepay, redeemData);
        } else {
            address migrateTo = _isMigrate(receiver) ? receiver : address(0);
            uint256 assetsWithdrawn = _redeemShares(onBehalf, vault, asset, migrateTo, sharesToRedeem, redeemData);
            if (0 < assetsWithdrawn) ERC20(asset).safeTransfer(receiver, assetsWithdrawn);
        }

        if (balanceOfCollateral(onBehalf, vault) == 0) {
            ADDRESS_REGISTRY.clearPosition(onBehalf, vault);
        }
    }

    /// @inheritdoc ILendingRouter
    function liquidate(
        address liquidateAccount,
        address vault,
        uint256 sharesToLiquidate,
        uint256 debtToRepay
    ) external override returns (uint256 sharesToLiquidator) {
        if (sharesToLiquidate == 0) revert CannotLiquidateZeroShares();

        address liquidator = msg.sender;
        VaultPosition memory position = ADDRESS_REGISTRY.getVaultPosition(liquidator, vault);
        // If the liquidator has a position then they cannot liquidate or they will have
        // a native balance and a balance on the lending market.
        if (position.lendingRouter != address(0)) revert LiquidatorHasPosition();

        uint256 balanceBefore = balanceOfCollateral(liquidateAccount, vault);
        if (balanceBefore == 0) revert InsufficientSharesHeld();

        // Runs any checks on the vault to ensure that the liquidation can proceed, whitelists the lending platform
        // to transfer collateral to the lending router. The current account is set in this method.
        IYieldStrategy(vault).preLiquidation(liquidator, liquidateAccount, sharesToLiquidate, balanceBefore);

        // After this call, address(this) will have the liquidated shares
        sharesToLiquidator = _liquidate(liquidator, vault, liquidateAccount, sharesToLiquidate, debtToRepay);

        // Transfers the shares to the liquidator from the lending router and does any post liquidation logic. The
        // current account is cleared in this method.
        IYieldStrategy(vault).postLiquidation(liquidator, liquidateAccount, sharesToLiquidator);

        // The liquidator will receive shares in their native balance and then they can call redeem
        // on the yield strategy to get the assets.

        // Clear the position if the liquidator has taken all the shares, in the case of an insolvency,
        // the account's position will just be left on the lending market with zero collateral. The account
        // would be able to create a new position on this lending router or a new position on a different
        // lending router. If they do create a new position on an insolvent account their old debt may
        // be applied to their new position.
        if (sharesToLiquidator == balanceBefore) ADDRESS_REGISTRY.clearPosition(liquidateAccount, vault);
    }

    /// @inheritdoc ILendingRouter
    function initiateWithdraw(
        address onBehalf,
        address vault,
        bytes calldata data
    ) external override isAuthorized(onBehalf, vault) returns (uint256 requestId) {
        requestId = _initiateWithdraw(vault, onBehalf, data);
    }

    /// @inheritdoc ILendingRouter
    function forceWithdraw(address account, address vault, bytes calldata data) external returns (uint256 requestId) {
        // Can only force a withdraw if health factor is negative, this allows a liquidator to
        // force a withdraw and liquidate a position at a later time.
        (uint256 borrowed, /* */, uint256 maxBorrow) = healthFactor(account, vault);
        if (borrowed <= maxBorrow) revert CannotForceWithdraw(account);

        requestId = _initiateWithdraw(vault, account, data);

        // Clear the current account since this method is not called using isAuthorized
        IYieldStrategy(vault).clearCurrentAccount();
    }

    /// @inheritdoc ILendingRouter
    function claimRewards(address vault) external returns (uint256[] memory rewards) {
        return RewardManagerMixin(vault).claimAccountRewards(msg.sender, balanceOfCollateral(msg.sender, vault));
    }

    /// @inheritdoc ILendingRouter
    function healthFactor(address borrower, address vault) public override virtual returns (uint256 borrowed, uint256 collateralValue, uint256 maxBorrow);

    /// @inheritdoc ILendingRouter
    function balanceOfCollateral(address account, address vault) public override view virtual returns (uint256 collateralBalance);


    /*** Internal Methods ***/

    function _checkExit(address onBehalf, address vault) internal view  {
        VaultPosition memory position = ADDRESS_REGISTRY.getVaultPosition(onBehalf, vault);
        if (position.lendingRouter != address(this)) revert NoExistingPosition();
        if (block.timestamp - position.lastEntryTime < COOLDOWN_PERIOD) {
            revert CannotExitPositionWithinCooldownPeriod();
        }
    }

    /// @dev Checks if an exitPosition call is a migration, this would be called via a lending router
    function _isMigrate(address receiver) internal view returns (bool) {
        return receiver == msg.sender && ADDRESS_REGISTRY.isLendingRouter(msg.sender);
    }

    /// @dev Enters a position or migrates shares from a previous lending router
    function _enterOrMigrate(
        address onBehalf,
        address vault,
        address asset,
        uint256 assetAmount,
        bytes memory depositData,
        address migrateFrom
    ) internal returns (uint256 sharesReceived) {
        if (migrateFrom != address(0)) {
            // Allow the previous lending router to repay the debt from assets held here.
            ERC20(asset).checkApprove(migrateFrom, assetAmount);
            sharesReceived = ILendingRouter(migrateFrom).balanceOfCollateral(onBehalf, vault);

            // Must migrate the entire position
            ILendingRouter(migrateFrom).exitPosition(
                onBehalf, vault, address(this), sharesReceived, type(uint256).max, bytes("")
            );
        } else {
            ERC20(asset).approve(vault, assetAmount);
            sharesReceived = IYieldStrategy(vault).mintShares(assetAmount, onBehalf, depositData);
        }

        _supplyCollateral(onBehalf, vault, asset, sharesReceived);
    }

    /// @dev Redeems or withdraws shares from the lending market, handles migration
    function _redeemShares(
        address sharesOwner,
        address vault,
        address asset,
        address migrateTo,
        uint256 sharesToRedeem,
        bytes memory redeemData
    ) internal returns (uint256 assetsWithdrawn) {
        address receiver = migrateTo == address(0) ? sharesOwner : migrateTo;
        uint256 sharesHeld = balanceOfCollateral(sharesOwner, vault);

        // Allows the transfer from the lending market to the sharesOwner
        IYieldStrategy(vault).allowTransfer(receiver, sharesToRedeem, sharesOwner);
        _withdrawCollateral(vault, asset, sharesToRedeem, sharesOwner, receiver);

        // If we are not migrating then burn the shares
        if (migrateTo == address(0)) {
            assetsWithdrawn = IYieldStrategy(vault).burnShares(
                sharesOwner, sharesToRedeem, sharesHeld, redeemData
            );
        }
    }

    /// @dev Initiates a withdraw request for the vault shares held by the account
    function _initiateWithdraw(
        address vault,
        address account,
        bytes calldata data
    ) internal returns (uint256 requestId) {
        uint256 sharesHeld = balanceOfCollateral(account, vault);
        if (sharesHeld == 0) revert InsufficientSharesHeld();
        return IYieldStrategy(vault).initiateWithdraw(account, sharesHeld, data);
    }

    /*** Virtual Methods (lending market specific) ***/

    /// @dev Flash borrows the assets and enters a position
    function _flashBorrowAndEnter(
        address onBehalf,
        address vault,
        address asset,
        uint256 depositAssetAmount,
        uint256 borrowAmount,
        bytes memory depositData,
        address migrateFrom
    ) internal virtual;

    /// @dev Supplies collateral in the amount of shares received to the lending market
    function _supplyCollateral(
        address onBehalf, address vault, address asset, uint256 sharesReceived
    ) internal virtual;

    /// @dev Withdraws collateral from the lending market
    function _withdrawCollateral(
        address vault,
        address asset,
        uint256 sharesToRedeem,
        address sharesOwner,
        address receiver
    ) internal virtual;

    /// @dev Liquidates a position on the lending market
    function _liquidate(
        address liquidator,
        address vault,
        address liquidateAccount,
        uint256 sharesToLiquidate,
        uint256 debtToRepay
    ) internal virtual returns (uint256 sharesToLiquidator);

    /// @dev Exits a position with a debt repayment
    function _exitWithRepay(
        address onBehalf,
        address vault,
        address asset,
        address receiver,
        uint256 sharesToRedeem,
        uint256 assetToRepay,
        bytes calldata redeemData
    ) internal virtual;

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

import {ILendingRouter} from "../interfaces/ILendingRouter.sol";
import {InsufficientAssetsForRepayment} from "../interfaces/Errors.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {TokenUtils} from "../utils/TokenUtils.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IYieldStrategy} from "../interfaces/IYieldStrategy.sol";
import {IMorphoLiquidateCallback, IMorphoFlashLoanCallback, IMorphoRepayCallback} from "../interfaces/Morpho/IMorphoCallbacks.sol";
import {ADDRESS_REGISTRY} from "../utils/Constants.sol";
import {AbstractLendingRouter} from "./AbstractLendingRouter.sol";
import {
    MORPHO,
    MarketParams,
    Id,
    Position,
    Market,
    Withdrawal,
    PUBLIC_ALLOCATOR
} from "../interfaces/Morpho/IMorpho.sol";

struct MorphoParams {
    address irm;
    uint256 lltv;
}

struct MorphoAllocation {
    address vault;
    uint256 feeAmount;
    Withdrawal[] withdrawals;
}

contract MorphoLendingRouter is AbstractLendingRouter, IMorphoLiquidateCallback, IMorphoFlashLoanCallback, IMorphoRepayCallback {
    using SafeERC20 for ERC20;
    using TokenUtils for ERC20;

    mapping(address vault => MorphoParams params) private s_morphoParams;

    function initializeMarket(address vault, address irm, uint256 lltv) external {
        require(ADDRESS_REGISTRY.upgradeAdmin() == msg.sender);
        // Cannot override parameters once they are set
        require(s_morphoParams[vault].irm == address(0));
        require(s_morphoParams[vault].lltv == 0);

        s_morphoParams[vault] = MorphoParams({
            irm: irm,
            lltv: lltv
        });

        MORPHO.createMarket(marketParams(vault));
    }

    function marketParams(address vault) public view returns (MarketParams memory) {
        return marketParams(vault, IYieldStrategy(vault).asset());
    }

    function marketParams(address vault, address asset) internal view returns (MarketParams memory) {
        MorphoParams memory params = s_morphoParams[vault];

        return MarketParams({
            loanToken: asset,
            collateralToken: vault,
            oracle: vault,
            irm: params.irm,
            lltv: params.lltv
        });
    }

    function morphoId(MarketParams memory m) internal pure returns (Id) {
        return Id.wrap(keccak256(abi.encode(m)));
    }

    /// @dev Allows integration with the public allocator so that accounts can
    /// ensure there is sufficient liquidity in the lending market before entering
    function _allocate(address vault, MorphoAllocation[] calldata allocationData) internal {
        MarketParams memory m = marketParams(vault);

        uint256 totalFeeAmount;
        for (uint256 i = 0; i < allocationData.length; i++) {
            PUBLIC_ALLOCATOR.reallocateTo{value: allocationData[i].feeAmount}(
                allocationData[i].vault, allocationData[i].withdrawals, m
            );
            totalFeeAmount += allocationData[i].feeAmount;
        }
        require(msg.value == totalFeeAmount, "Insufficient fee amount");
    }

    function allocateAndEnterPosition(
        address onBehalf,
        address vault,
        uint256 depositAssetAmount,
        uint256 borrowAmount,
        bytes calldata depositData,
        MorphoAllocation[] calldata allocationData
    ) external payable isAuthorized(onBehalf, vault) {
        _allocate(vault, allocationData);
        enterPosition(onBehalf, vault, depositAssetAmount, borrowAmount, depositData);
    }

    function allocateAndMigratePosition(
        address onBehalf,
        address vault,
        address migrateFrom,
        MorphoAllocation[] calldata allocationData
    ) external payable isAuthorized(onBehalf, vault) {
        _allocate(vault, allocationData);
        migratePosition(onBehalf, vault, migrateFrom);
    }

    function _flashBorrowAndEnter(
        address onBehalf,
        address vault,
        address asset,
        uint256 depositAssetAmount,
        uint256 borrowAmount,
        bytes memory depositData,
        address migrateFrom
    ) internal override {
        // At this point we will flash borrow funds from the lending market and then
        // receive control in a different function on a callback.
        bytes memory flashLoanData = abi.encode(
            onBehalf, vault, asset, depositAssetAmount, depositData, migrateFrom
        );
        MORPHO.flashLoan(asset, borrowAmount, flashLoanData);
    }

    function onMorphoFlashLoan(uint256 assets, bytes calldata data) external override {
        require(msg.sender == address(MORPHO));

        (
            address onBehalf,
            address vault,
            address asset,
            uint256 depositAssetAmount,
            bytes memory depositData,
            address migrateFrom
        ) = abi.decode(data, (address, address, address, uint256, bytes, address));

        _enterOrMigrate(onBehalf, vault, asset, assets + depositAssetAmount, depositData, migrateFrom);

        MarketParams memory m = marketParams(vault, asset);
        // Borrow the assets in order to repay the flash loan
        MORPHO.borrow(m, assets, 0, onBehalf, address(this));

        // Allow for flash loan to be repaid
        ERC20(asset).checkApprove(address(MORPHO), assets);
    }

    function _supplyCollateral(
        address onBehalf,
        address vault,
        address asset,
        uint256 sharesReceived
    ) internal override {
        MarketParams memory m = marketParams(vault, asset);

        // Allows the transfer from the lending market to the Morpho contract
        IYieldStrategy(vault).allowTransfer(address(MORPHO), sharesReceived, onBehalf);

        // We should receive shares in return
        ERC20(vault).approve(address(MORPHO), sharesReceived);
        MORPHO.supplyCollateral(m, sharesReceived, onBehalf, "");
    }

    function _withdrawCollateral(
        address vault,
        address asset,
        uint256 sharesToRedeem,
        address sharesOwner,
        address receiver
    ) internal override {
        MarketParams memory m = marketParams(vault, asset);
        MORPHO.withdrawCollateral(m, sharesToRedeem, sharesOwner, receiver);
    }

    function _exitWithRepay(
        address onBehalf,
        address vault,
        address asset,
        address receiver,
        uint256 sharesToRedeem,
        uint256 assetToRepay,
        bytes calldata redeemData
    ) internal override {
        MarketParams memory m = marketParams(vault, asset);

        uint256 sharesToRepay;
        if (assetToRepay == type(uint256).max) {
            // If assetToRepay is uint256.max then get the morpho borrow shares amount to
            // get a full exit.
            sharesToRepay = MORPHO.position(morphoId(m), onBehalf).borrowShares;
            assetToRepay = 0;
        }

        bytes memory repayData = abi.encode(
            onBehalf, vault, asset, receiver, sharesToRedeem, redeemData, _isMigrate(receiver)
        );

        // Will trigger a callback to onMorphoRepay
        MORPHO.repay(m, assetToRepay, sharesToRepay, onBehalf, repayData);
    }

    function onMorphoRepay(uint256 assetToRepay, bytes calldata data) external override {
        require(msg.sender == address(MORPHO));

        (
            address sharesOwner,
            address vault,
            address asset,
            address receiver,
            uint256 sharesToRedeem,
            bytes memory redeemData,
            bool isMigrate
        ) = abi.decode(data, (address, address, address, address, uint256, bytes, bool));

        uint256 assetsWithdrawn = _redeemShares(
            sharesOwner, vault, asset, isMigrate ? receiver : address(0), sharesToRedeem, redeemData
        );

        if (isMigrate) {
            // When migrating we do not withdraw any assets and we must repay the entire debt
            // from the previous lending router.
            ERC20(asset).safeTransferFrom(receiver, address(this), assetToRepay);
            assetsWithdrawn = assetToRepay;
        }

        // Transfer any profits to the receiver
        if (assetsWithdrawn < assetToRepay) {
            // We have to revert in this case because we've already redeemed the yield tokens
            revert InsufficientAssetsForRepayment(assetToRepay, assetsWithdrawn);
        }

        uint256 profitsWithdrawn;
        unchecked {
            profitsWithdrawn = assetsWithdrawn - assetToRepay;
        }
        ERC20(asset).safeTransfer(receiver, profitsWithdrawn);

        // Allow morpho to repay the debt
        ERC20(asset).checkApprove(address(MORPHO), assetToRepay);
    }

    function _liquidate(
        address liquidator,
        address vault,
        address liquidateAccount,
        uint256 sharesToLiquidate,
        uint256 debtToRepay
    ) internal override returns (uint256 sharesToLiquidator) {
        MarketParams memory m = marketParams(vault);
        (sharesToLiquidator, /* */) = MORPHO.liquidate(
            m, liquidateAccount, sharesToLiquidate, debtToRepay, abi.encode(m.loanToken, liquidator)
        );
    }

    function onMorphoLiquidate(uint256 repaidAssets, bytes calldata data) external override {
        require(msg.sender == address(MORPHO));
        (address asset, address liquidator) = abi.decode(data, (address, address));

        ERC20(asset).safeTransferFrom(liquidator, address(this), repaidAssets);
        ERC20(asset).checkApprove(address(MORPHO), repaidAssets);
    }

    function balanceOfCollateral(address account, address vault) public view override returns (uint256 collateralBalance) {
        MarketParams memory m = marketParams(vault);
        collateralBalance = MORPHO.position(morphoId(m), account).collateral;
    }

    function healthFactor(address borrower, address vault) public override returns (uint256 borrowed, uint256 collateralValue, uint256 maxBorrow) {
        MarketParams memory m = marketParams(vault);
        Id id = morphoId(m);
        // Ensure interest is accrued before calculating health factor
        MORPHO.accrueInterest(m);
        Position memory position = MORPHO.position(id, borrower);
        Market memory market = MORPHO.market(id);

        if (position.borrowShares > 0) {
            borrowed = (uint256(position.borrowShares) * uint256(market.totalBorrowAssets)) / uint256(market.totalBorrowShares);
        } else {
            borrowed = 0;
        }
        collateralValue = (uint256(position.collateral) * IYieldStrategy(vault).price(borrower)) / 1e36;
        maxBorrow = collateralValue * m.lltv / 1e18;
    }

}

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

import {WithdrawRequestNotFinalized} from "../interfaces/Errors.sol";
import {AbstractYieldStrategy} from "../AbstractYieldStrategy.sol";
import {
    IWithdrawRequestManager,
    WithdrawRequest,
    TokenizedWithdrawRequest
} from "../interfaces/IWithdrawRequestManager.sol";
import {ADDRESS_REGISTRY} from "../utils/Constants.sol";
import {Trade, TradeType, TRADING_MODULE} from "../interfaces/ITradingModule.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

struct RedeemParams {
    uint8 dexId;
    uint256 minPurchaseAmount;
    bytes exchangeData;
}

struct DepositParams {
    uint8 dexId;
    uint256 minPurchaseAmount;
    bytes exchangeData;
}

/**
 * Supports vaults that borrow a token and stake it into a token that earns yield but may
 * require some illiquid redemption period.
 */
abstract contract AbstractStakingStrategy is AbstractYieldStrategy {
    using SafeERC20 for ERC20;

    address internal immutable withdrawToken;

    constructor(
        address _asset,
        address _yieldToken,
        uint256 _feeRate,
        IWithdrawRequestManager _withdrawRequestManager
    ) AbstractYieldStrategy(_asset, _yieldToken, _feeRate, ERC20(_yieldToken).decimals()) {
        // For Pendle PT the yield token does not define the withdraw request manager,
        // it is the token out sy
        withdrawRequestManager = _withdrawRequestManager;
        withdrawToken = address(withdrawRequestManager) != address(0) ? withdrawRequestManager.WITHDRAW_TOKEN() : address(0);
    }

    /// @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)) {
            (bool hasRequest, uint256 value) = withdrawRequestManager.getWithdrawRequestValue(
                address(this), t_CurrentAccount, asset, shares
            );

            // If the account does not have a withdraw request then this will fall through
            // to the super implementation.
            if (hasRequest) return value;
        }

        return super.convertToAssets(shares);
    }

    function _initiateWithdraw(
        address account,
        uint256 yieldTokenAmount,
        uint256 sharesHeld,
        bytes memory data
    ) internal override virtual returns (uint256 requestId) {
        ERC20(yieldToken).approve(address(withdrawRequestManager), yieldTokenAmount);
        requestId = withdrawRequestManager.initiateWithdraw({
            account: account, yieldTokenAmount: yieldTokenAmount, sharesAmount: sharesHeld, data: data
        });
    }

    /// @dev By default we can use the withdraw request manager to stake the tokens
    function _mintYieldTokens(uint256 assets, address /* receiver */, bytes memory depositData) internal override virtual {
        ERC20(asset).approve(address(withdrawRequestManager), assets);
        withdrawRequestManager.stakeTokens(address(asset), assets, depositData);
    }

    function _redeemShares(
        uint256 sharesToRedeem,
        address sharesOwner,
        bool isEscrowed,
        bytes memory redeemData
    ) internal override {
        if (isEscrowed) {
            (WithdrawRequest memory w, /* */) = withdrawRequestManager.getWithdrawRequest(address(this), sharesOwner);
            uint256 yieldTokensBurned = uint256(w.yieldTokenAmount) * sharesToRedeem / w.sharesAmount;

            (uint256 tokensClaimed, bool finalized) = withdrawRequestManager.finalizeAndRedeemWithdrawRequest({
                account: sharesOwner, withdrawYieldTokenAmount: yieldTokensBurned, sharesToBurn: sharesToRedeem
            });
            if (!finalized) revert WithdrawRequestNotFinalized(w.requestId);

            // Trades may be required here if the borrowed token is not the same as what is
            // received when redeeming.
            if (asset != withdrawToken) {
                RedeemParams memory params = abi.decode(redeemData, (RedeemParams));
                Trade memory trade = Trade({
                    tradeType: TradeType.EXACT_IN_SINGLE,
                    sellToken: address(withdrawToken),
                    buyToken: address(asset),
                    amount: tokensClaimed,
                    limit: params.minPurchaseAmount,
                    deadline: block.timestamp,
                    exchangeData: params.exchangeData
                });

                _executeTrade(trade, params.dexId);
            }
        } else {
            uint256 yieldTokensBurned = convertSharesToYieldToken(sharesToRedeem);
            _executeInstantRedemption(yieldTokensBurned, redeemData);
        }
    }

    /// @notice Default implementation for an instant redemption is to sell the staking token to the
    /// borrow token through the trading module. Can be overridden if required for different implementations.
    function _executeInstantRedemption(
        uint256 yieldTokensToRedeem,
        bytes memory redeemData
    ) internal virtual returns (uint256 assetsPurchased) {
        RedeemParams memory params = abi.decode(redeemData, (RedeemParams));
        Trade memory trade = Trade({
            tradeType: TradeType.EXACT_IN_SINGLE,
            sellToken: address(yieldToken),
            buyToken: address(asset),
            amount: yieldTokensToRedeem,
            limit: params.minPurchaseAmount,
            deadline: block.timestamp,
            exchangeData: params.exchangeData
        });

        // Executes a trade on the given Dex, the vault must have permissions set for
        // each dex and token it wants to sell.
        (/* */, assetsPurchased) = _executeTrade(trade, params.dexId);
    }
    
    function _preLiquidation(address, address, uint256, uint256) internal override { /* no-op */ }

    function _postLiquidation(address liquidator, address liquidateAccount, uint256 sharesToLiquidator) internal override returns (bool didTokenize) {
        if (address(withdrawRequestManager) != address(0)) {
            // No need to accrue fees because neither the total supply or total yield token balance is changing. If there
            // is no withdraw request then this will be a noop.
            didTokenize = withdrawRequestManager.tokenizeWithdrawRequest(liquidateAccount, liquidator, sharesToLiquidator);
        }
    }

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

import {AbstractWithdrawRequestManager} from "./AbstractWithdrawRequestManager.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/// @dev Used for ERC4626s that can be staked and unstaked on demand without any additional
/// time constraints.
contract GenericERC4626WithdrawRequestManager is AbstractWithdrawRequestManager {

    uint256 private currentRequestId;
    mapping(uint256 => uint256) private s_withdrawRequestShares;

    constructor(address _erc4626)
        AbstractWithdrawRequestManager(IERC4626(_erc4626).asset(), _erc4626, IERC4626(_erc4626).asset()) { }

    function _initiateWithdrawImpl(
        address /* account */,
        uint256 sharesToWithdraw,
        bytes calldata /* data */
    ) override internal returns (uint256 requestId) {
        requestId = ++currentRequestId;
        s_withdrawRequestShares[requestId] = sharesToWithdraw;
    }

    function _stakeTokens(uint256 amount, bytes memory /* stakeData */) internal override {
        ERC20(STAKING_TOKEN).approve(address(YIELD_TOKEN), amount);
        IERC4626(YIELD_TOKEN).deposit(amount, address(this));
    }

    function _finalizeWithdrawImpl(
        address /* account */,
        uint256 requestId
    ) internal override returns (uint256 tokensClaimed, bool finalized) {
        uint256 sharesToRedeem = s_withdrawRequestShares[requestId];
        delete s_withdrawRequestShares[requestId];
        tokensClaimed = IERC4626(YIELD_TOKEN).redeem(sharesToRedeem, address(this), address(this));
        finalized = true;
    }

    function canFinalizeWithdrawRequest(uint256 /* requestId */) public pure override returns (bool) {
        return true;
    }
}

Vulnerability type: CWE-703|CWE-754|CWE-394