StablecoinVault

Description:

Proxy contract enabling upgradeable smart contract patterns. Delegates calls to an implementation contract.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/StablecoinVault.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;

import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import {
    SafeERC20Upgradeable,
    IERC20Upgradeable
} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";

import { UtilLib } from "src/utils/UtilLib.sol";

/// @notice Interface for ERC20 tokens with metadata
interface IERC20Metadata {
    /// @notice Returns the token's decimals
    function decimals() external view returns (uint8);
}

/**
 * @title Stablecoin Vault
 * @author Kelp DAO
 * @notice Upgradeable ERC‑4626 vault that pools user deposits and hands excess funds
 *         to an off‑chain strategy custodian wallet while issuing a floating‑price
 *         receipt token (vault shares).
 */
contract StablecoinVault is
    Initializable,
    ERC4626Upgradeable,
    AccessControlUpgradeable,
    PausableUpgradeable,
    ReentrancyGuardUpgradeable
{
    using SafeERC20Upgradeable for IERC20Upgradeable;

    /*//////////////////////////////////////////////////////////////
                            CONSTANTS
    //////////////////////////////////////////////////////////////*/

    /// @notice The maximum withdrawal delay allowed
    uint256 public constant MAX_WITHDRAWAL_DELAY = 30 days;

    /// @notice The maximum number of open (unclaimed) withdrawals allowed per user at any time
    uint256 public constant MAX_WITHDRAWALS_PER_USER = 100;

    /// @notice The maximum number of withdrawals that can be claimed in a single batch
    uint256 public constant MAX_BATCH_CLAIM_WITHDRAWALS = 100;

    /// @notice Pauser role constant
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    /// @notice Operator role constant
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

    /*//////////////////////////////////////////////////////////////
                            ERRORS
    //////////////////////////////////////////////////////////////*/

    /// @notice Custom errors
    error InvalidDecimals();
    error InvalidMinDeposit();
    error InvalidWithdrawalDelay();
    error InvalidMaxNumberOfWithdrawalsPerUser();
    error UserNotWhitelisted();
    error AlreadyWhitelisted();
    error BelowMinDeposit();
    error CannotWithdrawZeroShares();
    error NothingQueued();
    error NoAssetsToQueue();
    error WithdrawalNotReady();
    error NoExcessAssets();
    error InsufficientShares();
    error WithdrawalDoesNotExist();
    error NotYourWithdrawal();
    error WithdrawalAlreadyClaimed();
    error WithdrawalLimitReached();
    error InstantWithdrawalNotAllowed();
    error NoWithdrawalsToClaim();
    error TooManyWithdrawalsInBatch();
    error InvalidMaxBatchClaimWithdrawals();
    error SlippageExceeded();

    /*//////////////////////////////////////////////////////////////
                            EVENTS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Emitted when an address is added to the deposit whitelist
     * @param account Address of the user added to the whitelist
     */
    event AddedToWhitelist(address indexed account);

    /**
     * @notice Emitted when an address is removed from the deposit whitelist
     * @param account Address of the user removed from the whitelist
     */
    event RemovedFromWhitelist(address indexed account);

    /**
     * @notice Emitted when the minimum deposit amount is updated
     * @param newMinDeposit The new minimum deposit amount
     */
    event MinDepositUpdated(uint256 newMinDeposit);

    /**
     * @notice Emitted when the withdrawal delay is updated
     * @param newWithdrawalDelay The new withdrawal delay in seconds
     */
    event WithdrawalDelayUpdated(uint256 newWithdrawalDelay);

    /**
     * @notice Emitted when the maximum number of withdrawals per user is updated
     * @param newMaxNumberOfWithdrawalsPerUser The new maximum number of withdrawals per user
     */
    event MaxNumberOfWithdrawalsPerUserUpdated(uint256 newMaxNumberOfWithdrawalsPerUser);

    /**
     * @notice Emitted when the maximum number of withdrawals that can be claimed in a batch is updated
     * @param newMaxBatchClaimWithdrawals The new maximum number of withdrawals that can be claimed in a batch
     */
    event MaxBatchClaimWithdrawalsUpdated(uint256 newMaxBatchClaimWithdrawals);

    /**
     * @notice Emitted when the custodian deposit address is updated
     * @param newCustodianDepositAddress The new custodian deposit address
     */
    event CustodianDepositAddressUpdated(address indexed newCustodianDepositAddress);

    /**
     * @notice Emitted when a withdrawal is queued
     * @param withdrawalId Unique ID of the queued withdrawal
     * @param user Address of the user who queued the withdrawal
     * @param assets Amount of assets queued for withdrawal
     * @param sharesToRedeem Amount of shares to be redeemed when the withdrawal is claimed
     * @param unlockTime Timestamp when the withdrawal can be claimed
     */
    event WithdrawalQueued(
        uint256 withdrawalId, address indexed user, uint256 assets, uint256 sharesToRedeem, uint256 unlockTime
    );

    /**
     * @notice Emitted when a withdrawal is completed
     * @param withdrawalId Unique ID of the completed withdrawal
     * @param user Address of the user who completed the withdrawal
     * @param assets Amount of assets withdrawn
     * @param sharesBurned Amount of shares burned during the withdrawal
     */
    event WithdrawalCompleted(uint256 withdrawalId, address indexed user, uint256 assets, uint256 sharesBurned);

    /**
     * @notice Emitted when multiple withdrawals are claimed in a batch
     * @param count Number of withdrawals claimed in the batch
     * @param sender Address of the operator who performed the batch claim
     */
    event BatchClaimCompleted(uint256 count, address indexed sender);

    /**
     * @notice Emitted when assets are swept to the custodian wallet
     * @param custodianDepositAddress Address of the custodian wallet where assets are swept
     * @param amount Amount of assets swept to the custodian wallet
     */
    event AssetsSweptToCustodian(address indexed custodianDepositAddress, uint256 amount);

    /**
     * @notice Emitted when the latest custodian balance is updated
     * @param newBalance The new custodian balance
     * @param timestamp The timestamp when the balance was recorded
     * @param reporter The address of the operator who reported the new balance
     */
    event CustodianBalanceUpdated(uint256 newBalance, uint256 timestamp, address indexed reporter);

    /*//////////////////////////////////////////////////////////////
                            STRUCTS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Represents a queued withdrawal request
     * @param withdrawalId Unique ID of the withdrawal request
     * @param user Address of the user who queued the withdrawal
     * @param assets Amount of assets owed to the user
     * @param shares Shares to be burned once the withdrawal is claimable
     * @param unlockTime When the withdrawal can be claimed (timestamp)
     * @param claimed Whether the withdrawal has been claimed or not
     */
    struct WithdrawalRequest {
        uint256 withdrawalId;
        address user;
        uint256 assets;
        uint256 shares;
        uint256 unlockTime;
        bool claimed;
    }

    /*//////////////////////////////////////////////////////////////
                            STATE VARIABLES
    //////////////////////////////////////////////////////////////*/

    /// @notice Whitelisted addresses allowed to deposit into the vault
    mapping(address user => bool allowed) public whitelist;

    /// @notice Minimal deposit denominated in the underlying asset’s decimals
    uint256 public minDeposit;

    /// @notice Minimum delay in seconds before a withdrawal can be claimed after queuing
    uint256 public withdrawalDelay;

    /// @notice The maximum number of withdrawals that any user can have open (unclaimed) at any time
    uint256 public maxNumberOfWithdrawalsPerUser;

    /// @notice The maximum number of withdrawals that can be claimed in a single batch
    uint256 public maxBatchClaimWithdrawals;

    /// @notice Total assets currently queued for withdrawal
    uint256 public totalAssetsQueuedForWithdrawal;

    /// @notice A global incremental counter for withdrawal IDs
    uint256 public withdrawalCounter;

    /// @notice Mapping of withdrawal IDs to withdrawal requests
    mapping(uint256 withdrawalId => WithdrawalRequest withdrawalRequest) public withdrawals;

    /// @notice Mapping of user addresses to their withdrawal IDs
    mapping(address user => uint256[] withdrawalIds) public userWithdrawalIds;

    /// @notice Address of the custodian's wallet where excess assets are swept
    address public custodianDepositAddress;

    /// @notice The latest custodian balance
    uint256 public latestCustodianBalance;

    /// @notice The timestamp at which the latest custodian balance was recorded
    uint256 public latestCustodianBalanceUpdate;

    /*//////////////////////////////////////////////////////////////
                            MODIFIERS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Modifier to check if the user is whitelisted for deposits or not
     * @param user The address of the user to check
     */
    modifier onlyWhitelistedUser(address user) {
        if (!whitelist[user]) revert UserNotWhitelisted();
        _;
    }

    /*//////////////////////////////////////////////////////////////
                        CONSTRUCTOR
    //////////////////////////////////////////////////////////////*/

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    /*//////////////////////////////////////////////////////////////
                        INITIALIZER
    //////////////////////////////////////////////////////////////*/

    /**
     * @param asset_ The underlying ERC‑20 token of the vault (e.g. USDT)
     * @param name_ Vault share name
     * @param symbol_ Vault share symbol
     * @param admin Address of the admin who can change the vault parameters
     * @param operator Address of the operator who can perform certain operational actions (e.g. sweep excess assets to
     * custodian, update reported custodian balance or batch claim withdrawals on behalf of users)
     * @param custodianDepositAddress_ Destination wallet for strategy funds
     * @param minDeposit_ Initial minimum deposit amount in underlying asset's decimals
     * @param withdrawalDelay_ Initial withdrawal delay in seconds (e.g. 7 days)
     * @param maxNumberOfWithdrawalsPerUser_ Initial maximum number of withdrawals per user (e.g. 5)
     * @param maxBatchClaimWithdrawals_ Initial maximum number of withdrawals that can be claimed in a batch
     */
    function initialize(
        IERC20Upgradeable asset_,
        string memory name_,
        string memory symbol_,
        address admin,
        address operator,
        address custodianDepositAddress_,
        uint256 minDeposit_,
        uint256 withdrawalDelay_,
        uint256 maxNumberOfWithdrawalsPerUser_,
        uint256 maxBatchClaimWithdrawals_
    )
        external
        initializer
    {
        UtilLib.checkNonZeroAddress(address(asset_));
        UtilLib.checkNonZeroAddress(admin);
        UtilLib.checkNonZeroAddress(operator);
        UtilLib.checkNonZeroAddress(custodianDepositAddress_);

        if (IERC20Metadata(address(asset_)).decimals() > 18) {
            revert InvalidDecimals();
        }

        __ERC4626_init(asset_);
        __ERC20_init(name_, symbol_);
        __AccessControl_init();
        __Pausable_init();
        __ReentrancyGuard_init();

        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(PAUSER_ROLE, admin);
        _grantRole(OPERATOR_ROLE, operator);

        if (minDeposit_ == 0) revert InvalidMinDeposit();
        if (withdrawalDelay_ == 0 || withdrawalDelay_ > MAX_WITHDRAWAL_DELAY) {
            revert InvalidWithdrawalDelay();
        }
        if (maxNumberOfWithdrawalsPerUser_ == 0 || maxNumberOfWithdrawalsPerUser_ > MAX_WITHDRAWALS_PER_USER) {
            revert InvalidMaxNumberOfWithdrawalsPerUser();
        }

        if (maxBatchClaimWithdrawals_ == 0 || maxBatchClaimWithdrawals_ > MAX_BATCH_CLAIM_WITHDRAWALS) {
            revert InvalidMaxBatchClaimWithdrawals();
        }

        custodianDepositAddress = custodianDepositAddress_;
        minDeposit = minDeposit_;
        withdrawalDelay = withdrawalDelay_;
        maxNumberOfWithdrawalsPerUser = maxNumberOfWithdrawalsPerUser_;
        maxBatchClaimWithdrawals = maxBatchClaimWithdrawals_;
        latestCustodianBalance = 0;
        latestCustodianBalanceUpdate = 0;

        emit CustodianDepositAddressUpdated(custodianDepositAddress_);
        emit MinDepositUpdated(minDeposit_);
        emit WithdrawalDelayUpdated(withdrawalDelay_);
        emit MaxNumberOfWithdrawalsPerUserUpdated(maxNumberOfWithdrawalsPerUser_);
        emit MaxBatchClaimWithdrawalsUpdated(maxBatchClaimWithdrawals_);
    }

    /*//////////////////////////////////////////////////////////////
                      ADMIN FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Pauses the contract
    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    /// @notice Unpauses the contract
    function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
        _unpause();
    }

    /**
     * @notice Adds `account` to the deposit whitelist.
     * @param account Address to add to the whitelist.
     */
    function addToWhitelist(address account) external onlyRole(DEFAULT_ADMIN_ROLE) {
        UtilLib.checkNonZeroAddress(account);
        if (whitelist[account]) revert AlreadyWhitelisted();

        whitelist[account] = true;
        emit AddedToWhitelist(account);
    }

    /**
     * @notice Removes `account` from the deposit whitelist.
     * @param account Address to remove from the whitelist.
     */
    function removeFromWhitelist(address account) external onlyRole(DEFAULT_ADMIN_ROLE) onlyWhitelistedUser(account) {
        UtilLib.checkNonZeroAddress(account);
        whitelist[account] = false;
        emit RemovedFromWhitelist(account);
    }

    /**
     * @notice Sets a new minimum deposit amount.
     * @param newMinDeposit New minimum deposit amount in the underlying asset's decimals.
     */
    function setMinDeposit(uint256 newMinDeposit) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if (newMinDeposit == 0) revert InvalidMinDeposit();
        minDeposit = newMinDeposit;
        emit MinDepositUpdated(newMinDeposit);
    }

    /**
     * @notice Sets a new withdrawal delay.
     * @param newWithdrawalDelay New withdrawal delay in seconds.
     */
    function setWithdrawalDelay(uint256 newWithdrawalDelay) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if (newWithdrawalDelay == 0 || newWithdrawalDelay > MAX_WITHDRAWAL_DELAY) {
            revert InvalidWithdrawalDelay();
        }
        withdrawalDelay = newWithdrawalDelay;
        emit WithdrawalDelayUpdated(newWithdrawalDelay);
    }

    /**
     * @notice Updates the maximum number of withdrawals per user
     * @param _maxNumberOfWithdrawalsPerUser The new maximum number of withdrawals per user
     */
    function setMaxNumberOfWithdrawalsPerUser(uint256 _maxNumberOfWithdrawalsPerUser)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        if (_maxNumberOfWithdrawalsPerUser == 0 || _maxNumberOfWithdrawalsPerUser > MAX_WITHDRAWALS_PER_USER) {
            revert InvalidMaxNumberOfWithdrawalsPerUser();
        }

        maxNumberOfWithdrawalsPerUser = _maxNumberOfWithdrawalsPerUser;
        emit MaxNumberOfWithdrawalsPerUserUpdated(_maxNumberOfWithdrawalsPerUser);
    }

    /**
     * @notice Updates the maximum number of withdrawals that can be claimed in a batch
     * @param _maxBatchClaimWithdrawals The new maximum number of withdrawals that can be claimed in a batch
     */
    function setMaxBatchClaimWithdrawals(uint256 _maxBatchClaimWithdrawals) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if (_maxBatchClaimWithdrawals == 0 || _maxBatchClaimWithdrawals > MAX_BATCH_CLAIM_WITHDRAWALS) {
            revert InvalidMaxBatchClaimWithdrawals();
        }

        maxBatchClaimWithdrawals = _maxBatchClaimWithdrawals;
        emit MaxBatchClaimWithdrawalsUpdated(_maxBatchClaimWithdrawals);
    }

    /**
     * @notice Sets a new custodian deposit address.
     * @param newCustodianDepositAddress New custodian deposit address.
     */
    function setCustodianDepositAddress(address newCustodianDepositAddress) external onlyRole(DEFAULT_ADMIN_ROLE) {
        UtilLib.checkNonZeroAddress(newCustodianDepositAddress);
        custodianDepositAddress = newCustodianDepositAddress;
        emit CustodianDepositAddressUpdated(newCustodianDepositAddress);
    }

    /*//////////////////////////////////////////////////////////////
                      OPERATOR FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Transfers the idle assets to the custodian deposit address
    function sweepToCustodian() external nonReentrant onlyRole(OPERATOR_ROLE) {
        IERC20Upgradeable assetToken = IERC20Upgradeable(asset());

        uint256 excessAssets = getExcessAssets();
        if (excessAssets == 0) revert NoExcessAssets();

        assetToken.safeTransfer(custodianDepositAddress, excessAssets);

        // After sweeping assets to the custodian, we assume that the custodian balance has increased by the same amount
        latestCustodianBalance += excessAssets;

        emit AssetsSweptToCustodian(custodianDepositAddress, excessAssets);
    }

    /**
     * @notice Updates the latest custodian balance and timestamp
     * @param newBalance The new custodian balance
     */
    function updateCustodianBalance(uint256 newBalance) external nonReentrant onlyRole(OPERATOR_ROLE) {
        latestCustodianBalance = newBalance;
        latestCustodianBalanceUpdate = block.timestamp;

        emit CustodianBalanceUpdated(newBalance, block.timestamp, msg.sender);
    }

    /*//////////////////////////////////////////////////////////////
                      ERC4626 OVERRIDES
    //////////////////////////////////////////////////////////////*/

    /**
     * @inheritdoc ERC4626Upgradeable
     * @dev Use same decimals as the underlying asset in order to avoid any possible precision loss when
     *      converting between assets and shares.
     */
    function decimals() public view override returns (uint8) {
        return IERC20Metadata(asset()).decimals();
    }

    /**
     * @inheritdoc ERC4626Upgradeable
     * @notice Overrides the totalAssets function from ERC4626 to include assets reported by the custodian
     */
    function totalAssets() public view override returns (uint256) {
        IERC20Upgradeable assetToken = IERC20Upgradeable(asset());
        return assetToken.balanceOf(address(this)) + latestCustodianBalance;
    }

    /**
     * @inheritdoc ERC4626Upgradeable
     * @dev Overrides the ERC4626Upgradeable deposit function to enforce whitelist and minimum deposit checks.
     */
    function _deposit(
        address caller,
        address receiver,
        uint256 assets,
        uint256 shares
    )
        internal
        override
        nonReentrant
        whenNotPaused
        onlyWhitelistedUser(caller)
        onlyWhitelistedUser(receiver)
    {
        if (assets < minDeposit) revert BelowMinDeposit();
        super._deposit(caller, receiver, assets, shares);
    }

    /**
     * @inheritdoc ERC4626Upgradeable
     * @dev Overrides the ERC4626Upgradeable withdraw function to disallow instant withdrawals.
     */
    function withdraw(uint256, address, address) public pure override returns (uint256) {
        revert InstantWithdrawalNotAllowed();
    }

    /**
     * @inheritdoc ERC4626Upgradeable
     * @dev Overrides the ERC4626Upgradeable redeem function to disallow instant redemptions.
     */
    function redeem(uint256, address, address) public pure override returns (uint256) {
        revert InstantWithdrawalNotAllowed();
    }

    /**
     * @inheritdoc ERC4626Upgradeable
     * @dev Overrides the ERC4626Upgradeable maxWithdraw function to return 0, to signal that instant withdrawals
     *      are not allowed.
     */
    function maxWithdraw(address) public pure override returns (uint256) {
        return 0;
    }

    /**
     * @inheritdoc ERC4626Upgradeable
     * @dev Overrides the ERC4626Upgradeable maxRedeem function to return 0, to signal that instant redemptions
     *      are not allowed.
     */
    function maxRedeem(address) public pure override returns (uint256) {
        return 0;
    }

    /*//////////////////////////////////////////////////////////////
                            DEPOSITS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Deposits `assets` of underlying tokens and mints vault shares to `receiver`.
     * @dev A modified version of the ERC‑4626 `deposit` function that includes a slippage check.
     * @param assets Amount of underlying tokens to deposit.
     * @param receiver Address to receive the minted vault shares.
     * @param minSharesOut Minimum amount of shares that the user expects to receive.
     * @return shares Amount of vault shares minted to the `receiver`.
     */
    function deposit(uint256 assets, address receiver, uint256 minSharesOut) external returns (uint256 shares) {
        shares = previewDeposit(assets);
        if (shares < minSharesOut) revert SlippageExceeded();
        _deposit(msg.sender, receiver, assets, shares);
    }

    /*//////////////////////////////////////////////////////////////
                            WITHDRAWALS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Queues a withdrawal request that becomes claimable after `withdrawalDelay`.
     * @param shares Amount of vault shares to redeem.
     * @param minAssetsOut Minimum amount of assets that the user expects to receive when claiming the withdrawal.
     * @return assets Amount of assets to be withdrawn, based on the current exchange rate.
     */
    function queueWithdrawal(
        uint256 shares,
        uint256 minAssetsOut
    )
        external
        nonReentrant
        whenNotPaused
        onlyWhitelistedUser(msg.sender)
        returns (uint256 assets)
    {
        IERC20Upgradeable shareToken = IERC20Upgradeable(address(this));

        if (shares == 0) revert CannotWithdrawZeroShares();
        if (shares > shareToken.balanceOf(msg.sender)) revert InsufficientShares();
        if (userWithdrawalIds[msg.sender].length >= maxNumberOfWithdrawalsPerUser) revert WithdrawalLimitReached();

        // Snapshots the minimum amount of assets that a user will receive when they claim the withdrawal and
        // adds it to the total assets queued for withdrawal
        assets = previewRedeem(shares);
        if (assets == 0) revert NoAssetsToQueue();
        if (assets < minAssetsOut) revert SlippageExceeded();
        totalAssetsQueuedForWithdrawal += assets;

        // Transfer vault shares to the vault itself to lock them for the withdrawal
        shareToken.safeTransferFrom(msg.sender, address(this), shares);

        // Create a new withdrawal request
        uint256 withdrawalId = ++withdrawalCounter;
        uint256 unlockTime = block.timestamp + withdrawalDelay;

        withdrawals[withdrawalId] = WithdrawalRequest({
            withdrawalId: withdrawalId,
            user: msg.sender,
            assets: assets,
            shares: shares,
            unlockTime: unlockTime,
            claimed: false
        });
        userWithdrawalIds[msg.sender].push(withdrawalId);

        emit WithdrawalQueued(withdrawalId, msg.sender, assets, shares, unlockTime);
    }

    /**
     * @notice Completes a previously queued withdrawal request.
     * @param withdrawalId The ID of the withdrawal request to claim.
     * @return assets The amount of assets withdrawn, based on the current exchange rate.
     */
    function claimWithdrawal(uint256 withdrawalId) external nonReentrant returns (uint256 assets) {
        WithdrawalRequest storage req = withdrawals[withdrawalId];

        if (req.user == address(0)) revert WithdrawalDoesNotExist();
        if (req.user != msg.sender) revert NotYourWithdrawal();
        if (req.assets == 0) revert NothingQueued();
        if (block.timestamp < req.unlockTime) revert WithdrawalNotReady();
        if (req.claimed) revert WithdrawalAlreadyClaimed();

        // Finalize the claim
        assets = _finalizeClaim(withdrawalId, req, msg.sender);
    }

    /**
     * @notice Batch claims multiple queued withdrawals.
     * @param withdrawalIds Array of withdrawal IDs to claim.
     */
    function batchClaimWithdrawals(uint256[] calldata withdrawalIds)
        external
        nonReentrant
        onlyRole(OPERATOR_ROLE)
        returns (uint256[] memory assets)
    {
        uint256 length = withdrawalIds.length;

        if (length == 0) revert NoWithdrawalsToClaim();
        if (length > maxBatchClaimWithdrawals) revert TooManyWithdrawalsInBatch();

        assets = new uint256[](length);

        uint256 processedCount = 0;

        for (uint256 i = 0; i < length; ++i) {
            uint256 withdrawalId = withdrawalIds[i];
            WithdrawalRequest storage req = withdrawals[withdrawalId];

            if (!_isClaimable(req)) continue; // skip unclaimable requests
            assets[i] = _finalizeClaim(withdrawalId, req, req.user);
            ++processedCount;
        }

        if (processedCount > 0) emit BatchClaimCompleted(processedCount, msg.sender);
    }

    /*/////////////////////////////////////////////////////////////
                            VIEW FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Returns the current exchange rate of 1 vault share to the underlying asset.
    function exchangeRate() external view returns (uint256) {
        return convertToAssets(10 ** decimals());
    }

    /**
     * @notice Returns the amount of excess assets currently present in the vault
     * @dev Excess assets = vault's current balance of an underlying asset ‑ totalAssetsQueuedForWithdrawal
     */
    function getExcessAssets() public view returns (uint256) {
        uint256 assetsInVault = IERC20Upgradeable(asset()).balanceOf(address(this));
        if (assetsInVault <= totalAssetsQueuedForWithdrawal) {
            return 0;
        }
        return assetsInVault - totalAssetsQueuedForWithdrawal;
    }

    /**
     * @notice Returns the withdrawal IDs for a given user
     * @param _user The user for which to retrieve withdrawal IDs
     * @return The withdrawal IDs
     */
    function getUserWithdrawalIds(address _user) external view returns (uint256[] memory) {
        return userWithdrawalIds[_user];
    }

    /**
     * @notice Returns the withdrawal request details for a given withdrawal ID
     * @param _withdrawalId The ID of the withdrawal to retrieve
     * @return The withdrawal request details
     */
    function getWithdrawal(uint256 _withdrawalId) external view returns (WithdrawalRequest memory) {
        return withdrawals[_withdrawalId];
    }

    /**
     * @notice Returns all of the currently unclaimed withdrawals for a given user
     * @param _user The user for which to retrieve withdrawals
     * @return The withdrawals
     */
    function getUserWithdrawals(address _user) public view returns (WithdrawalRequest[] memory) {
        uint256[] memory withdrawalIds = userWithdrawalIds[_user];
        uint256 length = withdrawalIds.length;
        WithdrawalRequest[] memory userWithdrawals = new WithdrawalRequest[](length);

        if (length == 0) {
            return userWithdrawals;
        }

        for (uint256 i = 0; i < length; ++i) {
            userWithdrawals[i] = withdrawals[withdrawalIds[i]];
        }

        return userWithdrawals;
    }

    /**
     * @notice Returns all of the currently pending (non-claimable) withdrawals for a given use
     * @param _user The user for which to retrieve withdrawals
     * @return The withdrawals
     */
    function getPendingUserWithdrawals(address _user) external view returns (WithdrawalRequest[] memory) {
        WithdrawalRequest[] memory userWithdrawals = getUserWithdrawals(_user);
        uint256 length = userWithdrawals.length;

        if (length == 0) {
            return userWithdrawals;
        }

        uint256 pendingCount = 0;
        for (uint256 i = 0; i < length; ++i) {
            if (block.timestamp < userWithdrawals[i].unlockTime) {
                ++pendingCount;
            }
        }

        WithdrawalRequest[] memory pendingWithdrawals = new WithdrawalRequest[](pendingCount);
        uint256 pendingIndex = 0;
        for (uint256 i = 0; i < length; ++i) {
            if (block.timestamp < userWithdrawals[i].unlockTime) {
                pendingWithdrawals[pendingIndex] = userWithdrawals[i];
                ++pendingIndex;
            }
        }

        return pendingWithdrawals;
    }

    /**
     * @notice Returns all of the currently claimable withdrawals for a given user
     * @param _user The user for which to retrieve withdrawals
     * @return The withdrawals
     */
    function getClaimableUserWithdrawals(address _user) external view returns (WithdrawalRequest[] memory) {
        WithdrawalRequest[] memory userWithdrawals = getUserWithdrawals(_user);
        uint256 length = userWithdrawals.length;

        if (length == 0) {
            return userWithdrawals;
        }

        uint256 claimableCount = 0;
        for (uint256 i = 0; i < length; ++i) {
            if (block.timestamp >= userWithdrawals[i].unlockTime) {
                ++claimableCount;
            }
        }

        WithdrawalRequest[] memory claimableWithdrawals = new WithdrawalRequest[](claimableCount);
        uint256 claimableIndex = 0;
        for (uint256 i = 0; i < length; ++i) {
            if (block.timestamp >= userWithdrawals[i].unlockTime) {
                claimableWithdrawals[claimableIndex] = userWithdrawals[i];
                ++claimableIndex;
            }
        }

        return claimableWithdrawals;
    }

    /**
     * @notice Returns whether a withdrawal is ready to be claimed or not
     * @param _withdrawalId The ID of the withdrawal to check
     * @return Whether the withdrawal is ready to be claimed or not
     */
    function isWithdrawalClaimable(uint256 _withdrawalId) external view returns (bool) {
        return _isClaimable(withdrawals[_withdrawalId]);
    }

    /**
     * @notice Returns whether a withdrawal has been claimed or not
     * @param _withdrawalId The ID of the withdrawal to check
     * @return Whether the withdrawal has been claimed or not
     */
    function isWithdrawalClaimed(uint256 _withdrawalId) external view returns (bool) {
        return withdrawals[_withdrawalId].claimed;
    }

    /*//////////////////////////////////////////////////////////////
                            INTERNAL FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Checks if a withdrawal request is claimable.
     * @param req The withdrawal request to check.
     * @return Whether the withdrawal request is claimable or not.
     */
    function _isClaimable(WithdrawalRequest storage req) internal view returns (bool) {
        return (req.user != address(0) && req.assets != 0 && block.timestamp >= req.unlockTime && !req.claimed);
    }

    /**
     * @notice Finalizes the claim of a withdrawal request.
     * @param withdrawalId The ID of the withdrawal request to finalize.
     * @param req The withdrawal request to finalize.
     * @param receiver The address that will receive the withdrawn assets.
     * @return assets The amount of assets withdrawn, based on the current exchange rate.
     */
    function _finalizeClaim(
        uint256 withdrawalId,
        WithdrawalRequest storage req,
        address receiver
    )
        internal
        returns (uint256 assets)
    {
        // Mark the withdrawal as claimed
        req.claimed = true;

        // Remove the withdrawal ID from the user's list of withdrawal IDs (swap & pop)
        uint256[] storage ids = userWithdrawalIds[req.user];
        uint256 length = ids.length;
        for (uint256 i = 0; i < length; ++i) {
            if (ids[i] == withdrawalId) {
                ids[i] = ids[length - 1];
                ids.pop();
                break;
            }
        }

        // Remove the withdrawn assets from the total assets queued for withdrawal
        assets = req.assets;
        totalAssetsQueuedForWithdrawal -= assets;

        // Burn the shares from the vault
        _burn(address(this), req.shares);

        // Transfer the withdrawable amount of assets to the user
        IERC20Upgradeable assetToken = IERC20Upgradeable(asset());
        assetToken.safeTransfer(receiver, assets);

        emit WithdrawalCompleted(withdrawalId, req.user, assets, req.shares);
    }
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControl.sol)

