UltraVault

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/vaults/UltraVault.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.28;

import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IPriceSource } from "src/interfaces/IPriceSource.sol";
import { Fees, IUltraVaultEvents, IUltraVaultErrors } from "src/interfaces/IUltraVault.sol";
import { AddressUpdateProposal } from "src/utils/AddressUpdates.sol";
import { FixedPointMathLib } from "src/utils/FixedPointMathLib.sol";
import { BaseControlledAsyncRedeem, BaseControlledAsyncRedeemInitParams } from "src/vaults/BaseControlledAsyncRedeem.sol";
import { OPERATOR_ROLE } from "src/utils/Roles.sol";

// keccak256(abi.encode(uint256(keccak256("ultrayield.storage.UltraVault")) - 1)) & ~bytes32(uint256(0xff));
bytes32 constant ULTRA_VAULT_STORAGE_LOCATION = 0x4a2c313ed01a3f9b3ba2e7d99f5ac7985ad5a3c0482c127738b38df017064300;

/// @dev Initialization parameters for UltraVault
struct UltraVaultInitParams {
    // Owner of the vault
    address owner;
    // Underlying asset address
    address asset;
    // Vault name
    string name;
    // Vault symbol
    string symbol;
    // Oracle for assets exchange rate
    address rateProvider;
    // Fee recipient
    address feeRecipient;
    // Fee configuration
    Fees fees;
    // Oracle to use for pricing
    address oracle;
    // FundsHolder which will manage the assets
    address fundsHolder;
}

