ConcreteStandardVaultImpl

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/implementation/ConcreteStandardVaultImpl.sol": {
      "content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.24;

/**
 * @title ConcreteStandardVaultImpl
 * @notice ERC-4626 upgradeable standard vault implementation for the Concrete Earn V2 protocol.
 *         Holds an underlying ERC20 asset and exposes deposit/mint/withdraw/redeem flows.
 *         Integrates with strategy modules to route assets to yield sources.
 *
 * @author Blueprint Finance
 * @custom:protocol Concrete Earn V2
 * @custom:oz-upgrades Use OZ Upgradeable patterns and eip7201 storage layout
 * @custom:source on request
 * @custom:audits on request
 * @custom:license AGPL-3.0
 */

// ─────────────────────────────────────────────────────────────────────────────
// External dependencies
// ─────────────────────────────────────────────────────────────────────────────
import {
    AccessControlEnumerableUpgradeable
} from "@openzeppelin-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
import {
    ERC4626Upgradeable,
    IERC20,
    IERC4626,
    Math,
    SafeERC20
} from "@openzeppelin-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {Address} from "@openzeppelin-contracts/utils/Address.sol";
import {EnumerableSet} from "@openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import {Ownable} from "@openzeppelin-contracts/access/Ownable.sol";
import {SafeCast} from "@openzeppelin-contracts/utils/math/SafeCast.sol";

// ─────────────────────────────────────────────────────────────────────────────
// Protocol-facing interfaces
// ─────────────────────────────────────────────────────────────────────────────
import {IConcreteStandardVaultImpl} from "../interface/IConcreteStandardVaultImpl.sol";
import {IStrategyTemplate} from "../interface/IStrategyTemplate.sol";

// ─────────────────────────────────────────────────────────────────────────────
// Internal modules/contracts
// ─────────────────────────────────────────────────────────────────────────────
import {IAllocateModule} from "../module/AllocateModule.sol";
import {UpgradeableVault} from "../common/UpgradeableVault.sol";

// ─────────────────────────────────────────────────────────────────────────────
// Internal libraries
// ─────────────────────────────────────────────────────────────────────────────
import {ConcreteV2ConstantsLib as ConstantsLib} from "../lib/Constants.sol";
import {ConcreteV2ConversionLib as ConversionLib} from "../lib/Conversion.sol";
import {Hooks, HooksLibV1 as HooksLib} from "../lib/Hooks.sol";
import {ConcreteV2RolesLib as RolesLib} from "../lib/Roles.sol";
import {StateInitLib} from "../lib/StateInitLib.sol";
import {StateSetterLib} from "../lib/StateSetterLib.sol";
import {Time} from "../lib/Time.sol";

// ─────────────────────────────────────────────────────────────────────────────
// Storage layout libraries
// ─────────────────────────────────────────────────────────────────────────────
import {
    ConcreteCachedVaultStateStorageLib as CachedVaultStateLib
} from "../lib/storage/ConcreteCachedVaultStateStorageLib.sol";
import {ConcreteStandardVaultImplStorageLib as SVLib} from "../lib/storage/ConcreteStandardVaultImplStorageLib.sol";