pragma solidity ^0.8.0;

import "./IAccessControlUpgradeable.sol";
import "../utils/ContextUpgradeable.sol";
import "../utils/StringsUpgradeable.sol";
import "../utils/introspection/ERC165Upgradeable.sol";
import "../proxy/utils/Initializable.sol";

/**
 * @dev Contract module that allows children to implement role-based access
 * control mechanisms. This is a lightweight version that doesn't allow enumerating role
 * members except through off-chain means by accessing the contract event logs. Some
 * applications may benefit from on-chain enumerability, for those cases see
 * {AccessControlEnumerable}.
 *
 * Roles are referred to by their `bytes32` identifier. These should be exposed
 * in the external API and be unique. The best way to achieve this is by
 * using `public constant` hash digests:
 *
 * ```solidity
 * bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
 * ```
 *
 * Roles can be used to represent a set of permissions. To restrict access to a
 * function call, use {hasRole}:
 *
 * ```solidity
 * function foo() public {
 *     require(hasRole(MY_ROLE, msg.sender));
 *     ...
 * }
 * ```
 *
 * Roles can be granted and revoked dynamically via the {grantRole} and
 * {revokeRole} functions. Each role has an associated admin role, and only
 * accounts that have a role's admin role can call {grantRole} and {revokeRole}.
 *
 * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
 * that only accounts with this role will be able to grant or revoke other
 * roles. More complex role relationships can be created by using
 * {_setRoleAdmin}.
 *
 * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
 * grant and revoke this role. Extra precautions should be taken to secure
 * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
 * to enforce additional security measures for this role.
 */