/// @title UltraVault
/// @notice ERC-7540 compliant async redeem vault with UltraVaultOracle pricing and multisig asset management
contract UltraVault is BaseControlledAsyncRedeem, IUltraVaultEvents, IUltraVaultErrors {
    using FixedPointMathLib for uint256;
    using SafeERC20 for IERC20;

    ///////////////
    // Constants //
    ///////////////

    uint256 internal constant ONE_YEAR = 365 * 24 * 60 * 60; // 31_536_000 seconds
    uint256 internal constant ONE_UNIT = 1e18; // Default scale
    uint64 internal constant ONE_PERCENT = uint64(ONE_UNIT) / 100;
    uint64 internal constant MAX_PERFORMANCE_FEE = 30 * ONE_PERCENT; // 30%
    uint64 internal constant MAX_MANAGEMENT_FEE = 5 * ONE_PERCENT; // 5%
    uint64 internal constant MAX_WITHDRAWAL_FEE = ONE_PERCENT; // 1%

    /////////////
    // Storage //
    /////////////

    /// @custom:storage-location erc7201:ultrayield.storage.UltraVault
    struct UltraVaultStorage {
        address fundsHolder;
        IPriceSource oracle;
        AddressUpdateProposal proposedFundsHolder;
        AddressUpdateProposal proposedOracle;
        address feeRecipient;
        Fees fees;
    }

    function _getUltraVaultStorage() private pure returns (UltraVaultStorage storage $) {
        assembly {
            $.slot := ULTRA_VAULT_STORAGE_LOCATION
        }
    }
    
    //////////
    // Init //
    //////////

    /// @notice Disable implementation's initializer
    constructor() {
        _disableInitializers();
    }

    /// @param params Initialization parameters struct
    function initialize(
        UltraVaultInitParams memory params
    ) external initializer {
        require(params.fundsHolder != address(0), ZeroFundsHolderAddress());
        require(params.oracle != address(0), ZeroOracleAddress());
        require(params.feeRecipient != address(0), ZeroFeeRecipientAddress());

        UltraVaultStorage storage $ = _getUltraVaultStorage();
        $.fundsHolder = params.fundsHolder;
        $.oracle = IPriceSource(params.oracle);
        _pause();
        
        // Calling at the end since we need oracle to be setup
        super.initialize(BaseControlledAsyncRedeemInitParams({
            owner: params.owner,
            asset: params.asset,
            name: params.name,
            symbol: params.symbol,
            rateProvider: params.rateProvider
        }));

        $.feeRecipient = params.feeRecipient;
        emit FeesRecipientUpdated(address(0), params.feeRecipient);
        _setFees(params.fees);
    }

    /////////////
    // Getters //
    /////////////

    /// @notice Get the funds holder address
    function fundsHolder() public view returns (address) {
        return _getUltraVaultStorage().fundsHolder;
    }

    /// @notice Get the oracle address
    function oracle() public view returns (IPriceSource) {
        return _getUltraVaultStorage().oracle;
    }

    /// @notice Get the proposed funds holder
    function proposedFundsHolder() public view returns (address, uint256) {
        AddressUpdateProposal memory proposal = _getUltraVaultStorage().proposedFundsHolder;
        return (proposal.addr, proposal.timestamp);
    }

    /// @notice Get the proposed oracle
    function proposedOracle() public view returns (address, uint256) {
        AddressUpdateProposal memory proposal = _getUltraVaultStorage().proposedOracle;
        return (proposal.addr, proposal.timestamp);
    }

    ////////////////////////////
    // Initial Balances Setup //
    ////////////////////////////

    /// @notice Setup initial balances in the vault without depositing the funds
    /// @notice We expect the funds to be separately sent to funds holder
    /// @param users Array of users to setup balances
    /// @param shares Shares of respective users
    /// @dev Reverts if arrays length mismatch
    function setupInitialBalances(
        address[] memory users,
        uint256[] memory shares
    ) external onlyOwner {
        require(totalSupply() == 0, CannotSetBalancesInNonEmptyVault());
        require(users.length == shares.length, InputLengthMismatch());

        for (uint256 i; i < users.length; i++) {
            _mint(users[i], shares[i]);
        }
    }

    ////////////////
    // Accounting //
    ////////////////

    /// @notice Get total assets managed by fundsHolder
    function totalAssets() public view override returns (uint256) {
        return oracle().getQuote(totalSupply(), share(), asset());
    }

    ////////////////////
    // Hook Overrides //
    ////////////////////

    /// @dev After deposit hook - collect fees and send funds to fundsHolder
    function afterDeposit(address _asset, uint256 assets, uint256) internal override {
        IERC20(_asset).safeTransfer(fundsHolder(), assets);
    }

    /// @dev Before fulfill redeem - transfer funds from fundsHolder to vault
    /// @dev "assets" will already be correct given the token user requested
    function beforeFulfillRedeem(address _asset, uint256 assets, uint256) internal override {
        IERC20(_asset).safeTransferFrom(fundsHolder(), address(this), assets);
    }

    ////////////////////
    // Deposit & Mint //
    ////////////////////

    /// @dev Collects fees right before making a deposit
    function _depositAsset(
        address _asset,
        uint256 assets,
        address receiver
    ) internal override returns (uint256 shares) {
        _collectFees();
        shares = super._depositAsset(_asset, assets, receiver);
    }

    /// @dev Collects fees right before performing a mint
    function _mintWithAsset(
        address _asset,
        uint256 shares,
        address receiver
    ) internal override returns (uint256 assets) {
        _collectFees();
        assets = super._mintWithAsset(_asset, shares, receiver);
    }

    ////////////////////////
    // Redeem Fulfillment //
    ////////////////////////

    /// @notice Fulfill redeem request
    /// @param _asset Asset
    /// @param shares Amount to redeem
    /// @param controller Controller address
    /// @return assets Amount of claimable assets
    function fulfillRedeem(
        address _asset,
        uint256 shares,
        address controller
    ) external override onlyRole(OPERATOR_ROLE) returns (uint256 assets) {
        // Collect fees accrued to date
        _collectFees();
        
        // Convert shares to underlying assets, then to asset units
        uint256 underlyingAssets = convertToAssets(shares);
        assets = _convertFromUnderlying(_asset, underlyingAssets);
        
        // Calculate the withdrawal incentive fee directly in asset units
        uint256 withdrawalFee = assets.mulDivDown(getFees().withdrawalFee, ONE_UNIT);

        // Fulfill request with asset units (base contract expects asset units)
        uint256 withdrawalAmount = assets - withdrawalFee;
        _fulfillRedeem(_asset, withdrawalAmount, shares, controller);

        // Burn shares
        _burn(address(this), shares);

        // Collect withdrawal fee in asset units
        _transferWithdrawalFeeInAsset(_asset, withdrawalFee);

        // Return the amount in asset units for consistency with base contract
        return withdrawalAmount;
    }

    /// @notice Fulfill multiple redeem requests
    /// @param assets Array of assets
    /// @param shares Array of share amounts
    /// @param controllers Array of controllers
    /// @return Array of fulfilled amounts in requested asset
    /// @dev Reverts if arrays length mismatch
    /// @dev Collects withdrawal fee to incentivize manager
    function fulfillMultipleRedeems(
        address[] memory assets,
        uint256[] memory shares,
        address[] memory controllers
    ) external override onlyRole(OPERATOR_ROLE) returns (uint256[] memory) {
        // Check input length
        uint256 length = assets.length;
        require(length == shares.length && length == controllers.length, InputLengthMismatch());

        // Collect fees accrued to date
        _collectFees();

        // Prepare values for calculations
        uint256 totalShares;
        uint256 _totalAssets = totalAssets();
        uint256 _totalSupply = totalSupply();
        uint256 withdrawalFeeRate = getFees().withdrawalFee;
        uint256[] memory result = new uint256[](length);
        for (uint256 i; i < length; ) {
            // Resolve redeem amount in the requested asset
            address _asset = assets[i];
            uint256 _shares = shares[i];
            address _controller = controllers[i];
            uint256 underlyingAssets = _optimizedConvertToAssets(_shares, _totalAssets, _totalSupply);
            uint256 convertedAssets = _convertFromUnderlying(_asset, underlyingAssets);

            // Calculate and transfer withdrawal fee (in asset units)
            uint256 withdrawalFee = convertedAssets.mulDivDown(withdrawalFeeRate, ONE_UNIT);
            _transferWithdrawalFeeInAsset(_asset, withdrawalFee);

            // Fulfill redeem
            uint256 assetsFulfilled = _fulfillRedeem(_asset, convertedAssets - withdrawalFee, _shares, _controller);
            result[i] = assetsFulfilled;
            totalShares += _shares;

            unchecked { ++i; }
        }

        // Burn shares
        _burn(address(this), totalShares);

        return result;
    }

    //////////////////////////
    // Funds Holder Updates //
    //////////////////////////

    /// @notice Propose fundsHolder change, can be accepted after delay
    /// @param newFundsHolder New fundsHolder address
    /// @dev changing the holder should be used only in case of multisig upgrade after funds transfer
    function proposeFundsHolder(address newFundsHolder) external onlyOwner {
        require(newFundsHolder != address(0), ZeroFundsHolderAddress());

        _getUltraVaultStorage().proposedFundsHolder = AddressUpdateProposal({
            addr: newFundsHolder,
            timestamp: uint96(block.timestamp)
        });

        emit FundsHolderProposed(newFundsHolder);
    }

    /// @notice Accept proposed fundsHolder
    /// @dev Pauses vault to ensure oracle setup and prevent deposits with faulty prices
    /// @dev Oracle must be switched before unpausing
    function acceptFundsHolder(address newFundsHolder) external onlyOwner {
        (address proposedHolder, uint256 proposalTimestamp) = proposedFundsHolder();
        require(proposedHolder != address(0), NoPendingFundsHolderUpdate());
        require(proposedHolder == newFundsHolder, ProposedFundsHolderMismatch());
        require(block.timestamp >= proposalTimestamp + ADDRESS_UPDATE_TIMELOCK, CannotAcceptFundsHolderYet());
        require(block.timestamp <= proposalTimestamp + MAX_ADDRESS_UPDATE_WAIT, FundsHolderUpdateExpired());

        emit FundsHolderChanged(fundsHolder(), newFundsHolder);

        UltraVaultStorage storage $ = _getUltraVaultStorage();
        $.fundsHolder = newFundsHolder;
        delete $.proposedFundsHolder;

        // Pause to manually check the setup by operators
        _pause();
    }

    ////////////////////
    // Oracle Updates //
    ////////////////////

    /// @notice Propose new oracle for owner acceptance after delay
    /// @param newOracle Address of the new oracle
    function proposeOracle(address newOracle) external onlyOwner {
        require(newOracle != address(0), ZeroOracleAddress());

        _getUltraVaultStorage().proposedOracle = AddressUpdateProposal({
            addr: newOracle,
            timestamp: uint96(block.timestamp)
        });

        emit OracleProposed(newOracle);
    }

    /// @notice Accept proposed oracle
    /// @dev Pauses vault to ensure oracle setup and prevent deposits with faulty prices
    /// @dev Oracle must be switched before unpausing
    function acceptProposedOracle(address newOracle) external onlyOwner {
        (address pendingOracle, uint256 proposalTimestamp) = proposedOracle();
        require(pendingOracle != address(0), NoOracleProposed());
        require(pendingOracle == newOracle, ProposedOracleMismatch());
        require(block.timestamp >= proposalTimestamp + ADDRESS_UPDATE_TIMELOCK, CannotAcceptOracleYet());
        require(block.timestamp <= proposalTimestamp + MAX_ADDRESS_UPDATE_WAIT, OracleUpdateExpired());

        emit OracleUpdated(address(oracle()), newOracle);

        UltraVaultStorage storage $ = _getUltraVaultStorage();
        $.oracle = IPriceSource(newOracle);
        delete $.proposedOracle;

        // Pause to manually check the setup by operators
        _pause();
    }

    //////////
    // Fees //
    //////////

    /// @notice Get vault fee parameters
    function getFees() public view returns (Fees memory) {
        return _getUltraVaultStorage().fees;
    }

    /// @notice Get vault fee recipient
    function feeRecipient() public view returns (address) {
        return _getUltraVaultStorage().feeRecipient;
    }

    /// @notice Get total accrued fees
    function accruedFees() external view returns (uint256) {
        Fees memory fees = getFees();
        Totals memory totals = _snapshotTotals();
        return _calculateAccruedPerformanceFee(fees, totals) + _calculateAccruedManagementFee(fees, totals);
    }

    /// @notice Get accrued management fees
    function accruedManagementFees() external view returns (uint256) {
        Fees memory fees = getFees();
        Totals memory totals = _snapshotTotals();
        return _calculateAccruedManagementFee(fees, totals);
    }

    /// @notice Get accrued performance fees
    function accruedPerformanceFees() external view returns (uint256) {
        Fees memory fees = getFees();
        Totals memory totals = _snapshotTotals();
        return _calculateAccruedPerformanceFee(fees, totals);
    }

    /// @notice Get the withdrawal fee
    function calculateWithdrawalFee(uint256 assets) external view returns (uint256) {
        return assets.mulDivDown(getFees().withdrawalFee, ONE_UNIT);
    }

    /// @notice Update vault's fee recipient
    /// @param newFeeRecipient New fee recipient
    /// @dev Collects pending fees before update
    function setFeeRecipient(address newFeeRecipient) public onlyOwner whenNotPaused {
        require(newFeeRecipient != address(0), ZeroFeeRecipientAddress());

        address currentFeeRecipient = feeRecipient();
        if (currentFeeRecipient != newFeeRecipient) {
            _collectFees();
            _getUltraVaultStorage().feeRecipient = newFeeRecipient;
            emit FeesRecipientUpdated(currentFeeRecipient, newFeeRecipient);
        }
    }

    /// @notice Update vault fees
    /// @param fees New fee configuration
    /// @dev Reverts if fees exceed limits (30% performance, 5% management, 1% withdrawal)
    /// @dev Collects pending fees before update
    function setFees(Fees memory fees) public onlyOwner whenNotPaused {
        _collectFees();
        _setFees(fees);
    }

    /// @notice Mint fees as shares to recipient
    /// @dev Updates fee-related variables
    function collectFees() external onlyOwner whenNotPaused {
        _collectFees();
    }

    ////////////////////////
    // Internal functions //
    ////////////////////////

    /// @dev Struct wrapping total assets, supply and share value to optimize fee calculations
    struct Totals {
        uint256 totalAssets;
        uint256 totalSupply;
        uint256 shareValue;
    }

    function _setFees(Fees memory fees) internal {
        require(
            fees.performanceFee <= MAX_PERFORMANCE_FEE &&
            fees.managementFee <= MAX_MANAGEMENT_FEE &&
            fees.withdrawalFee <= MAX_WITHDRAWAL_FEE,
            InvalidFees()
        );

        fees.lastUpdateTimestamp = uint64(block.timestamp);

        Fees memory currentFees = getFees();
        if (currentFees.highwaterMark == 0) {
            fees.highwaterMark = 10 ** IERC20Metadata(asset()).decimals();
        } else {
            fees.highwaterMark = currentFees.highwaterMark;
        }
        _getUltraVaultStorage().fees = fees;

        emit FeesUpdated(currentFees, fees);
    }

    function _collectFees() internal {
        // Prepare inputs for calculations
        Fees memory fees = getFees();
        Totals memory totals = _snapshotTotals();

        // Calculate fees
        uint256 managementFee = _calculateAccruedManagementFee(fees, totals);
        uint256 performanceFee = _calculateAccruedPerformanceFee(fees, totals);
        uint256 totalFeesInAssets = managementFee + performanceFee;

        // Update highwater mark
        if (totals.shareValue > fees.highwaterMark) {
            _getUltraVaultStorage().fees.highwaterMark = totals.shareValue;
        }
        // Collect fees as shares if non-zero
        if (totalFeesInAssets > 0) {
            // Update timestamp
            _getUltraVaultStorage().fees.lastUpdateTimestamp = uint64(block.timestamp);

            // Convert fees to shares
            uint256 feesInShares = _optimizedConvertToShares(totalFeesInAssets, totals.totalAssets, totals.totalSupply);

            // Mint shares to fee recipient
            _mint(feeRecipient(), feesInShares);

            emit FeesCollected(feesInShares, managementFee, performanceFee);
        }
    }

    function _snapshotTotals() internal view returns (Totals memory) {
        uint256 _totalAssets = totalAssets();
        uint256 _totalSupply = totalSupply();
        return Totals({
            totalAssets: _totalAssets,
            totalSupply: _totalSupply,
            shareValue: _optimizedConvertToAssets(10 ** decimals(), _totalAssets, _totalSupply)
        });
    }

    /// @notice Calculate accrued performance fee
    /// @return accruedPerformanceFee Fee amount in asset token
    /// @dev Based on high water mark value
    function _calculateAccruedPerformanceFee(
        Fees memory fees, 
        Totals memory totals
    ) internal view returns (uint256) {
        uint256 performanceFee = fees.performanceFee;
        if (performanceFee == 0 || totals.shareValue <= fees.highwaterMark) {
            return 0;
        }
        return performanceFee.mulDivDown(
            (totals.shareValue - fees.highwaterMark) * totals.totalSupply,
            (10 ** (18 + decimals()))
        );
    }

    /// @notice Calculate accrued management fee
    /// @return accruedManagementFee Fee amount in asset token
    /// @dev Annualized per minute, based on 525_600 minutes or 31_536_000 seconds per year
    function _calculateAccruedManagementFee(Fees memory fees, Totals memory totals) internal view returns (uint256) {
        uint256 managementFee = fees.managementFee;
        if (managementFee == 0) {
            return 0;
        }
        uint256 timePassed = block.timestamp - fees.lastUpdateTimestamp;
        return managementFee.mulDivDown(totals.totalAssets * timePassed, ONE_YEAR) / ONE_UNIT;
    }

    /// @notice Transfer withdrawal fee to fee recipient
    /// @param _asset Asset to transfer
    /// @param fee Amount to transfer
    function _transferWithdrawalFeeInAsset(
        address _asset,
        uint256 fee
    ) internal {
        if (fee > 0) {
            // Transfer the fee from the fundsHolder to the fee recipient
            IERC20(_asset).safeTransferFrom(fundsHolder(), feeRecipient(), fee);
            emit WithdrawalFeeCollected(fee);
        }
    }
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";

/**
 * @title SafeERC20
 * @dev Wrappers around ERC-20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    /**
     * @dev An operation with an ERC-20 token failed.
     */
    error SafeERC20FailedOperation(address token);

    /**
     * @dev Indicates a failed `decreaseAllowance` request.
     */
    error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);

    /**
     * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     */
    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
    }

    /**
     * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
     * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
     */
    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
    }

    /**
     * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
     */
    function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
        return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));
    }

    /**
     * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
     */
    function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
        return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));
    }

    /**
     * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        uint256 oldAllowance = token.allowance(address(this), spender);
        forceApprove(token, spender, oldAllowance + value);
    }

    /**
     * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
     * value, non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
        unchecked {
            uint256 currentAllowance = token.allowance(address(this), spender);
            if (currentAllowance < requestedDecrease) {
                revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
            }
            forceApprove(token, spender, currentAllowance - requestedDecrease);
        }
    }

    /**
     * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
     * to be set to zero before setting it to a non-zero value, such as USDT.
     *
     * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
     * only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
     * set here.
     */
    function forceApprove(IERC20 token, address spender, uint256 value) internal {
        bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));

        if (!_callOptionalReturnBool(token, approvalCall)) {
            _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
            _callOptionalReturn(token, approvalCall);
        }
    }

    /**
     * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
     * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * Reverts if the returned value is other than `true`.
     */
    function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
        if (to.code.length == 0) {
            safeTransfer(token, to, value);
        } else if (!token.transferAndCall(to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
     * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * Reverts if the returned value is other than `true`.
     */
    function transferFromAndCallRelaxed(
        IERC1363 token,
        address from,
        address to,
        uint256 value,
        bytes memory data
    ) internal {
        if (to.code.length == 0) {
            safeTransferFrom(token, from, to, value);
        } else if (!token.transferFromAndCall(from, to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
     * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
     * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
     * once without retrying, and relies on the returned value to be true.
     *
     * Reverts if the returned value is other than `true`.
     */
    function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
        if (to.code.length == 0) {
            forceApprove(token, to, value);
        } else if (!token.approveAndCall(to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     *
     * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
     */
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        uint256 returnSize;
        uint256 returnValue;
        assembly ("memory-safe") {
            let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            // bubble errors
            if iszero(success) {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0, returndatasize())
                revert(ptr, returndatasize())
            }
            returnSize := returndatasize()
            returnValue := mload(0)
        }

        if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     *
     * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
     */
    function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
        bool success;
        uint256 returnSize;
        uint256 returnValue;
        assembly ("memory-safe") {
            success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            returnSize := returndatasize()
            returnValue := mload(0)
        }
        return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
    }
}
"
    },
    "lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol)

pragma solidity ^0.8.20;

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

/**
 * @dev Interface for the optional metadata functions from the ERC-20 standard.
 */
interface IERC20Metadata is IERC20 {
    /**
     * @dev Returns the name of the token.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the symbol of the token.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the decimals places of the token.
     */
    function decimals() external view returns (uint8);
}
"
    },
    "src/interfaces/IPriceSource.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;

interface IPriceSource {
    /// @notice Get one-sided price quote
    /// @param inAmount Amount of base token to convert
    /// @param base Token being priced
    /// @param quote Token used as unit of account
    /// @return outAmount Amount of quote token equivalent to inAmount of base
    /// @dev Assumes no price spread
    function getQuote(
        uint256 inAmount,
        address base,
        address quote
    ) external view returns (uint256 outAmount);
}
"
    },
    "src/interfaces/IUltraVault.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;

import { IBaseVault, IBaseVaultEvents, IBaseVaultErrors } from "src/interfaces/IBaseVault.sol";
import { IPriceSource } from "src/interfaces/IPriceSource.sol";

/// @dev Vault fee configuration
struct Fees {
    // Performance fee rate (100% = 1e18)
    uint64 performanceFee;
    // Management fee rate (100% = 1e18)
    uint64 managementFee;
    // Withdrawal fee rate (100% = 1e18)
    uint64 withdrawalFee;
    // Last fee update timestamp
    uint64 lastUpdateTimestamp;
    // High water mark for performance fees
    uint256 highwaterMark;
}

interface IUltraVaultEvents {
    event FundsHolderProposed(address indexed proposedFundsHolder);
    event FundsHolderChanged(address indexed oldFundsHolder, address indexed newFundsHolder);
    event OracleProposed(address indexed proposedOracle);
    event OracleUpdated(address indexed oldOracle, address indexed newOracle);
    event FeesRecipientUpdated(address oldRecipient, address newRecipient);
    event FeesUpdated(Fees oldFees, Fees newFees);
    event FeesCollected(uint256 shares, uint256 managementFee, uint256 performanceFee);
    event WithdrawalFeeCollected(uint256 amount);
}

interface IUltraVaultErrors {
    error ZeroFundsHolderAddress();
    error ZeroOracleAddress();
    error ZeroFeeRecipientAddress();
    error NoPendingFundsHolderUpdate();
    error ProposedFundsHolderMismatch();
    error CannotAcceptFundsHolderYet();
    error FundsHolderUpdateExpired();
    error NoOracleProposed();
    error ProposedOracleMismatch();
    error CannotAcceptOracleYet();
    error OracleUpdateExpired();
    error CannotSetBalancesInNonEmptyVault();
    error InvalidFees();
}

/// @title IUltraVault
/// @notice A simplified interface for use in other contracts
interface IUltraVault is IBaseVault, IUltraVaultEvents, IUltraVaultErrors {
    ////////////////////
    // View Functions //
    ////////////////////

    /// @notice Returns the funds holder address of the vault
    /// @return fundsHolder The address of the funds holder
    function fundsHolder() external view returns (address);

    /// @notice Returns the oracle address of the vault
    /// @return oracle The address of the oracle
    function oracle() external view returns (IPriceSource);

    /// @notice Returns the current fees configuration
    /// @return fees The current fees configuration
    function getFees() external view returns (Fees memory);

    /// @notice Get vault fee recipient
    function feeRecipient() external view returns (address);

    /// @notice Get total accrued fees
    function accruedFees() external view returns (uint256);

    /// @notice Get accrued management fees
    function accruedManagementFees() external view returns (uint256);

    /// @notice Get accrued performance fees
    function accruedPerformanceFees() external view returns (uint256);

    /// @notice Get the withdrawal fee
    function calculateWithdrawalFee(uint256 assets) external view returns (uint256);

    /// @notice Get the proposed funds holder
    function proposedFundsHolder() external view returns (address, uint256);
    
    /// @notice Get the proposed oracle
    function proposedOracle() external view returns (address, uint256);

    /////////////////////
    // Admin Functions //
    /////////////////////

    /// @notice Update vault's fee recipient
    /// @param newFeeRecipient New fee recipient
    function setFeeRecipient(address newFeeRecipient) external;

    /// @notice Update vault fees
    /// @param fees New fee configuration
    function setFees(Fees memory fees) external;

    /// @notice Mint fees as shares to fee recipient
    function collectFees() external;

    /// @notice Propose fundsHolder change, can be accepted after delay
    /// @param newFundsHolder New fundsHolder address
    /// @dev changing the holder should be used only in case of multisig upgrade after funds transfer
    function proposeFundsHolder(address newFundsHolder) external;

    /// @notice Accept proposed fundsHolder
    /// @dev Pauses vault to ensure oracle setup and prevent deposits with faulty prices
    /// @dev Oracle must be switched before unpausing
    function acceptFundsHolder(address newFundsHolder) external;

    /// @notice Propose new oracle for owner acceptance after delay
    /// @param newOracle Address of the new oracle
    function proposeOracle(address newOracle) external;

    /// @notice Accept proposed oracle
    /// @dev Pauses vault to ensure oracle setup and prevent deposits with faulty prices
    /// @dev Oracle must be switched before unpausing
    function acceptProposedOracle(address newOracle) external;

    /// @notice Setup initial balances in the vault without depositing the funds
    /// @notice We expect the funds to be separately sent to funds holder
    /// @param users Array of users to setup balances
    /// @param shares Shares of respective users
    /// @dev Reverts if arrays length mismatch
    function setupInitialBalances(
        address[] memory users,
        uint256[] memory shares
    ) external;
} 
"
    },
    "src/utils/AddressUpdates.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.28;