contract ConcreteStandardVaultImpl is
    ERC4626Upgradeable,
    UpgradeableVault,
    AccessControlEnumerableUpgradeable,
    IConcreteStandardVaultImpl
{
    using Address for address;
    using EnumerableSet for EnumerableSet.AddressSet;
    using SafeCast for uint256;
    using ConversionLib for uint256;

    using HooksLib for Hooks;

    event HooksSet(Hooks hooks);

    /// @dev Modifier to accrue yield before function execution
    modifier withYieldAccrual() {
        _accrueYield();
        _;
    }
    /**
     * @dev Constructor
     * @param factory The address of the factory
     */

    constructor(address factory) UpgradeableVault(factory) {}

    /// @inheritdoc IConcreteStandardVaultImpl
    function allocate(bytes calldata data) public virtual nonReentrant onlyRole(RolesLib.ALLOCATOR) withYieldAccrual {
        // delegatecall allocate module
        bytes memory delegateData = abi.encodeWithSelector(IAllocateModule.allocateFunds.selector, data);
        allocateModule().functionDelegateCall(delegateData);
        require(IERC20(asset()).balanceOf(address(this)) >= _lockedAssets(), InsufficientBalance());
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function accrueYield() external virtual nonReentrant {
        _accrueYield();
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function updateManagementFee(uint16 managementFee_)
        external
        nonReentrant
        onlyRole(RolesLib.VAULT_MANAGER)
        withYieldAccrual
    {
        StateSetterLib.updateManagementFee(managementFee_);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function updateManagementFeeRecipient(address recipient) external nonReentrant withYieldAccrual {
        require(_msgSender() == Ownable(factory).owner(), InvalidFactoryOwner());

        StateSetterLib.updateManagementFeeRecipient(recipient);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function updatePerformanceFee(uint16 performanceFee_)
        external
        nonReentrant
        onlyRole(RolesLib.VAULT_MANAGER)
        withYieldAccrual
    {
        StateSetterLib.updatePerformanceFee(performanceFee_);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function updatePerformanceFeeRecipient(address recipient) external nonReentrant withYieldAccrual {
        require(msg.sender == Ownable(factory).owner(), InvalidFactoryOwner());

        StateSetterLib.updatePerformanceFeeRecipient(recipient);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function setDepositLimits(uint256 minDepositAmount, uint256 maxDepositAmount)
        external
        nonReentrant
        onlyRole(RolesLib.VAULT_MANAGER)
    {
        StateSetterLib.setDepositLimits(minDepositAmount, maxDepositAmount);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function setWithdrawLimits(uint256 minWithdrawAmount, uint256 maxWithdrawAmount)
        external
        nonReentrant
        onlyRole(RolesLib.VAULT_MANAGER)
    {
        StateSetterLib.setWithdrawLimits(minWithdrawAmount, maxWithdrawAmount);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function getFeeConfig()
        external
        view
        override
        returns (
            uint16 currentManagementFee,
            address currentManagementFeeRecipient,
            uint32 currentLastManagementFeeAccrual,
            uint16 currentPerformanceFee,
            address currentPerformanceFeeRecipient
        )
    {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        return (
            $.managementFee,
            $.managementFeeRecipient,
            $.lastManagementFeeAccrual,
            $.performanceFee,
            $.performanceFeeRecipient
        );
    }

    /**
     * @inheritdoc IERC4626
     */
    function deposit(uint256 assets, address receiver)
        public
        virtual
        override(IERC4626, ERC4626Upgradeable)
        nonReentrant
        withYieldAccrual
        returns (uint256)
    {
        require(receiver != address(0), InvalidReceiver());

        Hooks memory h = SVLib.fetch().hooks;
        uint256 totalAssetsBeforeDeposit = cachedTotalAssets();
        // invoke pre-deposit hook if enabled
        if (h.checkIsValid(HooksLib.PRE_DEPOSIT)) {
            h.preDeposit(_msgSender(), assets, receiver, totalAssetsBeforeDeposit);
        }

        // deposit assets
        uint256 maxAssets = maxDeposit(receiver);
        if (assets > maxAssets) {
            revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);
        }

        uint256 shares = assets.calcConvertToShares(totalSupply(), totalAssetsBeforeDeposit, Math.Rounding.Floor, true);

        (uint256 maxDepositAmount, uint256 minDepositAmount) = getDepositLimits();
        require(
            assets + totalAssetsBeforeDeposit <= maxDepositAmount && assets >= minDepositAmount,
            AssetAmountOutOfBounds(msg.sender, assets, minDepositAmount, maxDepositAmount)
        );

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

        // invoke post-deposit hook if enabled
        if (h.checkIsValid(HooksLib.POST_DEPOSIT)) {
            h.postDeposit(_msgSender(), assets, shares, receiver, cachedTotalAssets());
        }

        return shares;
    }

    /**
     * @inheritdoc IERC4626
     */
    function mint(uint256 shares, address receiver)
        public
        virtual
        override(IERC4626, ERC4626Upgradeable)
        nonReentrant
        withYieldAccrual
        returns (uint256)
    {
        require(receiver != address(0), InvalidReceiver());

        Hooks memory h = SVLib.fetch().hooks;
        uint256 totalAssetsBeforeDeposit = cachedTotalAssets();
        // invoke pre-mint hook if enabled
        if (h.checkIsValid(HooksLib.PRE_MINT)) h.preMint(_msgSender(), shares, receiver, totalAssetsBeforeDeposit);

        uint256 maxShares = maxMint(receiver);
        if (shares > maxShares) {
            revert ERC4626ExceededMaxMint(receiver, shares, maxShares);
        }

        uint256 assets = shares.calcConvertToAssets(totalSupply(), totalAssetsBeforeDeposit, Math.Rounding.Ceil, true);

        (uint256 maxDepositAmount, uint256 minDepositAmount) = getDepositLimits();
        require(
            assets + totalAssetsBeforeDeposit <= maxDepositAmount && assets >= minDepositAmount,
            AssetAmountOutOfBounds(_msgSender(), assets, minDepositAmount, maxDepositAmount)
        );

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

        // invoke post-mint hook if enabled
        if (h.checkIsValid(HooksLib.POST_MINT)) {
            h.postMint(_msgSender(), assets, shares, receiver, cachedTotalAssets());
        }

        return assets;
    }

    /**
     * @inheritdoc IERC4626
     */
    function withdraw(uint256 assets, address receiver, address owner)
        public
        virtual
        override(IERC4626, ERC4626Upgradeable)
        nonReentrant
        withYieldAccrual
        returns (uint256)
    {
        require(receiver != address(0), InvalidReceiver());

        Hooks memory h = SVLib.fetch().hooks;
        uint256 totalAssetsBeforeWithdrawal = cachedTotalAssets();

        if (h.checkIsValid(HooksLib.PRE_WITHDRAW)) {
            h.preWithdraw(_msgSender(), assets, receiver, owner, totalAssetsBeforeWithdrawal);
        }
        // Optimistic maxAssets that does not account for the withdrawals from strategies, this is to avoid the need to call _simulateWithdraw().
        // If maxAssets is greater than the actual withdrawable amount, _executeWithdraw() will revert.
        uint256 totalSupply_ = totalSupply();
        uint256 maxAssets =
            balanceOf(owner).calcConvertToAssets(totalSupply_, totalAssetsBeforeWithdrawal, Math.Rounding.Floor, false);

        uint256 shares = assets.calcConvertToShares(totalSupply_, totalAssetsBeforeWithdrawal, Math.Rounding.Ceil, true);
        if (assets > maxAssets || _executeWithdraw(_msgSender(), receiver, owner, assets, shares) < assets) {
            revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
        }

        (uint256 maxWithdrawAmount, uint256 minWithdrawAmount) = getWithdrawLimits();
        require(
            assets <= maxWithdrawAmount && assets >= minWithdrawAmount,
            AssetAmountOutOfBounds(_msgSender(), assets, minWithdrawAmount, maxWithdrawAmount)
        );

        // invoke post-withdraw hook if enabled
        if (h.checkIsValid(HooksLib.POST_WITHDRAW)) {
            h.postWithdraw(_msgSender(), assets, shares, receiver, cachedTotalAssets());
        }

        return shares;
    }

    /**
     * @inheritdoc IERC4626
     */
    function redeem(uint256 shares, address receiver, address owner)
        public
        virtual
        override(IERC4626, ERC4626Upgradeable)
        nonReentrant
        withYieldAccrual
        returns (uint256)
    {
        require(receiver != address(0), InvalidReceiver());

        Hooks memory h = SVLib.fetch().hooks;
        uint256 totalAssetsBeforeWithdrawal = cachedTotalAssets();

        // invoke pre-redeem hook if enabled
        if (h.checkIsValid(HooksLib.PRE_REDEEM)) {
            h.preRedeem(_msgSender(), shares, receiver, owner, totalAssetsBeforeWithdrawal);
        }

        // Optimistic maxShares that does not account for the withdrawals from strategies, this is to avoid the need to call _simulateWithdraw().
        // If maxShares is greater than the actual redeemable amount, _executeWithdraw() will revert.
        uint256 maxShares = balanceOf(owner);

        uint256 assets =
            shares.calcConvertToAssets(totalSupply(), totalAssetsBeforeWithdrawal, Math.Rounding.Floor, true);

        if (shares > maxShares || _executeWithdraw(_msgSender(), receiver, owner, assets, shares) < assets) {
            revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
        }

        (uint256 maxWithdrawAmount, uint256 minWithdrawAmount) = getWithdrawLimits();
        require(
            assets <= maxWithdrawAmount && assets >= minWithdrawAmount,
            AssetAmountOutOfBounds(_msgSender(), assets, minWithdrawAmount, maxWithdrawAmount)
        );

        // invoke post-redeem hook if enabled
        if (h.checkIsValid(HooksLib.POST_REDEEM)) {
            h.postRedeem(_msgSender(), assets, shares, receiver, cachedTotalAssets());
        }

        return assets;
    }

    /**
     * @inheritdoc IConcreteStandardVaultImpl
     */
    function setHooks(Hooks memory hooks) external virtual nonReentrant onlyRole(RolesLib.HOOK_MANAGER) {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        $.hooks = hooks;

        emit HooksSet(hooks);
    }

    /**
     * @notice overwrites the deallocation order from strategies;
     */
    function setDeallocationOrder(address[] calldata order) external virtual nonReentrant onlyRole(RolesLib.ALLOCATOR) {
        StateSetterLib.setDeallocationOrder(order);
    }

    /**
     * @inheritdoc IConcreteStandardVaultImpl
     */
    function addStrategy(address strategy) public virtual nonReentrant onlyRole(RolesLib.STRATEGY_MANAGER) {
        require(IStrategyTemplate(strategy).asset() == asset(), InvalidStrategyAsset());
        StateSetterLib.addStrategy(strategy);
    }

    /**
     * @inheritdoc IConcreteStandardVaultImpl
     */
    function removeStrategy(address strategy) public virtual nonReentrant onlyRole(RolesLib.STRATEGY_MANAGER) {
        StateSetterLib.removeStrategy(strategy);
    }

    /**
     * @inheritdoc IConcreteStandardVaultImpl
     */
    function toggleStrategyStatus(address strategy) public virtual nonReentrant onlyRole(RolesLib.STRATEGY_MANAGER) {
        StateSetterLib.toggleStrategyStatus(strategy);
    }

    /**
     * @notice Returns the deallocation order from strategies.
     */
    function getDeallocationOrder() external view returns (address[] memory order) {
        return SVLib.fetch().deallocationOrder;
    }

    /**
     * @dev See {IERC4626-previewDeposit}.
     */
    function previewDeposit(uint256 assets)
        public
        view
        virtual
        override(IERC4626, ERC4626Upgradeable)
        returns (uint256)
    {
        (uint256 totalAssetsPreview, uint256 totalSupply) = _previewAccrueYieldAndFees();
        return assets.calcConvertToShares(totalSupply, totalAssetsPreview, Math.Rounding.Floor, false);
    }

    /**
     * @inheritdoc IERC4626
     */
    function previewMint(uint256 shares) public view virtual override(IERC4626, ERC4626Upgradeable) returns (uint256) {
        (uint256 totalAssetsPreview, uint256 totalSupply) = _previewAccrueYieldAndFees();

        return shares.calcConvertToAssets(totalSupply, totalAssetsPreview, Math.Rounding.Ceil, false);
    }

    /**
     * @inheritdoc IERC4626
     */
    function previewWithdraw(uint256 assets)
        public
        view
        virtual
        override(IERC4626, ERC4626Upgradeable)
        returns (uint256)
    {
        (uint256 totalAssetsPreview, uint256 totalSupply) = _previewAccrueYieldAndFees();

        return assets.calcConvertToShares(totalSupply, totalAssetsPreview, Math.Rounding.Ceil, false);
    }

    /**
     * @inheritdoc IERC4626
     */
    function previewRedeem(uint256 shares)
        public
        view
        virtual
        override(IERC4626, ERC4626Upgradeable)
        returns (uint256)
    {
        (uint256 totalAssetsPreview, uint256 totalSupply) = _previewAccrueYieldAndFees();

        return shares.calcConvertToAssets(totalSupply, totalAssetsPreview, Math.Rounding.Floor, false);
    }

    /**
     * @inheritdoc IConcreteStandardVaultImpl
     */
    function previewAccrueYield() public view virtual returns (uint256, uint256) {
        return _previewAccrueYieldAndFees();
    }

    /**
     * @inheritdoc IERC4626
     */
    function totalAssets()
        public
        view
        virtual
        override(IERC4626, ERC4626Upgradeable)
        returns (uint256 totalAssetsWithYield)
    {
        (totalAssetsWithYield,) = _previewAccrueYieldAndFees();
    }

    /**
     * @inheritdoc IERC4626
     */
    function maxRedeem(address owner) public view virtual override(IERC4626, ERC4626Upgradeable) returns (uint256) {
        (uint256 maxAssets, uint256 expectedTotalAssets, uint256 expectedTotalSupply) = _maxWithdraw(owner);

        return maxAssets.calcConvertToShares(expectedTotalSupply, expectedTotalAssets, Math.Rounding.Floor, false);
    }

    /**
     * @inheritdoc IERC4626
     */
    function maxWithdraw(address owner)
        public
        view
        virtual
        override(IERC4626, ERC4626Upgradeable)
        returns (uint256 maxAssets)
    {
        (maxAssets,,) = _maxWithdraw(owner);
    }

    /**
     * @inheritdoc IConcreteStandardVaultImpl
     */
    function getStrategyData(address strategy) public view returns (StrategyData memory) {
        return SVLib.fetch().strategyData[strategy];
    }

    /**
     * @inheritdoc IConcreteStandardVaultImpl
     */
    function getStrategies() public view returns (address[] memory) {
        return SVLib.fetch().strategies.values();
    }

    /**
     * @inheritdoc IConcreteStandardVaultImpl
     */
    function allocateModule() public view returns (address) {
        return SVLib.fetch().allocateModule;
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function getDepositLimits() public view returns (uint256, uint256) {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
        return ($.maxDepositAmount, $.minDepositAmount);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function getWithdrawLimits() public view returns (uint256, uint256) {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
        return ($.maxWithdrawAmount, $.minWithdrawAmount);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function managementFee() public view returns (address, uint16, uint32) {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
        return ($.managementFeeRecipient, $.managementFee, $.lastManagementFeeAccrual);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function performanceFee() public view returns (address, uint16) {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();
        return ($.performanceFeeRecipient, $.performanceFee);
    }

    /// @inheritdoc IConcreteStandardVaultImpl
    function getTotalAllocated() public view returns (uint256 totalAllocated) {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        address[] memory strategies = $.strategies.values();

        for (uint256 i = 0; i < strategies.length; i++) {
            totalAllocated += $.strategyData[strategies[i]].allocated;
        }
    }

    function cachedTotalAssets() public view returns (uint256) {
        return CachedVaultStateLib.fetch().cachedTotalAssets;
    }

    /**
     * @dev Initialization function that will be called when a proxy vault is deployed through `ConcreteFactory`.
     */
    function _initialize(
        uint64,
        /*initialVersion*/
        address,
        /*owner*/
        bytes memory data
    )
        internal
        virtual
        override
    {
        (
            address allocateModuleAddr,
            address asset,
            address initialVaultManager,
            string memory name,
            string memory symbol
        ) = abi.decode(data, (address, address, address, string, string));

        require(allocateModuleAddr != address(0), InvalidAllocateModule());
        require(asset != address(0), InvalidAsset());
        require(initialVaultManager != address(0), InvalidInitialVaultManager());

        require(bytes(name).length > 0, InvalidName());
        require(bytes(symbol).length > 0, InvalidSymbol());

        __ERC20_init_unchained(name, symbol);
        __ERC4626_init_unchained(IERC20(asset));
        __AccessControlEnumerable_init_unchained();

        StateInitLib.stateInitStandardVaultImpl(allocateModuleAddr, initialVaultManager, _msgSender());
    }

    /**
     * @dev Upgrade function that will be called when a proxy vault upgrades to this implementation
     */
    function _upgrade(
        uint64,
        /* oldVersion */
        uint64,
        /* newVersion */
        bytes calldata /* data */
    )
        internal
        virtual
        override
    {}

    /**
     * @dev Internal function that executes the yield accrual operation across all active strategies.
     * @dev This function iterates through all strategies managed by the vault, calculates
     *      yield generated and losses incurred since the last yield accrual, and updates the vault's
     *      internal accounting accordingly.
     * @dev This function does not trigger actual fund movements, it only updates accounting
     *      to reflect the current state of strategy allocations.
     */
    function _accrueYield() internal virtual returns (uint256) {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        uint256 totalPositiveYield;
        uint256 totalNegativeYield;
        {
            address[] memory strategies = $.strategies.values();
            uint256 strategiesCounter = strategies.length;

            for (uint256 i; i < strategiesCounter; ++i) {
                (uint256 positiveYield, uint256 loss, uint256 strategyTotalAllocatedValue) =
                    _previewStrategyYield(strategies[i]);

                // update the strategy allocated amount only if there is yield or loss, otherwise it's the same amount as when we called `allocate()`.
                // we do update the lastTotalAssets after netting the yield and loss
                if (positiveYield != 0 || loss != 0) {
                    $.strategyData[strategies[i]].allocated = strategyTotalAllocatedValue.toUint120();

                    totalPositiveYield += positiveYield;
                    totalNegativeYield += loss;

                    emit StrategyYieldAccrued(strategies[i], strategyTotalAllocatedValue, positiveYield, loss);
                }
            }
        }

        CachedVaultStateLib.ConcreteCachedVaultStateStorage storage $cached = CachedVaultStateLib.fetch();

        uint256 totalAssetsCached = $cached.cachedTotalAssets + totalPositiveYield - totalNegativeYield;
        // update the lastTotalAssets
        $cached.cachedTotalAssets = totalAssetsCached;

        // Accrue management fees after accruing yield to calculate fee asset amount on total vault AUM
        accrueManagementFee(totalAssetsCached);

        // Accrue performance fees on net yield amount
        accruePerformanceFee(totalAssetsCached, totalPositiveYield, totalNegativeYield);

        emit YieldAccrued(totalPositiveYield, totalNegativeYield);

        return totalAssetsCached;
    }

    /**
     * @dev Internal function that handles withdrawal operations, including strategy deallocation when needed.
     * @dev This function implements the core withdrawal logic for the vault, automatically managing
     *      fund retrieval from both idle vault balance and allocated strategies to fulfill withdrawal requests.
     * @dev Withdrawal Process:
     *      1. First attempts to use idle funds (assets sitting in the vault contract)
     *      2. If idle funds are insufficient, iterates through active strategies to deallocate funds
     *      3. For each strategy, respects the strategy's maxWithdraw() limit
     *      4. Updates strategy allocation accounting after successful deallocations
     *      5. Delegates to parent contract for final ERC4626 withdrawal execution
     * @dev Requirements:
     *      - Combined idle funds and strategy liquidity must be sufficient for withdrawal amount
     *      - All deallocated strategies must be in Active status
     *      - Strategy onWithdraw() calls must succeed and return expected amounts
     * @param caller The address that initiated the withdrawal (for access control)
     * @param receiver The address that will receive the withdrawn assets
     * @param owner The address whose shares are being burned for the withdrawal
     * @param assets The amount of assets to withdraw from the vault
     * @param shares The amount of shares to burn in exchange for the assets
     */
    function _executeWithdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
        internal
        virtual
        returns (uint256)
    {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        uint256 floatingFunds = IERC20(asset()).balanceOf(address(this));
        uint256 lockedAssets = _lockedAssets();
        uint256 totalWithdrawableAmount = floatingFunds >= lockedAssets ? floatingFunds - lockedAssets : 0;
        if (totalWithdrawableAmount < assets) {
            address[] memory deallocationOrder = $.deallocationOrder;
            uint256 strategiesCounter = deallocationOrder.length;

            uint256 desiredAssets;
            for (uint256 i; i < strategiesCounter; ++i) {
                if (
                    ($.strategyData[deallocationOrder[i]].status != IConcreteStandardVaultImpl.StrategyStatus.Active)
                        || !$.strategies.contains(deallocationOrder[i])
                ) continue;

                unchecked {
                    desiredAssets = assets - totalWithdrawableAmount;
                }

                uint256 withdrawableAmountFromStrategy = IStrategyTemplate(deallocationOrder[i]).maxWithdraw();
                uint256 withdrawAmount =
                    (withdrawableAmountFromStrategy >= desiredAssets) ? desiredAssets : withdrawableAmountFromStrategy;

                if (withdrawAmount > 0) {
                    // Actually withdraw from the strategy
                    uint256 actualWithdrawn = IStrategyTemplate(deallocationOrder[i]).onWithdraw(withdrawAmount);

                    // Update strategy allocated amount
                    $.strategyData[deallocationOrder[i]].allocated -= actualWithdrawn.toUint120();

                    totalWithdrawableAmount += actualWithdrawn;
                }

                if (totalWithdrawableAmount >= assets) break;
            }
        }

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

        return totalWithdrawableAmount;
    }

    function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
        internal
        virtual
        override
    {
        if (caller != owner) {
            _spendAllowance(owner, caller, shares);
        }

        // If asset() is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
        // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
        // calls the vault, which is assumed not malicious.
        //
        // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
        // shares are burned and after the assets are transferred, which is a valid state.

        CachedVaultStateLib.fetch().cachedTotalAssets -= assets;
        _burn(owner, shares);
        SafeERC20.safeTransfer(IERC20(asset()), receiver, assets);

        emit Withdraw(caller, receiver, owner, assets, shares);
    }

    /**
     * @dev Deposit/mint common workflow.
     */
    function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override {
        // If asset() is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
        // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
        // calls the vault, which is assumed not malicious.
        //
        // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
        // assets are transferred and before the shares are minted, which is a valid state.
        // slither-disable-next-line reentrancy-no-eth
        SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(this), assets);
        CachedVaultStateLib.fetch().cachedTotalAssets += assets;
        _mint(receiver, shares);

        emit Deposit(caller, receiver, assets, shares);
    }

    /**
     * @dev Accrue management fees by minting shares to the fee recipient.
     * @param totalAssetsAmount The total assets in the vault to calculate fee on
     */
    function accrueManagementFee(uint256 totalAssetsAmount) internal {
        (uint256 feeShares, uint256 feeAmount) = previewManagementFee(totalAssetsAmount);

        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        // Update last accrual timestamp
        $.lastManagementFeeAccrual = Time.timestamp();

        // Mint shares to management fee recipient
        address managementFeeRecipient = $.managementFeeRecipient;
        if (feeShares != 0 && managementFeeRecipient != address(0)) {
            // Mint shares to management fee recipient
            _mint(managementFeeRecipient, feeShares);

            emit ManagementFeeAccrued(managementFeeRecipient, feeShares, feeAmount);
        }
    }

    /**
     * @dev Accrue performance fees by minting shares to the fee recipient.
     * @param totalAssetsAmount The total assets in the vault to calculate fee on
     * @param positiveYield The total positive yield generated by all strategies
     * @param loss The total losses incurred by all strategies
     */
    function accruePerformanceFee(uint256 totalAssetsAmount, uint256 positiveYield, uint256 loss) internal {
        (uint256 performanceFeeShares, uint256 feeAmount) =
            previewPerformanceFee(totalAssetsAmount, positiveYield, loss, totalSupply());

        if (performanceFeeShares == 0) return;

        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        // Mint shares to performance fee recipient
        address performanceFeeRecipient = $.performanceFeeRecipient;
        if (performanceFeeRecipient != address(0)) {
            _mint(performanceFeeRecipient, performanceFeeShares);
            emit PerformanceFeeAccrued(performanceFeeRecipient, performanceFeeShares, feeAmount);
        }
    }

    /**
     * @dev Simulates the yield accrual operation across all strategies including fees.
     * @return totalAssets The projected total assets after yield accrual (current + yield - losses)
     * @return totalSupply The projected total supply after yield accrual (current + management fee shares + performance fee shares)
     */
    function _previewAccrueYieldAndFees() internal view virtual returns (uint256, uint256) {
        (uint256 totalPositiveYield, uint256 totalNegativeYield) = _previewYieldNoFees();

        uint256 totalSupplyCached = totalSupply();
        uint256 totalAssetsCached = cachedTotalAssets() + totalPositiveYield - totalNegativeYield;

        (uint256 managementFeeShares,) = previewManagementFee(totalAssetsCached);
        totalSupplyCached += managementFeeShares;

        (uint256 performanceFeeShares,) =
            previewPerformanceFee(totalAssetsCached, totalPositiveYield, totalNegativeYield, totalSupplyCached);
        totalSupplyCached += performanceFeeShares;

        return (totalAssetsCached, totalSupplyCached);
    }

    /**
     * @dev Calculates total positive and negative yield across all strategies.
     * @return totalPositiveYield The sum of all positive yields from strategies
     * @return totalNegativeYield The sum of all losses from strategies
     */
    function _previewYieldNoFees()
        internal
        view
        virtual
        returns (uint256 totalPositiveYield, uint256 totalNegativeYield)
    {
        address[] memory strategies = SVLib.fetch().strategies.values();
        uint256 strategiesCounter = strategies.length;

        for (uint256 i; i < strategiesCounter; ++i) {
            (uint256 positiveYield, uint256 loss,) = _previewStrategyYield(strategies[i]);

            if (positiveYield != 0 || loss != 0) {
                totalPositiveYield += positiveYield;
                totalNegativeYield += loss;
            }
        }
    }

    /**
     * @dev Accrues yield and accounts for losses for a single strategy.
     * @dev This function queries the current total allocated value from a strategy,
     *      compares it against the previously recorded allocated amount, and calculates
     *      the yield generated or loss incurred since the last yield accrual.
     * @param strategy The address of the strategy contract to accrue yield from.
     * @return yield The amount of positive yield generated by the strategy since last accrual.
     * @return loss The amount of loss incurred by the strategy since last accrual.
     */
    function _previewStrategyYield(address strategy) internal view returns (uint256, uint256, uint256) {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        uint120 strategyAllocatedAmount = $.strategyData[strategy].allocated;

        if ($.strategyData[strategy].status != IConcreteStandardVaultImpl.StrategyStatus.Active) {
            return (0, 0, 0);
        }

        uint256 currentTotalAllocatedValue = IStrategyTemplate(strategy).totalAllocatedValue();
        currentTotalAllocatedValue =
            (currentTotalAllocatedValue >= type(uint120).max) ? type(uint120).max : currentTotalAllocatedValue;

        uint256 yield;
        uint256 loss;
        if (currentTotalAllocatedValue == strategyAllocatedAmount) {
            return (yield, loss, currentTotalAllocatedValue);
        } else if (currentTotalAllocatedValue > strategyAllocatedAmount) {
            yield = currentTotalAllocatedValue - strategyAllocatedAmount;
        } else {
            loss = strategyAllocatedAmount - currentTotalAllocatedValue;
        }

        return (yield, loss, currentTotalAllocatedValue);
    }

    /**
     * @dev Preview management fee accrual.
     * @param _lastTotalAssets The total assets deposited in the vault to calculate fee on
     * @return feeShares The number of shares to mint as management fee
     * @return feeAmount The asset value of the management fee
     */
    function previewManagementFee(uint256 _lastTotalAssets)
        internal
        view
        returns (uint256 feeShares, uint256 feeAmount)
    {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        if ($.managementFee == 0) return (0, 0);

        uint32 currentTime = Time.timestamp();
        uint32 lastAccrual = $.lastManagementFeeAccrual;

        if (currentTime == lastAccrual) return (0, 0);

        uint256 timeElapsed = currentTime - lastAccrual;
        // management fee is calculated on total vault AUM(after yield accrual)
        uint256 annualFeeAmount = (_lastTotalAssets * $.managementFee) / ConstantsLib.BASIS_POINTS_DENOMINATOR;
        feeAmount = (annualFeeAmount * timeElapsed) / (365 days);

        if (feeAmount == 0) return (0, 0);

        // sanity check - clamp the fee amount to the last total assets
        if (feeAmount > _lastTotalAssets) {
            feeAmount = _lastTotalAssets;
        }

        // convert fee amount to shares using total assets net of the fee to prevent dilution. of the fee amount
        feeShares =
            feeAmount.calcConvertToShares(totalSupply(), _lastTotalAssets - feeAmount, Math.Rounding.Floor, false);

        return (feeShares, feeAmount);
    }

    /**
     * @dev Preview performance fee accrual.
     * @param totalAssetsAmount The total assets in the vault to calculate fee on
     * @param positiveYield The total positive yield generated by all strategies
     * @param loss The total losses incurred by all strategies
     * @param totalSupply The current total supply of vault shares
     * @return shares The number of shares to mint as performance fee
     * @return feeAmount The asset value of the performance fee
     */
    function previewPerformanceFee(uint256 totalAssetsAmount, uint256 positiveYield, uint256 loss, uint256 totalSupply)
        internal
        view
        returns (uint256, uint256)
    {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        if ($.performanceFee == 0 || (loss >= positiveYield)) return (0, 0);

        uint256 netPositiveYield = positiveYield - loss;
        uint256 feeAmount = Math.mulDiv(netPositiveYield, $.performanceFee, ConstantsLib.BASIS_POINTS_DENOMINATOR);

        if (feeAmount == 0) return (0, 0);

        uint256 feeShares =
            feeAmount.calcConvertToShares(totalSupply, totalAssetsAmount - feeAmount, Math.Rounding.Floor, false);

        return (feeShares, feeAmount);
    }

    /**
     * @dev Internal function to calculate the maximum amount of assets that can be withdrawn by an owner.
     * @dev The calculation considers:
     *      - Owner's current share balance converted to equivalent assets
     *      - Current vault liquidity (idle assets + withdrawable amounts from strategies)
     *      - Strategy withdrawal limitations and availability
     * @param owner The address of the account for which to calculate maximum withdrawal.
     * @return The maximum amount of assets that can actually be withdrawn by the owner,
     *         considering both ownership rights and liquidity constraints.
     * @return totalAssets The total amount of assets in the vault after previewing the yield accrual
     */
    function _maxWithdraw(address owner) internal view virtual returns (uint256, uint256, uint256) {
        uint256 ownerShares = balanceOf(owner);
        (uint256 totalAssetsPreview, uint256 totalSupplyPreview) = _previewAccrueYieldAndFees();

        uint256 maxAssets =
            ownerShares.calcConvertToAssets(totalSupplyPreview, totalAssetsPreview, Math.Rounding.Floor, false);

        return (_simulateWithdraw(maxAssets), totalAssetsPreview, totalSupplyPreview);
    }

    /// @dev Simulate withdrawing an amount of assets from the vault.
    /// @param requestedAssets Amount of assets to withdraw.
    /// @return Amount of assets filled.
    function _simulateWithdraw(uint256 requestedAssets) internal view virtual returns (uint256) {
        SVLib.ConcreteStandardVaultImplStorage storage $ = SVLib.fetch();

        uint256 totalWithdrawableAmount = IERC20(asset()).balanceOf(address(this));
        if (totalWithdrawableAmount < requestedAssets) {
            address[] memory deallocationOrder = $.deallocationOrder;
            uint256 strategiesCounter = deallocationOrder.length;

            for (uint256 i; i < strategiesCounter; ++i) {
                if (
                    ($.strategyData[deallocationOrder[i]].status != IConcreteStandardVaultImpl.StrategyStatus.Active)
                        || !$.strategies.contains(deallocationOrder[i])
                ) continue;

                uint256 desiredAssets;
                unchecked {
                    desiredAssets = requestedAssets - totalWithdrawableAmount;
                }

                uint256 withdrawableAmountFromStrategy = IStrategyTemplate(deallocationOrder[i]).maxWithdraw();
                uint256 withdrawAmount =
                    (withdrawableAmountFromStrategy >= desiredAssets) ? desiredAssets : withdrawableAmountFromStrategy;

                totalWithdrawableAmount += withdrawAmount;

                if (totalWithdrawableAmount >= requestedAssets) break;
            }
        } else {
            totalWithdrawableAmount = requestedAssets;
        }

        return totalWithdrawableAmount;
    }

    /**
     * @dev Internal function used only by the public convertToShares() function.
     * @dev This function returns share amounts NOT inclusive of management or performance fees,
     *      as required by the ERC4626 specification. The convertToShares() function should return
     *      the theoretical share amount for a given asset amount without considering fee deductions.
     * @param assets The amount of assets to convert to shares
     * @param rounding The rounding direction for the conversion
     * @return The equivalent amount of shares, NOT inclusive of fees
     */
    function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) {
        (uint256 totalPositiveYield, uint256 totalNegativeYield) = _previewYieldNoFees();
        return assets.calcConvertToShares(
            totalSupply(), cachedTotalAssets() + totalPositiveYield - totalNegativeYield, rounding, false
        );
    }

    /**
     * @dev Internal function used only by the public convertToAssets() function.
     * @dev This function returns asset amounts NOT inclusive of management or performance fees,
     *      as required by the ERC4626 specification. The convertToAssets() function should return
     *      the theoretical asset amount for a given share amount without considering fee deductions.
     * @param shares The amount of shares to convert to assets
     * @param rounding The rounding direction for the conversion
     * @return The equivalent amount of assets, NOT inclusive of fees
     */
    function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) {
        (uint256 totalPositiveYield, uint256 totalNegativeYield) = _previewYieldNoFees();
        return shares.calcConvertToAssets(
            totalSupply(), cachedTotalAssets() + totalPositiveYield - totalNegativeYield, rounding, false
        );
    }

    function _lockedAssets() internal view virtual returns (uint256) {
        return 0;
    }
}
"
    },
    "node_modules/@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/AccessControlEnumerable.sol)

pragma solidity ^0.8.20;

import {IAccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/IAccessControlEnumerable.sol";
import {AccessControlUpgradeable} from "../AccessControlUpgradeable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {Initializable} from "../../proxy/utils/Initializable.sol";

/**
 * @dev Extension of {AccessControl} that allows enumerating the members of each role.
 */
abstract contract AccessControlEnumerableUpgradeable is Initializable, IAccessControlEnumerable, AccessControlUpgradeable {
    using EnumerableSet for EnumerableSet.AddressSet;

    /// @custom:storage-location erc7201:openzeppelin.storage.AccessControlEnumerable
    struct AccessControlEnumerableStorage {
        mapping(bytes32 role => EnumerableSet.AddressSet) _roleMembers;
    }

    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.AccessControlEnumerable")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant AccessControlEnumerableStorageLocation = 0xc1f6fe24621ce81ec5827caf0253cadb74709b061630e6b55e82371705932000;

    function _getAccessControlEnumerableStorage() private pure returns (AccessControlEnumerableStorage storage $) {
        assembly {
            $.slot := AccessControlEnumerableStorageLocation
        }
    }

    function __AccessControlEnumerable_init() internal onlyInitializing {
    }

    function __AccessControlEnumerable_init_unchained() internal onlyInitializing {
    }
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev Returns one of the accounts that have `role`. `index` must be a
     * value between 0 and {getRoleMemberCount}, non-inclusive.
     *
     * Role bearers are not sorted in any particular way, and their ordering may
     * change at any point.
     *
     * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
     * you perform all queries on the same block. See the following
     * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
     * for more information.
     */
    function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        return $._roleMembers[role].at(index);
    }

    /**
     * @dev Returns the number of accounts that have `role`. Can be used
     * together with {getRoleMember} to enumerate all bearers of a role.
     */
    function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        return $._roleMembers[role].length();
    }

    /**
     * @dev Return all accounts that have `role`
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function getRoleMembers(bytes32 role) public view virtual returns (address[] memory) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        return $._roleMembers[role].values();
    }

    /**
     * @dev Overload {AccessControl-_grantRole} to track enumerable memberships
     */
    function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        bool granted = super._grantRole(role, account);
        if (granted) {
            $._roleMembers[role].add(account);
        }
        return granted;
    }

    /**
     * @dev Overload {AccessControl-_revokeRole} to track enumerable memberships
     */
    function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        bool revoked = super._revokeRole(role, account);
        if (revoked) {
            $._roleMembers[role].remove(account);
        }
        return revoked;
    }
}
"
    },
    "node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC4626.sol)

pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {ERC20Upgradeable} from "../ERC20Upgradeable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Initializable} from "../../../proxy/utils/Initializable.sol";

/**
 * @dev Implementation of the ERC-4626 "Tokenized Vault Standard" as defined in
 * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626].
 *
 * This extension allows the minting and burning of "shares" (represented using the ERC-20 inheritance) in exchange for
 * underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
 * the ERC-20 standard. Any additional extensions included along it would affect the "shares" token represented by this
 * contract and not the "assets" token which is an independent contract.
 *
 * [CAUTION]
 * ====
 * In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through frontrunning
 * with a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation
 * attack and is essentially a problem of slippage. Vault deployers can protect against this attack by making an initial
 * deposit of a non-trivial amount of the asset, such that price manipulation becomes infeasible. Withdrawals may
 * similarly be affected by slippage. Users can protect against this attack as well as unexpected slippage in general by
 * verifying the amount received is as expected, using a wrapper that performs these checks such as
 * https://github.com/fei-protocol/ERC4626#erc4626router-and-base[ERC4626Router].
 *
 * Since v4.9, this implementation introduces configurable virtual assets and shares to help developers mitigate that risk.
 * The `_decimalsOffset()` corresponds to an offset in the decimal representation between the underlying asset's decimals
 * and the vault decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which
 * itself determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default
 * offset (0) makes it non-profitable even if an attacker is able to capture value from multiple user deposits, as a result
 * of the value being captured by the virtual shares (out of the attacker's donation) matching the attacker's expected gains.
 * With a larger offset, the attack becomes orders of magnitude more expensive than it is profitable. More details about the
 * underlying math can be found xref:erc4626.adoc#inflation-attack[here].
 *
 * The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued
 * to the vault. Also, if the vault experiences losses, the users try to exit the vault, the virtual shares and assets
 * will cause the first user to exit to experience reduced losses in detriment to the last users that will experience
 * bigger losses. Developers willing to revert back to the pre-v4.9 behavior just need to override the
 * `_convertToShares` and `_convertToAssets` functions.
 *
 * To learn more, check out our xref:ROOT:erc4626.adoc[ERC-4626 guide].
 * ====
 */
abstract contract ERC4626Upgradeable is Initializable, ERC20Upgradeable, IERC4626 {
    using Math for uint256;

    /// @custom:storage-location erc7201:openzeppelin.storage.ERC4626
    struct ERC4626Storage {
        IERC20 _asset;
        uint8 _underlyingDecimals;
    }

    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC4626")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant ERC4626StorageLocation = 0x0773e532dfede91f04b12a73d3d2acd361424f41f76b4fb79f090161e36b4e00;

    function _getERC4626Storage() private pure returns (ERC4626Storage storage $) {
        assembly {
            $.slot := ERC4626StorageLocation
        }
    }

    /**
     * @dev Attempted to deposit more assets than the max amount for `receiver`.
     */
    error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max);

    /**
     * @dev Attempted to mint more shares than the max amount for `receiver`.
     */
    error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max);

    /**
     * @dev Attempted to withdraw more assets than the max amount for `receiver`.
     */
    error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max);

    /**
     * @dev Attempted to redeem more shares than the max amount for `receiver`.
     */
    error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max);

    /**
     * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC-20 or ERC-777).
     */
    function __ERC4626_init(IERC20 asset_) internal onlyInitializing {
        __ERC4626_init_unchained(asset_);
    }

    function __ERC4626_init_unchained(IERC20 asset_) internal onlyInitializing {
        ERC4626Storage storage $ = _getERC4626Storage();
        (bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
        $._underlyingDecimals = success ? assetDecimals : 18;
        $._asset = asset_;
    }

    /**
     * @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way.
     */
    function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool ok, uint8 assetDecimals) {
        (bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
            abi.encodeCall(IERC20Metadata.decimals, ())
        );
        if (success && encodedDecimals.length >= 32) {
            uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256));
            if (returnedDecimals <= type(uint8).max) {
                return (true, uint8(returnedDecimals));
            }
        }
        return (false, 0);
    }

    /**
     * @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This
     * "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the
     * asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals.
     *
     * See {IERC20Metadata-decimals}.
     */
    function decimals() public view virtual override(IERC20Metadata, ERC20Upgradeable) returns (uint8) {
        ERC4626Storage storage $ = _getERC4626Storage();
        return $._underlyingDecimals + _decimalsOffset();
    }

    /** @dev See {IERC4626-asset}. */
    function asset() public view virtual returns (address) {
        ERC4626Storage storage $ = _getERC4626Storage();
        return address($._asset);
    }

    /** @dev See {IERC4626-totalAssets}. */
    function totalAssets() public view virtual returns (uint256) {
        ERC4626Storage storage $ = _getERC4626Storage();
        return $._asset.balanceOf(address(this));
    }

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

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

    /** @dev See {IERC4626-maxDeposit}. */
    function maxDeposit(address) public view virtual returns (uint256) {
        return type(uint256).max;
    }

    /** @dev See {IERC4626-maxMint}. */
    function maxMint(address) public view virtual returns (uint256) {
        return type(uint256).max;
    }

    /** @dev See {IERC4626-maxWithdraw}. */
    function maxWithdraw(address owner) public view virtual returns (uint256) {
        return _convertToAssets(balanceOf(owner), Math.Rounding.Floor);
    }

    /** @dev See {IERC4626-maxRedeem}. */
    function maxRedeem(address owner) public view virtual returns (uint256) {
        return balanceOf(owner);
    }

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

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

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

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

    /** @dev See {IERC4626-deposit}. */
    function deposit(uint256 assets, address receiver) public virtual returns (uint256) {
        uint256 maxAssets = maxDeposit(receiver);
        if (assets > maxAssets) {
            revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);
        }

        uint256 shares = previewDeposit(assets);
        _deposit(_msgSender(), receiver, assets, shares);

        return shares;
    }

    /** @dev See {IERC4626-mint}. */
    function mint(uint256 shares, address receiver) public virtual returns (uint256) {
        uint256 maxShares = maxMint(receiver);
        if (shares > maxShares) {
            revert ERC4626ExceededMaxMint(receiver, shares, maxShares);
        }

        uint256 assets = previewMint(shares);
        _deposit(_msgSender(), receiver, assets, shares);

        return assets;
    }

    /** @dev See {IERC4626-withdraw}. */
    function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) {
        uint256 maxAssets = maxWithdraw(owner);
        if (assets > maxAssets) {
            revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
        }

        uint256 shares = previewWithdraw(assets);
        _withdraw(_msgSender(), receiver, owner, assets, shares);

        return shares;
    }

    /** @dev See {IERC4626-redeem}. */
    function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) {
        uint256 maxShares = maxRedeem(owner);
        if (shares > maxShares) {
            revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
        }

        uint256 assets = previewRedeem(shares);
        _withdraw(_msgSender(), receiver, owner, assets, shares);

        return assets;
    }

    /**
     * @dev Internal conversion function (from assets to shares) with support for rounding direction.
     */
    function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
        return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
    }

    /**
     * @dev Internal conversion function (from shares to assets) with support for rounding direction.
     */
    function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) {
        return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding);
    }

    /**
     * @dev Deposit/mint common workflow.
     */
    function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual {
        ERC4626Storage storage $ = _getERC4626Storage();
        // If _asset is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
        // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
        // calls the vault, which is assumed not malicious.
        //
        // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
        // assets are transferred and before the shares are minted, which is a valid state.
        // slither-disable-next-line reentrancy-no-eth
        SafeERC20.safeTransferFrom($._asset, caller, address(this), assets);
        _mint(receiver, shares);

        emit Deposit(caller, receiver, assets, shares);
    }

    /**
     * @dev Withdraw/redeem common workflow.
     */
    function _withdraw(
        address caller,
        address receiver,
        address owner,
        uint256 assets,
        uint256 shares
    ) internal virtual {
        ERC4626Storage storage $ = _getERC4626Storage();
        if (caller != owner) {
            _spendAllowance(owner, caller, shares);
        }

        // If _asset is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
        // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
        // calls the vault, which is assumed not malicious.
        //
        // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
        // shares are burned and after the assets are transferred, which is a valid state.
        _burn(owner, shares);
        SafeERC20.safeTransfer($._asset, receiver, assets);

        emit Withdraw(caller, receiver, owner, assets, shares);
    }

    function _decimalsOffset() internal view virtual returns (uint8) {
        return 0;
    }
}
"
    },
    "node_modules/@openzeppelin/contracts/utils/Address.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (utils/Address.sol)

pragma solidity ^0.8.20;

import {Errors} from "./Errors.sol";

/**
 * @dev Collection of functions related to the address type
 */
library Address {
    /**
     * @dev There's no code at `target` (it is not a contract).
     */
    error AddressEmptyCode(address target);

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * i

Tags:
ERC20, ERC165, Multisig, Mintable, Swap, Liquidity, Yield, Upgradeable, Multi-Signature, Factory|addr:0xab423fc3c5ded455b5617afcfe54663b7167c7ac|verified:true|block:23727976|tx:0xeb74a1c64449c762aaf04e844e4e16ecaa6165944d4cbae3b8881fc46b272351|first_check:1762335787

Submitted on: 2025-11-05 10:43:09

Comments

Log in to comment.

No comments yet.