abstract contract AccessControlUpgradeable is Initializable, ContextUpgradeable, IAccessControlUpgradeable, ERC165Upgradeable {
    function __AccessControl_init() internal onlyInitializing {
    }

    function __AccessControl_init_unchained() internal onlyInitializing {
    }
    struct RoleData {
        mapping(address => bool) members;
        bytes32 adminRole;
    }

    mapping(bytes32 => RoleData) private _roles;

    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    /**
     * @dev Modifier that checks that an account has a specific role. Reverts
     * with a standardized message including the required role.
     *
     * The format of the revert reason is given by the following regular expression:
     *
     *  /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/
     *
     * _Available since v4.1._
     */
    modifier onlyRole(bytes32 role) {
        _checkRole(role);
        _;
    }

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

    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) public view virtual override returns (bool) {
        return _roles[role].members[account];
    }

    /**
     * @dev Revert with a standard message if `_msgSender()` is missing `role`.
     * Overriding this function changes the behavior of the {onlyRole} modifier.
     *
     * Format of the revert message is described in {_checkRole}.
     *
     * _Available since v4.6._
     */
    function _checkRole(bytes32 role) internal view virtual {
        _checkRole(role, _msgSender());
    }

    /**
     * @dev Revert with a standard message if `account` is missing `role`.
     *
     * The format of the revert reason is given by the following regular expression:
     *
     *  /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/
     */
    function _checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            revert(
                string(
                    abi.encodePacked(
                        "AccessControl: account ",
                        StringsUpgradeable.toHexString(account),
                        " is missing role ",
                        StringsUpgradeable.toHexString(uint256(role), 32)
                    )
                )
            );
        }
    }

    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role's admin, use {_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) public view virtual override returns (bytes32) {
        return _roles[role].adminRole;
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleGranted} event.
     */
    function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleRevoked} event.
     */
    function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }

    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function's
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been revoked `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `account`.
     *
     * May emit a {RoleRevoked} event.
     */
    function renounceRole(bytes32 role, address account) public virtual override {
        require(account == _msgSender(), "AccessControl: can only renounce roles for self");

        _revokeRole(role, account);
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event. Note that unlike {grantRole}, this function doesn't perform any
     * checks on the calling account.
     *
     * May emit a {RoleGranted} event.
     *
     * [WARNING]
     * ====
     * This function should only be called from the constructor when setting
     * up the initial roles for the system.
     *
     * Using this function in any other way is effectively circumventing the admin
     * system imposed by {AccessControl}.
     * ====
     *
     * NOTE: This function is deprecated in favor of {_grantRole}.
     */
    function _setupRole(bytes32 role, address account) internal virtual {
        _grantRole(role, account);
    }

    /**
     * @dev Sets `adminRole` as ``role``'s admin role.
     *
     * Emits a {RoleAdminChanged} event.
     */
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        bytes32 previousAdminRole = getRoleAdmin(role);
        _roles[role].adminRole = adminRole;
        emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleGranted} event.
     */
    function _grantRole(bytes32 role, address account) internal virtual {
        if (!hasRole(role, account)) {
            _roles[role].members[account] = true;
            emit RoleGranted(role, account, _msgSender());
        }
    }

    /**
     * @dev Revokes `role` from `account`.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleRevoked} event.
     */
    function _revokeRole(bytes32 role, address account) internal virtual {
        if (hasRole(role, account)) {
            _roles[role].members[account] = false;
            emit RoleRevoked(role, account, _msgSender());
        }
    }

    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[49] private __gap;
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC4626.sol)

pragma solidity ^0.8.0;

import "../ERC20Upgradeable.sol";
import "../utils/SafeERC20Upgradeable.sol";
import "../../../interfaces/IERC4626Upgradeable.sol";
import "../../../utils/math/MathUpgradeable.sol";
import "../../../proxy/utils/Initializable.sol";

/**
 * @dev Implementation of the ERC4626 "Tokenized Vault Standard" as defined in
 * https://eips.ethereum.org/EIPS/eip-4626[EIP-4626].
 *
 * This extension allows the minting and burning of "shares" (represented using the ERC20 inheritance) in exchange for
 * underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
 * the ERC20 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 uses virtual assets and shares to 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, 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].
 * ====
 *
 * _Available since v4.7._
 */
abstract contract ERC4626Upgradeable is Initializable, ERC20Upgradeable, IERC4626Upgradeable {
    using MathUpgradeable for uint256;

    IERC20Upgradeable private _asset;
    uint8 private _underlyingDecimals;

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

    function __ERC4626_init_unchained(IERC20Upgradeable asset_) internal onlyInitializing {
        (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(IERC20Upgradeable asset_) private view returns (bool, uint8) {
        (bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
            abi.encodeWithSelector(IERC20MetadataUpgradeable.decimals.selector)
        );
        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(IERC20MetadataUpgradeable, ERC20Upgradeable) returns (uint8) {
        return _underlyingDecimals + _decimalsOffset();
    }

    /** @dev See {IERC4626-asset}. */
    function asset() public view virtual override returns (address) {
        return address(_asset);
    }

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

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

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

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

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

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

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

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

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

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

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

    /** @dev See {IERC4626-deposit}. */
    function deposit(uint256 assets, address receiver) public virtual override returns (uint256) {
        require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max");

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

        return shares;
    }

    /** @dev See {IERC4626-mint}.
     *
     * As opposed to {deposit}, minting is allowed even if the vault is in a state where the price of a share is zero.
     * In this case, the shares will be minted without requiring any assets to be deposited.
     */
    function mint(uint256 shares, address receiver) public virtual override returns (uint256) {
        require(shares <= maxMint(receiver), "ERC4626: mint more than max");

        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 override returns (uint256) {
        require(assets <= maxWithdraw(owner), "ERC4626: withdraw more than max");

        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 override returns (uint256) {
        require(shares <= maxRedeem(owner), "ERC4626: redeem more than max");

        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, MathUpgradeable.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, MathUpgradeable.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 {
        // If _asset is ERC777, `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
        SafeERC20Upgradeable.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 {
        if (caller != owner) {
            _spendAllowance(owner, caller, shares);
        }

        // If _asset is ERC777, `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);
        SafeERC20Upgradeable.safeTransfer(_asset, receiver, assets);

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

    function _decimalsOffset() internal view virtual returns (uint8) {
        return 0;
    }

    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[49] private __gap;
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol)