struct AddressUpdateProposal {
    address addr;
    uint96 timestamp;
}
"
    },
    "src/utils/FixedPointMathLib.sol": {
      "content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;

/// @notice Arithmetic library with operations for fixed-point numbers.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol)
/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)
library FixedPointMathLib {
    /*//////////////////////////////////////////////////////////////
                    SIMPLIFIED FIXED POINT OPERATIONS
    //////////////////////////////////////////////////////////////*/

    uint256 internal constant MAX_UINT256 = 2**256 - 1;

    uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s.

    function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
        return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down.
    }

    function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
        return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up.
    }

    function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
        return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down.
    }

    function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
        return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up.
    }

    /*//////////////////////////////////////////////////////////////
                    LOW LEVEL FIXED POINT OPERATIONS
    //////////////////////////////////////////////////////////////*/

    function mulDivDown(
        uint256 x,
        uint256 y,
        uint256 denominator
    ) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
            if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
                revert(0, 0)
            }

            // Divide x * y by the denominator.
            z := div(mul(x, y), denominator)
        }
    }

    function mulDivUp(
        uint256 x,
        uint256 y,
        uint256 denominator
    ) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
            if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
                revert(0, 0)
            }

            // If x * y modulo the denominator is strictly greater than 0,
            // 1 is added to round up the division of x * y by the denominator.
            z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator))
        }
    }

    function rpow(
        uint256 x,
        uint256 n,
        uint256 scalar
    ) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            switch x
            case 0 {
                switch n
                case 0 {
                    // 0 ** 0 = 1
                    z := scalar
                }
                default {
                    // 0 ** n = 0
                    z := 0
                }
            }
            default {
                switch mod(n, 2)
                case 0 {
                    // If n is even, store scalar in z for now.
                    z := scalar
                }
                default {
                    // If n is odd, store x in z for now.
                    z := x
                }

                // Shifting right by 1 is like dividing by 2.
                let half := shr(1, scalar)

                for {
                    // Shift n right by 1 before looping to halve it.
                    n := shr(1, n)
                } n {
                    // Shift n right by 1 each iteration to halve it.
                    n := shr(1, n)
                } {
                    // Revert immediately if x ** 2 would overflow.
                    // Equivalent to iszero(eq(div(xx, x), x)) here.
                    if shr(128, x) {
                        revert(0, 0)
                    }

                    // Store x squared.
                    let xx := mul(x, x)

                    // Round to the nearest number.
                    let xxRound := add(xx, half)

                    // Revert if xx + half overflowed.
                    if lt(xxRound, xx) {
                        revert(0, 0)
                    }

                    // Set x to scaled xxRound.
                    x := div(xxRound, scalar)

                    // If n is even:
                    if mod(n, 2) {
                        // Compute z * x.
                        let zx := mul(z, x)

                        // If z * x overflowed:
                        if iszero(eq(div(zx, x), z)) {
                            // Revert if x is non-zero.
                            if iszero(iszero(x)) {
                                revert(0, 0)
                            }
                        }

                        // Round to the nearest number.
                        let zxRound := add(zx, half)

                        // Revert if zx + half overflowed.
                        if lt(zxRound, zx) {
                            revert(0, 0)
                        }

                        // Return properly scaled zxRound.
                        z := div(zxRound, scalar)
                    }
                }
            }
        }
    }

    /*//////////////////////////////////////////////////////////////
                        GENERAL NUMBER UTILITIES
    //////////////////////////////////////////////////////////////*/

    function sqrt(uint256 x) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            let y := x // We start y at x, which will help us make our initial estimate.

            z := 181 // The "correct" value is 1, but this saves a multiplication later.

            // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad
            // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically.

            // We check y >= 2^(k + 8) but shift right by k bits
            // each branch to ensure that if x >= 256, then y >= 256.
            if iszero(lt(y, 0x10000000000000000000000000000000000)) {
                y := shr(128, y)
                z := shl(64, z)
            }
            if iszero(lt(y, 0x1000000000000000000)) {
                y := shr(64, y)
                z := shl(32, z)
            }
            if iszero(lt(y, 0x10000000000)) {
                y := shr(32, y)
                z := shl(16, z)
            }
            if iszero(lt(y, 0x1000000)) {
                y := shr(16, y)
                z := shl(8, z)
            }

            // Goal was to get z*z*y within a small factor of x. More iterations could
            // get y in a tighter range. Currently, we will have y in [256, 256*2^16).
            // We ensured y >= 256 so that the relative difference between y and y+1 is small.
            // That's not possible if x < 256 but we can just verify those cases exhaustively.

            // Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256.
            // Correctness can be checked exhaustively for x < 256, so we assume y >= 256.
            // Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps.

            // For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range
            // (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256.

            // Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate
            // sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18.

            // There is no overflow risk here since y < 2^136 after the first branch above.
            z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181.

            // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough.
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))

            // If x+1 is a perfect square, the Babylonian method cycles between
            // floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor.
            // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division
            // Since the ceil is rare, we save gas on the assignment and repeat division in the rare case.
            // If you don't care whether the floor or ceil square root is returned, you can remove this statement.
            z := sub(z, lt(div(x, z), z))
        }
    }

    function unsafeMod(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Mod x by y. Note this will return
            // 0 instead of reverting if y is zero.
            z := mod(x, y)
        }
    }

    function unsafeDiv(uint256 x, uint256 y) internal pure returns (uint256 r) {
        /// @solidity memory-safe-assembly
        assembly {
            // Divide x by y. Note this will return
            // 0 instead of reverting if y is zero.
            r := div(x, y)
        }
    }

    function unsafeDivUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Add 1 to x * y if x % y > 0. Note this will
            // return 0 instead of reverting if y is zero.
            z := add(gt(mod(x, y), 0), div(x, y))
        }
    }
}
"
    },
    "src/vaults/BaseControlledAsyncRedeem.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.28;

