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
Submitted on: 2025-11-04 12:57:54
Comments
Log in to comment.
No comments yet.