pragma solidity ^0.8.2;

import "../../utils/AddressUpgradeable.sol";

/**
 * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
 * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
 * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
 * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
 *
 * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
 * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
 * case an upgrade adds a module that needs to be initialized.
 *
 * For example:
 *
 * [.hljs-theme-light.nopadding]
 * ```solidity
 * contract MyToken is ERC20Upgradeable {
 *     function initialize() initializer public {
 *         __ERC20_init("MyToken", "MTK");
 *     }
 * }
 *
 * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
 *     function initializeV2() reinitializer(2) public {
 *         __ERC20Permit_init("MyToken");
 *     }
 * }
 * ```
 *
 * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
 * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
 *
 * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
 * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
 *
 * [CAUTION]
 * ====
 * Avoid leaving a contract uninitialized.
 *
 * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
 * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
 * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
 *
 * [.hljs-theme-light.nopadding]
 * ```
 * /// @custom:oz-upgrades-unsafe-allow constructor
 * constructor() {
 *     _disableInitializers();
 * }
 * ```
 * ====
 */
abstract contract Initializable {
    /**
     * @dev Indicates that the contract has been initialized.
     * @custom:oz-retyped-from bool
     */
    uint8 private _initialized;

    /**
     * @dev Indicates that the contract is in the process of being initialized.
     */
    bool private _initializing;

    /**
     * @dev Triggered when the contract has been initialized or reinitialized.
     */
    event Initialized(uint8 version);

    /**
     * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
     * `onlyInitializing` functions can be used to initialize parent contracts.
     *
     * Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a
     * constructor.
     *
     * Emits an {Initialized} event.
     */
    modifier initializer() {
        bool isTopLevelCall = !_initializing;
        require(
            (isTopLevelCall && _initialized < 1) || (!AddressUpgradeable.isContract(address(this)) && _initialized == 1),
            "Initializable: contract is already initialized"
        );
        _initialized = 1;
        if (isTopLevelCall) {
            _initializing = true;
        }
        _;
        if (isTopLevelCall) {
            _initializing = false;
            emit Initialized(1);
        }
    }

    /**
     * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
     * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
     * used to initialize parent contracts.
     *
     * A reinitializer may be used after the original initialization step. This is essential to configure modules that
     * are added through upgrades and that require initialization.
     *
     * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
     * cannot be nested. If one is invoked in the context of another, execution will revert.
     *
     * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
     * a contract, executing them in the right order is up to the developer or operator.
     *
     * WARNING: setting the version to 255 will prevent any future reinitialization.
     *
     * Emits an {Initialized} event.
     */
    modifier reinitializer(uint8 version) {
        require(!_initializing && _initialized < version, "Initializable: contract is already initialized");
        _initialized = version;
        _initializing = true;
        _;
        _initializing = false;
        emit Initialized(version);
    }

    /**
     * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
     * {initializer} and {reinitializer} modifiers, directly or indirectly.
     */
    modifier onlyInitializing() {
        require(_initializing, "Initializable: contract is not initializing");
        _;
    }

    /**
     * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
     * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
     * to any version. It is recommended to use this to lock implementation contracts that are designed to be called
     * through proxies.
     *
     * Emits an {Initialized} event the first time it is successfully executed.
     */
    function _disableInitializers() internal virtual {
        require(!_initializing, "Initializable: contract is initializing");
        if (_initialized != type(uint8).max) {
            _initialized = type(uint8).max;
            emit Initialized(type(uint8).max);
        }
    }

    /**
     * @dev Returns the highest version that has been initialized. See {reinitializer}.
     */
    function _getInitializedVersion() internal view returns (uint8) {
        return _initialized;
    }

    /**
     * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
     */
    function _isInitializing() internal view returns (bool) {
        return _initializing;
    }
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol)