import { IERC165, IERC7575 } from "ERC-7540/interfaces/IERC7575.sol";
import { IERC7540Redeem, IERC7540Operator } from "ERC-7540/interfaces/IERC7540.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import { TimelockedUUPSUpgradeable } from "src/utils/TimelockedUUPSUpgradeable.sol";
import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { PendingRedeem, ClaimableRedeem } from "src/interfaces/IRedeemQueue.sol";
import { IRedeemAccounting } from "src/interfaces/IRedeemAccounting.sol";
import { IUltraVaultRateProvider } from "src/interfaces/IUltraVaultRateProvider.sol";
import { OPERATOR_ROLE, PAUSER_ROLE, UPGRADER_ROLE } from "src/utils/Roles.sol";
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { AddressUpdateProposal } from "src/utils/AddressUpdates.sol";
import { RedeemQueue } from "src/vaults/accounting/RedeemQueue.sol";
import { IBaseVaultErrors, IBaseVaultEvents } from "src/interfaces/IBaseVault.sol";

// keccak256(abi.encode(uint256(keccak256("ultrayield.storage.BaseControlledAsyncRedeem")) - 1)) & ~bytes32(uint256(0xff));
bytes32 constant BASE_ASYNC_REDEEM_STORAGE_LOCATION = 0xaf7389673351d5ab654ae6fb9b324c897ebef437a969ec1524dcc6c7b5ca5400;

/// @dev Initialization parameters for BaseControlledAsyncRedeem
struct BaseControlledAsyncRedeemInitParams {
    // Owner of the vault
    address owner;
    // Underlying asset address
    address asset;
    // Vault name
    string name;
    // Vault symbol
    string symbol;
    // Oracle for assets exchange rate
    address rateProvider;
}

/// @title BaseControlledAsyncRedeem
/// @notice Base contract for controlled async redeem flows
/// @dev Based on ERC-7540 Reference Implementation
abstract contract BaseControlledAsyncRedeem is
    AccessControlUpgradeable,
    ERC4626Upgradeable,
    PausableUpgradeable,
    TimelockedUUPSUpgradeable,
    RedeemQueue,
    IERC7540Operator,
    IRedeemAccounting,
    IBaseVaultErrors,
    IBaseVaultEvents
{
    using Math for uint256;
    using SafeERC20 for IERC20;

    ///////////////
    // Constants //
    ///////////////

    uint256 internal constant REQUEST_ID = 0;
    uint256 internal constant ADDRESS_UPDATE_TIMELOCK = 3 days;
    uint256 internal constant MAX_ADDRESS_UPDATE_WAIT = 7 days;

    /////////////
    // Storage //
    /////////////

    /// @custom:storage-location erc7201:ultrayield.storage.BaseControlledAsyncRedeem
    struct BaseAsyncRedeemStorage {
        mapping(address => mapping(address => bool)) isOperator;
        IUltraVaultRateProvider rateProvider;
        AddressUpdateProposal proposedRateProvider;
    }

    function _getBaseAsyncRedeemStorage() private pure returns (BaseAsyncRedeemStorage storage $) {
        assembly {
            $.slot := BASE_ASYNC_REDEEM_STORAGE_LOCATION
        }
    }

    //////////
    // Init //
    //////////

    /// @notice Initialize vault with basic parameters
    /// @param params Struct wrapping initialization parameters
    function initialize(
        BaseControlledAsyncRedeemInitParams memory params
    ) public virtual onlyInitializing {
        require(params.asset != address(0), ZeroAssetAddress());
        require(params.rateProvider != address(0), ZeroRateProviderAddress());

        // Init parents
        __TimelockedUUPSUpgradeable_init();
        __AccessControl_init();
        __Pausable_init();
        __ERC20_init(params.name, params.symbol);
        __ERC4626_init(IERC20(params.asset));

        // Init self
        _getBaseAsyncRedeemStorage().rateProvider = IUltraVaultRateProvider(params.rateProvider);

        // Grant roles to owner
        _grantRole(DEFAULT_ADMIN_ROLE, params.owner);
        _grantRole(OPERATOR_ROLE, params.owner);
        _grantRole(PAUSER_ROLE, params.owner);
        _grantRole(UPGRADER_ROLE, params.owner);
    }

    /////////////////
    // Public view //
    /////////////////

    /// @notice Returns the address of the rate provider
    /// @return rateProvider The address of the rate provider
    function rateProvider() public view returns (IUltraVaultRateProvider) {
        return _getBaseAsyncRedeemStorage().rateProvider;
    }

    /// @notice Returns the proposed rate provider
    /// @return proposedRateProvider The proposed rate provider
    function proposedRateProvider() public view returns (AddressUpdateProposal memory) {
        return _getBaseAsyncRedeemStorage().proposedRateProvider;
    }

    //////////////
    // IERC7575 //
    //////////////

    /// @notice Returns the address of the share token
    /// @return share The address of the share token
    function share() public view returns (address) {
        return address(this);
    }

    //////////////////////
    // IERC7540Operator //
    //////////////////////

    /// @inheritdoc IERC7540Operator
    function isOperator(address controller, address operator) public view returns (bool) {
        return _getBaseAsyncRedeemStorage().isOperator[controller][operator];
    }

    /// @inheritdoc IERC7540Operator
    function setOperator(
        address operator,
        bool approved
    ) external returns (bool success) {
        require(msg.sender != operator, "ERC7540Vault/cannot-set-self-as-operator");
        if (isOperator(msg.sender, operator) != approved) {
            _getBaseAsyncRedeemStorage().isOperator[msg.sender][operator] = approved;
            emit OperatorSet(msg.sender, operator, approved);
            success = true;
        }
    }

    //////////////////////
    // Asset Conversion //
    //////////////////////

    /// @notice Converts assets to underlying
    /// @param _asset The asset to convert
    /// @param assets The amount of assets to convert
    /// @return baseAssets The amount of underlying received
    function convertToUnderlying(
        address _asset,
        uint256 assets
    ) external view returns (uint256 baseAssets) {
        return _convertToUnderlying(_asset, assets);
    }

    /// @notice Converts underlying to assets
    /// @param _asset The asset to convert
    /// @param baseAssets The amount of underlying to convert
    /// @return assets The amount of assets received
    function convertFromUnderlying(
        address _asset,
        uint256 baseAssets
    ) external view returns (uint256 assets) {
        return _convertFromUnderlying(_asset, baseAssets);
    }

    /// @dev Internal function to convert assets to underlying
    function _convertToUnderlying(
        address _asset,
        uint256 assets
    ) internal view returns (uint256 baseAssets) {
        // If asset is the same as base asset, no conversion needed
        if (_asset == this.asset()) {
            return assets;
        }
        // Call reverts if asset not supported
        return rateProvider().convertToUnderlying(_asset, assets);
    }

    /// @dev Internal function to convert underlying to assets
    function _convertFromUnderlying(
        address _asset,
        uint256 baseAssets
    ) internal view returns (uint256 assets) {
        // If asset is the same as base asset, no conversion needed
        if (_asset == this.asset()) {
            return baseAssets;
        }
        // Call reverts if asset not supported
        return rateProvider().convertFromUnderlying(_asset, baseAssets);
    }

    /// @dev Optimized function mirroring `convertToAssets` in OZ ERC4626 v5.4.0
    /// @dev Uses pre-fetched totals. Always rounds down like `convertToAssets` does in the OZ implementation
    function _optimizedConvertToAssets(
        uint256 shares,
        uint256 _totalAssets,
        uint256 _totalSupply
    ) internal view returns (uint256 assets) {
        return shares.mulDiv(_totalAssets + 1, _totalSupply + 10 ** _decimalsOffset(), Math.Rounding.Floor);
    }

    /// @dev Optimized function mirroring `convertToShares` in OZ ERC4626 v5.4.0
    /// @dev Uses pre-fetched totals. Always rounds down like `convertToShares` does in the OZ implementation
    function _optimizedConvertToShares(
        uint256 assets,
        uint256 _totalAssets,
        uint256 _totalSupply
    ) internal view returns (uint256 shares) {
        return assets.mulDiv(_totalSupply + 10 ** _decimalsOffset(), _totalAssets + 1, Math.Rounding.Floor);
    }

    ////////////////////
    // Deposit & Mint //
    ////////////////////

    /// @notice Helper to deposit assets for msg.sender upon referral specifying receiver
    /// @param assets Amount to deposit
    /// @param receiver receiver of deposit
    /// @param referralId id of referral
    /// @return shares Amount of shares received
    function depositWithReferral(
        uint256 assets,
        address receiver,
        string calldata referralId
    ) external returns (uint256) {
        return _depositAssetWithReferral(asset(), assets, receiver, referralId);
    }

    /// @notice Helper to deposit particular asset for msg.sender upon referral
    /// @param _asset Asset to deposit
    /// @param assets Amount to deposit
    /// @param receiver receiver of deposit
    /// @param referralId id of referral
    /// @return shares Amount of shares received
    function depositAssetWithReferral(
        address _asset,
        uint256 assets,
        address receiver,
        string calldata referralId
    ) external returns (uint256) {
        return _depositAssetWithReferral(_asset, assets, receiver, referralId);
    }

    /// @notice Deposit exact number of assets in base asset and mint shares to receiver
    /// @param assets Amount of assets to deposit
    /// @param receiver Share receiver
    /// @return shares Amount of shares received
    /// @dev Reverts if paused
    function deposit(
        uint256 assets,
        address receiver
    ) public override returns (uint256 shares) {
        return _depositAsset(asset(), assets, receiver);
    }

    /// @notice Deposit assets for receiver
    /// @param _asset Asset
    /// @param assets Amount to deposit
    /// @param receiver Share recipient
    /// @return shares Amount of shares received
    /// @dev Reverts if paused
    function depositAsset(
        address _asset,
        uint256 assets,
        address receiver
    ) external returns (uint256 shares) {
        return _depositAsset(_asset, assets, receiver);
    }

    /// @dev Internal function for processing deposits with referral
    /// @dev Emits Referral event
    function _depositAssetWithReferral(
        address _asset,
        uint256 assets,
        address receiver,
        string calldata referralId
    ) internal returns (uint256 shares) {
        shares = _depositAsset(_asset, assets, receiver);
        emit Referral(referralId, msg.sender, shares);
    }

    /// @dev Internal function for depositing exact number of `assets` in `_asset` and minting shares to `receiver`
    /// @dev Reverts if paused
    /// @dev `receiver` is validated to be non-zero within ERC20Upgradeable
    function _depositAsset(
        address _asset,
        uint256 assets,
        address receiver
    ) internal virtual whenNotPaused returns (uint256 shares) {
        shares = previewDepositForAsset(_asset, assets);
        _performDeposit(_asset, assets, shares, receiver);
    }

    /// @notice Mint exact number of shares to receiver and deposit in base asset
    /// @param shares Amount of shares to mint
    /// @param receiver Share receiver
    /// @return assets Amount of assets required
    /// @dev Reverts if paused
    function mint(
        uint256 shares,
        address receiver
    ) public override returns (uint256 assets) {
        return _mintWithAsset(asset(), shares, receiver);
    }

    /// @notice Mint shares for receiver with specific asset
    /// @param _asset Asset to mint with
    /// @param shares Amount to mint
    /// @param receiver Share recipient
    /// @return assets Amount of assets required
    /// @dev Reverts if paused
    function mintWithAsset(
        address _asset,
        uint256 shares,
        address receiver
    ) external returns (uint256 assets) {
        return _mintWithAsset(_asset, shares, receiver);
    }

    /// @dev Internal function for minting exactly `shares` to `receiver` and depositing in `_asset`
    /// @dev Reverts if paused
    /// @dev `receiver` is validated to be non-zero within ERC20Upgradeable
    function _mintWithAsset(
        address _asset,
        uint256 shares,
        address receiver
    ) internal virtual whenNotPaused returns (uint256 assets) {
        assets = previewMintForAsset(_asset, shares);
        _performDeposit(_asset, assets, shares, receiver);
    }

    /// @dev Internal function to process deposit and mint flows
    function _performDeposit(
        address _asset,
        uint256 assets,
        uint256 shares,
        address receiver
    ) internal {
        // Checks
        require(assets != 0, EmptyDeposit());
        require(shares != 0, NothingToMint());

        // Pre-deposit hook - use the actual asset amount being transferred
        beforeDeposit(_asset, assets, shares);

        // Transfer assets from sender to the vault
        IERC20(_asset).safeTransferFrom(
            msg.sender,
            address(this),
            assets
        );

        // Mint shares to receiver
        _mint(receiver, shares);

        // Emit event
        emit Deposit(msg.sender, receiver, assets, shares);

        // After-deposit hook - use the actual asset amount that was transferred
        afterDeposit(_asset, assets, shares);
    }

    ///////////////////////
    // Withdraw & Redeem //
    ///////////////////////

    /// @notice Withdraw assets from fulfilled redeem requests
    /// @param assets Amount to withdraw
    /// @param receiver Asset recipient
    /// @param controller Controller address
    /// @return shares Amount of shares burned
    /// @dev Asynchronous function, works when paused
    /// @dev Caller must be controller or operator
    /// @dev Requires sufficient claimable assets
    function withdraw(
        uint256 assets,
        address receiver,
        address controller
    ) public override returns (uint256 shares) {
        return _withdrawAsset(asset(), assets, receiver, controller);
    }

    /// @notice Withdraw assets from fulfilled redeem requests
    /// @param _asset Asset
    /// @param assets Amount to withdraw
    /// @param receiver Asset recipient
    /// @param controller Controller address
    /// @return shares Amount of shares burned
    /// @dev Asynchronous function, works when paused
    /// @dev Caller must be controller or operator
    /// @dev Requires sufficient claimable assets
    function withdrawAsset(
        address _asset,
        uint256 assets,
        address receiver,
        address controller
    ) public returns (uint256 shares) {
        return _withdrawAsset(_asset, assets, receiver, controller);
    }

    /// @dev Internal function for withdrawing exact number of `assets` in `_asset`
    function _withdrawAsset(
        address _asset,
        uint256 assets,
        address receiver,
        address controller
    ) internal checkAccess(controller) returns (uint256 shares) {
        require(assets != 0, NothingToWithdraw());
        
        // Calculate shares to burn based on the claimable redeem ratio
        shares = _calculateClai

Tags:
ERC20, ERC165, Multisig, Mintable, Pausable, Yield, Voting, Timelock, Upgradeable, Multi-Signature, Factory, Oracle|addr:0x88300e0041cc88bdb1404031b53aa68058ecd847|verified:true|block:23725523|tx:0x3e0ed0fe40d48ae9402f4f0d277e1de7553cd3bb23870bf9c2aeb4d6959eb28c|first_check:1762257472

Submitted on: 2025-11-04 12:57:54

Comments

Log in to comment.

No comments yet.