pragma solidity ^0.8.0;

import "../utils/ContextUpgradeable.sol";
import "../proxy/utils/Initializable.sol";

/**
 * @dev Contract module which allows children to implement an emergency stop
 * mechanism that can be triggered by an authorized account.
 *
 * This module is used through inheritance. It will make available the
 * modifiers `whenNotPaused` and `whenPaused`, which can be applied to
 * the functions of your contract. Note that they will not be pausable by
 * simply including this module, only once the modifiers are put in place.
 */
abstract contract PausableUpgradeable is Initializable, ContextUpgradeable {
    /**
     * @dev Emitted when the pause is triggered by `account`.
     */
    event Paused(address account);

    /**
     * @dev Emitted when the pause is lifted by `account`.
     */
    event Unpaused(address account);

    bool private _paused;

    /**
     * @dev Initializes the contract in unpaused state.
     */
    function __Pausable_init() internal onlyInitializing {
        __Pausable_init_unchained();
    }

    function __Pausable_init_unchained() internal onlyInitializing {
        _paused = false;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    modifier whenNotPaused() {
        _requireNotPaused();
        _;
    }

    /**

Tags:
ERC20, ERC165, Proxy, Mintable, Pausable, Swap, Yield, Upgradeable, Factory|addr:0xb8d6cf158714fcb8639feb15e0f8082d6f402f38|verified:true|block:23382680|tx:0xece52af94e410f925c2cfd799e872ccfd5072e7b9e827741d56dace0f473f7cf|first_check:1758114243

Submitted on: 2025-09-17 15:04:04

Comments

Log in to comment.

No comments yet.