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": {
"lib/euler-earn/src/EulerEarnFactory.sol": {
"content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.26;
import {IEulerEarn} from "./interfaces/IEulerEarn.sol";
import {IEulerEarnFactory} from "./interfaces/IEulerEarnFactory.sol";
import {IPerspective} from "./interfaces/IPerspective.sol";
import {EventsLib} from "./libraries/EventsLib.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {EulerEarn} from "./EulerEarn.sol";
import {Ownable, Context} from "openzeppelin-contracts/access/Ownable.sol";
import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
/// @title EulerEarnFactory
/// @author Forked with gratitude from Morpho Labs. Inspired by Silo Labs.
/// @custom:contact security@morpho.org
/// @custom:contact security@euler.xyz
/// @notice This contract allows to create EulerEarn vaults, and to index them easily.
contract EulerEarnFactory is Ownable, EVCUtil, IEulerEarnFactory {
/* IMMUTABLES */
/// @dev The address of the Permit2 contract.
address public immutable permit2Address;
/* STORAGE */
/// @inheritdoc IEulerEarnFactory
mapping(address => bool) public isVault;
/// @dev The list of all the vaults created by the factory.
address[] public vaultList;
/// @dev The perspective contract that is used to verify the strategies.
IPerspective internal perspective;
/* CONSTRUCTOR */
/// @dev Initializes the contract.
/// @param _owner The owner of the factory contract.
/// @param _evc The address of the EVC contract.
/// @param _permit2 The address of the Permit2 contract.
/// @param _perspective The address of the supported perspective contract.
constructor(address _owner, address _evc, address _permit2, address _perspective) Ownable(_owner) EVCUtil(_evc) {
if (_perspective == address(0)) revert ErrorsLib.ZeroAddress();
permit2Address = _permit2;
perspective = IPerspective(_perspective);
}
/* EXTERNAL */
/// @inheritdoc IEulerEarnFactory
function supportedPerspective() external view returns (address) {
return address(perspective);
}
/// @inheritdoc IEulerEarnFactory
function getVaultListLength() external view returns (uint256) {
return vaultList.length;
}
/// @inheritdoc IEulerEarnFactory
function getVaultListSlice(uint256 start, uint256 end) external view returns (address[] memory list) {
if (end == type(uint256).max) end = vaultList.length;
if (end < start || end > vaultList.length) revert ErrorsLib.BadQuery();
list = new address[](end - start);
for (uint256 i; i < end - start; ++i) {
list[i] = vaultList[start + i];
}
}
/// @inheritdoc IEulerEarnFactory
function isStrategyAllowed(address id) external view returns (bool) {
return perspective.isVerified(id) || isVault[id];
}
/// @inheritdoc IEulerEarnFactory
function setPerspective(address _perspective) public onlyEVCAccountOwner onlyOwner {
if (_perspective == address(0)) revert ErrorsLib.ZeroAddress();
perspective = IPerspective(_perspective);
emit EventsLib.SetPerspective(_perspective);
}
/// @inheritdoc IEulerEarnFactory
function createEulerEarn(
address initialOwner,
uint256 initialTimelock,
address asset,
string memory name,
string memory symbol,
bytes32 salt
) external returns (IEulerEarn eulerEarn) {
eulerEarn = IEulerEarn(
address(
new EulerEarn{salt: salt}(
initialOwner, address(evc), permit2Address, initialTimelock, asset, name, symbol
)
)
);
isVault[address(eulerEarn)] = true;
vaultList.push(address(eulerEarn));
emit EventsLib.CreateEulerEarn(
address(eulerEarn), _msgSender(), initialOwner, initialTimelock, asset, name, symbol, salt
);
}
/// @notice Retrieves the message sender in the context of the EVC.
/// @dev This function returns the account on behalf of which the current operation is being performed, which is
/// either msg.sender or the account authenticated by the EVC.
/// @return The address of the message sender.
function _msgSender() internal view virtual override(EVCUtil, Context) returns (address) {
return EVCUtil._msgSender();
}
}
"
},
"lib/euler-earn/src/interfaces/IEulerEarn.sol": {
"content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;
import {IEulerEarnFactory} from "./IEulerEarnFactory.sol";
import {IERC4626} from "openzeppelin-contracts/interfaces/IERC4626.sol";
import {IERC20Permit} from "openzeppelin-contracts/token/ERC20/extensions/IERC20Permit.sol";
import {MarketConfig, PendingUint136, PendingAddress} from "../libraries/PendingLib.sol";
struct MarketAllocation {
/// @notice The vault to allocate.
IERC4626 id;
/// @notice The amount of assets to allocate.
uint256 assets;
}
interface IOwnable {
function owner() external view returns (address);
function transferOwnership(address) external;
function renounceOwnership() external;
function acceptOwnership() external;
function pendingOwner() external view returns (address);
}
/// @dev This interface is used for factorizing IEulerEarnStaticTyping and IEulerEarn.
/// @dev Consider using the IEulerEarn interface instead of this one.
interface IEulerEarnBase {
/// @notice The address of the Permit2 contract.
function permit2Address() external view returns (address);
/// @notice The address of the creator.
function creator() external view returns (address);
/// @notice The address of the curator.
function curator() external view returns (address);
/// @notice Stores whether an address is an allocator or not.
function isAllocator(address target) external view returns (bool);
/// @notice The current guardian. Can be set even without the timelock set.
function guardian() external view returns (address);
/// @notice The current fee.
function fee() external view returns (uint96);
/// @notice The fee recipient.
function feeRecipient() external view returns (address);
/// @notice The current timelock.
function timelock() external view returns (uint256);
/// @dev Stores the order of vaults in which liquidity is supplied upon deposit.
/// @dev Can contain any vault. A vault is skipped as soon as its supply cap is reached.
function supplyQueue(uint256) external view returns (IERC4626);
/// @notice Returns the length of the supply queue.
function supplyQueueLength() external view returns (uint256);
/// @dev Stores the order of vault from which liquidity is withdrawn upon withdrawal.
/// @dev Always contain all non-zero cap vault as well as all vault on which the Earn vault supplies liquidity,
/// without duplicate.
function withdrawQueue(uint256) external view returns (IERC4626);
/// @notice Returns the length of the withdraw queue.
function withdrawQueueLength() external view returns (uint256);
/// @notice Returns the amount of assets that can be withdrawn from given strategy vault.
/// @dev Accounts for internally tracked balance, ignoring direct shares transfer and for assets available in the strategy.
function maxWithdrawFromStrategy(IERC4626 id) external view returns (uint256);
/// @notice Returns the amount of assets expected to be supplied to the strategy vault.
/// @dev Accounts for internally tracked balance, ignoring direct shares transfer.
function expectedSupplyAssets(IERC4626 id) external view returns (uint256);
/// @notice Stores the total assets managed by this vault when the fee was last accrued.
function lastTotalAssets() external view returns (uint256);
/// @notice Stores the missing assets due to realized bad debt or forced vault removal.
/// @dev In order to cover those lost assets, it is advised to supply on behalf of address(1) on the vault
/// (canonical method).
function lostAssets() external view returns (uint256);
/// @notice Submits a `newTimelock`.
/// @dev Warning: Reverts if a timelock is already pending. Revoke the pending timelock to overwrite it.
/// @dev In case the new timelock is higher than the current one, the timelock is set immediately.
function submitTimelock(uint256 newTimelock) external;
/// @notice Accepts the pending timelock.
function acceptTimelock() external;
/// @notice Revokes the pending timelock.
/// @dev Does not revert if there is no pending timelock.
function revokePendingTimelock() external;
/// @notice Submits a `newSupplyCap` for the vault.
/// @dev Warning: Reverts if a cap is already pending. Revoke the pending cap to overwrite it.
/// @dev Warning: Reverts if a vault removal is pending.
/// @dev In case the new cap is lower than the current one, the cap is set immediately.
/// @dev For the sake of backwards compatibility, the max allowed cap can either be set to type(uint184).max or type(uint136).max.
function submitCap(IERC4626 id, uint256 newSupplyCap) external;
/// @notice Accepts the pending cap of the vault.
function acceptCap(IERC4626 id) external;
/// @notice Revokes the pending cap of the vault.
/// @dev Does not revert if there is no pending cap.
function revokePendingCap(IERC4626 id) external;
/// @notice Submits a forced vault removal from the Earn vault, eventually losing all funds supplied to the vault.
/// @notice This forced removal is expected to be used as an emergency process in case a vault constantly reverts.
/// To softly remove a sane vault, the curator role is expected to bundle a reallocation that empties the vault
/// first (using `reallocate`), followed by the removal of the vault (using `updateWithdrawQueue`).
/// @dev Warning: Reverts for non-zero cap or if there is a pending cap. Successfully submitting a zero cap will
/// prevent such reverts.
function submitMarketRemoval(IERC4626 id) external;
/// @notice Revokes the pending removal of the vault.
/// @dev Does not revert if there is no pending vault removal.
function revokePendingMarketRemoval(IERC4626 id) external;
/// @notice Sets the name of the Earn vault.
function setName(string memory newName) external;
/// @notice Sets the symbol of the Earn vault.
function setSymbol(string memory newSymbol) external;
/// @notice Submits a `newGuardian`.
/// @notice Warning: a malicious guardian could disrupt the Earn vault's operation, and would have the power to revoke
/// any pending guardian.
/// @dev In case there is no guardian, the guardian is set immediately.
/// @dev Warning: Submitting a guardian will overwrite the current pending guardian.
function submitGuardian(address newGuardian) external;
/// @notice Accepts the pending guardian.
function acceptGuardian() external;
/// @notice Revokes the pending guardian.
function revokePendingGuardian() external;
/// @notice Sets `newAllocator` as an allocator or not (`newIsAllocator`).
function setIsAllocator(address newAllocator, bool newIsAllocator) external;
/// @notice Sets `curator` to `newCurator`.
function setCurator(address newCurator) external;
/// @notice Sets the `fee` to `newFee`.
function setFee(uint256 newFee) external;
/// @notice Sets `feeRecipient` to `newFeeRecipient`.
function setFeeRecipient(address newFeeRecipient) external;
/// @notice Sets `supplyQueue` to `newSupplyQueue`.
/// @param newSupplyQueue is an array of enabled vaults, and can contain duplicate vaults, but it would only
/// increase the cost of depositing to the vault.
function setSupplyQueue(IERC4626[] calldata newSupplyQueue) external;
/// @notice Updates the withdraw queue. Some vaults can be removed, but no vault can be added.
/// @notice Removing a vault requires the vault to have 0 supply on it, or to have previously submitted a removal
/// for this vault (with the function `submitMarketRemoval`).
/// @notice Warning: Anyone can supply on behalf of the vault so the call to `updateWithdrawQueue` that expects a
/// vault to be empty can be griefed by a front-run. To circumvent this, the allocator can simply bundle a
/// reallocation that withdraws max from this vault with a call to `updateWithdrawQueue`.
/// @dev Warning: Removing a vault with supply will decrease the fee accrued until one of the functions updating
/// `lastTotalAssets` is triggered (deposit/mint/withdraw/redeem/setFee/setFeeRecipient).
/// @dev Warning: `updateWithdrawQueue` is not idempotent. Submitting twice the same tx will change the queue twice.
/// @param indexes The indexes of each vault in the previous withdraw queue, in the new withdraw queue's order.
function updateWithdrawQueue(uint256[] calldata indexes) external;
/// @notice Reallocates the vault's liquidity so as to reach a given allocation of assets on each given vault.
/// @dev The behavior of the reallocation can be altered by state changes, including:
/// - Deposits on the Earn vault that supplies to vaults that are expected to be supplied to during reallocation.
/// - Withdrawals from the Earn vault that withdraws from vaults that are expected to be withdrawn from during
/// reallocation.
/// - Donations to the vault on vaults that are expected to be supplied to during reallocation.
/// - Withdrawals from vaults that are expected to be withdrawn from during reallocation.
/// @dev Sender is expected to pass `assets = type(uint256).max` with the last MarketAllocation of `allocations` to
/// supply all the remaining withdrawn liquidity, which would ensure that `totalWithdrawn` = `totalSupplied`.
/// @dev A supply in a reallocation step will make the reallocation revert if the amount is greater than the net
/// amount from previous steps (i.e. total withdrawn minus total supplied).
function reallocate(MarketAllocation[] calldata allocations) external;
}
/// @dev This interface is inherited by IEulerEarn so that function signatures are checked by the compiler.
/// @dev Consider using the IEulerEarn interface instead of this one.
interface IEulerEarnStaticTyping is IEulerEarnBase {
/// @notice Returns the current configuration of each vault.
function config(IERC4626) external view returns (uint112 balance, uint136 cap, bool enabled, uint64 removableAt);
/// @notice Returns the pending guardian.
function pendingGuardian() external view returns (address guardian, uint64 validAt);
/// @notice Returns the pending cap for each vault.
function pendingCap(IERC4626) external view returns (uint136 value, uint64 validAt);
/// @notice Returns the pending timelock.
function pendingTimelock() external view returns (uint136 value, uint64 validAt);
}
/// @title IEulerEarn
/// @author Forked with gratitude from Morpho Labs. Inspired by Silo Labs.
/// @custom:contact security@morpho.org
/// @custom:contact security@euler.xyz
/// @dev Use this interface for IEulerEarn to have access to all the functions with the appropriate function
/// signatures.
interface IEulerEarn is IEulerEarnBase, IERC4626, IERC20Permit, IOwnable {
/// @notice Returns the address of the Ethereum Vault Connector (EVC) used by this contract.
function EVC() external view returns (address);
/// @notice Returns the current configuration of each vault.
function config(IERC4626) external view returns (MarketConfig memory);
/// @notice Returns the pending guardian.
function pendingGuardian() external view returns (PendingAddress memory);
/// @notice Returns the pending cap for each vault.
function pendingCap(IERC4626) external view returns (PendingUint136 memory);
/// @notice Returns the pending timelock.
function pendingTimelock() external view returns (PendingUint136 memory);
}
"
},
"lib/euler-earn/src/interfaces/IEulerEarnFactory.sol": {
"content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;
import {IEulerEarn} from "./IEulerEarn.sol";
/// @title IEulerEarnFactory
/// @author Forked with gratitude from Morpho Labs. Inspired by Silo Labs.
/// @custom:contact security@morpho.org
/// @custom:contact security@euler.xyz
/// @notice Interface of EulerEarn's factory.
interface IEulerEarnFactory {
/// @notice The address of the Permit2 contract.
function permit2Address() external view returns (address);
/// @notice The address of the supported perspective contract.
function supportedPerspective() external view returns (address);
/// @notice Whether a vault was created with the factory.
function isVault(address target) external view returns (bool);
/// @notice Fetch the length of the deployed proxies list
/// @return The length of the proxy list array
function getVaultListLength() external view returns (uint256);
/// @notice Get a slice of the deployed proxies array
/// @param start Start index of the slice
/// @param end End index of the slice
/// @return list An array containing the slice of the proxy list
function getVaultListSlice(uint256 start, uint256 end) external view returns (address[] memory list);
/// @notice Sets the perspective contract.
/// @param _perspective The address of the new perspective contract.
function setPerspective(address _perspective) external;
/// @notice Whether a strategy is allowed to be used by the Earn vault.
/// @dev Warning: Only allow trusted, correctly implemented ERC4626 strategies to be used by the Earn vault.
/// @dev Warning: Allowed strategies must not be prone to the first-depositor attack.
/// @dev Warning: To prevent exchange rate manipulation, it is recommended that the allowed strategies are not empty or have sufficient protection.
function isStrategyAllowed(address id) external view returns (bool);
/// @notice Creates a new EulerEarn vault.
/// @param initialOwner The owner of the vault.
/// @param initialTimelock The initial timelock of the vault.
/// @param asset The address of the underlying asset.
/// @param name The name of the vault.
/// @param symbol The symbol of the vault.
/// @param salt The salt to use for the EulerEarn vault's CREATE2 address.
function createEulerEarn(
address initialOwner,
uint256 initialTimelock,
address asset,
string memory name,
string memory symbol,
bytes32 salt
) external returns (IEulerEarn eulerEarn);
}
"
},
"lib/euler-earn/src/interfaces/IPerspective.sol": {
"content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.8.0;
/// @title IPerspective
/// @custom:security-contact security@euler.xyz
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice A contract that verifies the properties of a vault.
interface IPerspective {
/// @notice Checks if a vault is verified.
/// @param vault The address of the vault to check.
/// @return True if the vault is verified, false otherwise.
function isVerified(address vault) external view returns (bool);
}
"
},
"lib/euler-earn/src/libraries/EventsLib.sol": {
"content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;
import {FlowCapsConfig} from "../interfaces/IPublicAllocator.sol";
import {IERC4626} from "openzeppelin-contracts/interfaces/IERC4626.sol";
import {PendingAddress} from "./PendingLib.sol";
/// @title EventsLib
/// @author Forked with gratitude from Morpho Labs. Inspired by Silo Labs.
/// @custom:contact security@morpho.org
/// @custom:contact security@euler.xyz
/// @notice Library exposing events.
library EventsLib {
/// @notice Emitted when the perspective is set.
event SetPerspective(address);
/// @notice Emitted when the name of the Earn vault is set.
event SetName(string name);
/// @notice Emitted when the symbol of the Earn vault is set.
event SetSymbol(string symbol);
/// @notice Emitted when a pending `newTimelock` is submitted.
event SubmitTimelock(uint256 newTimelock);
/// @notice Emitted when `timelock` is set to `newTimelock`.
event SetTimelock(address indexed caller, uint256 newTimelock);
/// @notice Emitted `fee` is set to `newFee`.
event SetFee(address indexed caller, uint256 newFee);
/// @notice Emitted when a new `newFeeRecipient` is set.
event SetFeeRecipient(address indexed newFeeRecipient);
/// @notice Emitted when a pending `newGuardian` is submitted.
event SubmitGuardian(address indexed newGuardian);
/// @notice Emitted when `guardian` is set to `newGuardian`.
event SetGuardian(address indexed caller, address indexed guardian);
/// @notice Emitted when a pending `cap` is submitted for a vault.
event SubmitCap(address indexed caller, IERC4626 indexed id, uint256 cap);
/// @notice Emitted when a new `cap` is set for a vault.
event SetCap(address indexed caller, IERC4626 indexed id, uint256 cap);
/// @notice Emitted when the vault's last total assets is updated to `updatedTotalAssets`.
event UpdateLastTotalAssets(uint256 updatedTotalAssets);
/// @notice Emitted when the vault's lostAssets is updated to `newLostAssets`.
event UpdateLostAssets(uint256 newLostAssets);
/// @notice Emitted when the vault is submitted for removal.
event SubmitMarketRemoval(address indexed caller, IERC4626 indexed id);
/// @notice Emitted when `curator` is set to `newCurator`.
event SetCurator(address indexed newCurator);
/// @notice Emitted when an `allocator` is set to `isAllocator`.
event SetIsAllocator(address indexed allocator, bool isAllocator);
/// @notice Emitted when a `pendingTimelock` is revoked.
event RevokePendingTimelock(address indexed caller);
/// @notice Emitted when a `pendingCap` for the vault is revoked.
event RevokePendingCap(address indexed caller, IERC4626 indexed id);
/// @notice Emitted when a `pendingGuardian` is revoked.
event RevokePendingGuardian(address indexed caller);
/// @notice Emitted when a pending vault removal is revoked.
event RevokePendingMarketRemoval(address indexed caller, IERC4626 indexed id);
/// @notice Emitted when the `supplyQueue` is set to `newSupplyQueue`.
event SetSupplyQueue(address indexed caller, IERC4626[] newSupplyQueue);
/// @notice Emitted when the `withdrawQueue` is set to `newWithdrawQueue`.
event SetWithdrawQueue(address indexed caller, IERC4626[] newWithdrawQueue);
/// @notice Emitted when a reallocation supplies assets to the vault.
/// @param id The address of the vault.
/// @param suppliedAssets The amount of assets supplied to the vault.
/// @param suppliedShares The amount of shares minted.
event ReallocateSupply(address indexed caller, IERC4626 indexed id, uint256 suppliedAssets, uint256 suppliedShares);
/// @notice Emitted when a reallocation withdraws assets from the vault.
/// @param id The address of the vault.
/// @param withdrawnAssets The amount of assets withdrawn from the vault.
/// @param withdrawnShares The amount of shares burned.
event ReallocateWithdraw(
address indexed caller, IERC4626 indexed id, uint256 withdrawnAssets, uint256 withdrawnShares
);
/// @notice Emitted when interest are accrued.
/// @param newTotalAssets The assets of the vault after accruing the interest but before the interaction.
/// @param feeShares The shares minted to the fee recipient.
event AccrueInterest(uint256 newTotalAssets, uint256 feeShares);
/// @notice Emitted when a new EulerEarn vault is created.
/// @param eulerEarn The address of the EulerEarn vault.
/// @param caller The caller of the function.
/// @param initialOwner The initial owner of the EulerEarn vault.
/// @param initialTimelock The initial timelock of the EulerEarn vault.
/// @param asset The address of the underlying asset.
/// @param name The name of the EulerEarn vault.
/// @param symbol The symbol of the EulerEarn vault.
/// @param salt The salt used for the EulerEarn vault's CREATE2 address.
event CreateEulerEarn(
address indexed eulerEarn,
address indexed caller,
address initialOwner,
uint256 initialTimelock,
address indexed asset,
string name,
string symbol,
bytes32 salt
);
/// @notice Emitted during a public reallocation for each withdrawn-from vault.
event PublicWithdrawal(address indexed sender, address indexed vault, IERC4626 indexed id, uint256 withdrawnAssets);
/// @notice Emitted at the end of a public reallocation.
event PublicReallocateTo(
address indexed sender, address indexed vault, IERC4626 indexed supplyId, uint256 suppliedAssets
);
/// @notice Emitted when the admin is set for a vault.
event SetAdmin(address indexed sender, address indexed vault, address admin);
/// @notice Emitted when the fee is set for a vault.
event SetAllocationFee(address indexed sender, address indexed vault, uint256 fee);
/// @notice Emitted when the fee is transfered for a vault.
event TransferAllocationFee(
address indexed sender, address indexed vault, uint256 amount, address indexed feeRecipient
);
/// @notice Emitted when the flow caps are set for a vault.
event SetFlowCaps(address indexed sender, address indexed vault, FlowCapsConfig[] config);
}
"
},
"lib/euler-earn/src/libraries/ErrorsLib.sol": {
"content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;
import {IERC4626} from "openzeppelin-contracts/interfaces/IERC4626.sol";
/// @title ErrorsLib
/// @author Forked with gratitude from Morpho Labs. Inspired by Silo Labs.
/// @custom:contact security@morpho.org
/// @custom:contact security@euler.xyz
/// @notice Library exposing error messages.
library ErrorsLib {
/// @notice Thrown when the query is invalid.
error BadQuery();
/// @notice Thrown when the address passed is the zero address.
error ZeroAddress();
/// @notice Thrown when the caller doesn't have the curator role.
error NotCuratorRole();
/// @notice Thrown when the caller doesn't have the allocator role.
error NotAllocatorRole();
/// @notice Thrown when the caller doesn't have the guardian role.
error NotGuardianRole();
/// @notice Thrown when the caller doesn't have the curator nor the guardian role.
error NotCuratorNorGuardianRole();
/// @notice Thrown when the vault cannot be set in the supply queue.
error UnauthorizedMarket(IERC4626 id);
/// @notice Thrown when submitting a cap for a vault whose underlying asset does not correspond to the underlying
/// asset of the Earn vault.
error InconsistentAsset(IERC4626 id);
/// @notice Thrown when the supply cap has been exceeded on vault during a reallocation of funds.
error SupplyCapExceeded(IERC4626 id);
/// @notice Thrown when the fee to set exceeds the maximum fee.
error MaxFeeExceeded();
/// @notice Thrown when the value is already set.
error AlreadySet();
/// @notice Thrown when a value is already pending.
error AlreadyPending();
/// @notice Thrown when submitting the removal of a vault when there is a cap already pending on that vault.
error PendingCap(IERC4626 id);
/// @notice Thrown when submitting a cap for a vault with a pending removal.
error PendingRemoval();
/// @notice Thrown when submitting a vault removal for a vault with a non zero cap.
error NonZeroCap();
/// @notice Thrown when vault is a duplicate in the new withdraw queue to set.
error DuplicateMarket(IERC4626 id);
/// @notice Thrown when vault is missing in the updated withdraw queue and the vault has a non-zero cap set.
error InvalidMarketRemovalNonZeroCap(IERC4626 id);
/// @notice Thrown when vault is missing in the updated withdraw queue and the vault has a non-zero supply.
error InvalidMarketRemovalNonZeroSupply(IERC4626 id);
/// @notice Thrown when vault is missing in the updated withdraw queue and the vault is not yet disabled.
error InvalidMarketRemovalTimelockNotElapsed(IERC4626 id);
/// @notice Thrown when there's no pending value to set.
error NoPendingValue();
/// @notice Thrown when the requested liquidity cannot be withdrawn from EulerEarn.
error NotEnoughLiquidity();
/// @notice Thrown when interacting with a non previously enabled vault.
error MarketNotEnabled(IERC4626 id);
/// @notice Thrown when the submitted timelock is above the max timelock.
error AboveMaxTimelock();
/// @notice Thrown when the submitted timelock is below the min timelock.
error BelowMinTimelock();
/// @notice Thrown when the timelock is not elapsed.
error TimelockNotElapsed();
/// @notice Thrown when too many vaults are in the withdraw queue.
error MaxQueueLengthExceeded();
/// @notice Thrown when setting the fee to a non zero value while the fee recipient is the zero address.
error ZeroFeeRecipient();
/// @notice Thrown when the amount withdrawn is not exactly the amount supplied.
error InconsistentReallocation();
/// @notice Thrown when all caps have been reached.
error AllCapsReached();
/// @notice Thrown when the `msg.sender` is not the admin nor the owner of the vault.
error NotAdminNorVaultOwner();
/// @notice Thrown when the reallocation fee given is wrong.
error IncorrectFee();
/// @notice Thrown when `withdrawals` is empty.
error EmptyWithdrawals();
/// @notice Thrown when `withdrawals` contains a duplicate or is not sorted.
error InconsistentWithdrawals();
/// @notice Thrown when the deposit vault is in `withdrawals`.
error DepositMarketInWithdrawals();
/// @notice Thrown when attempting to deposit amount of assets corresponding to zero shares.
error ZeroShares();
/// @notice Thrown when attempting to withdraw zero from a vault.
error WithdrawZero(IERC4626 id);
/// @notice Thrown when attempting to set max inflow/outflow above the MAX_SETTABLE_FLOW_CAP.
error MaxSettableFlowCapExceeded();
/// @notice Thrown when the fee transfer fails.
error FeeTransferFailed(address feeRecipient);
/// @notice Thrown when attempting to withdraw more than the available supply of a vault.
error NotEnoughSupply(IERC4626 id);
/// @notice Thrown when attempting to withdraw more than the max outflow of a vault.
error MaxOutflowExceeded(IERC4626 id);
/// @notice Thrown when attempting to supply more than the max inflow of a vault.
error MaxInflowExceeded(IERC4626 id);
/// @notice Thrown when the maximum uint128 is exceeded.
error MaxUint128Exceeded();
/// @notice Thrown when withdrawal is attempted to an address known to be an EVC sub-account
error BadAssetReceiver();
}
"
},
"lib/euler-earn/src/EulerEarn.sol": {
"content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.26;
import {
MarketConfig,
PendingUint136,
PendingAddress,
MarketAllocation,
IEulerEarnBase,
IEulerEarnStaticTyping
} from "./interfaces/IEulerEarn.sol";
import {IEulerEarnFactory} from "./interfaces/IEulerEarnFactory.sol";
import {PendingLib} from "./libraries/PendingLib.sol";
import {ConstantsLib} from "./libraries/ConstantsLib.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {EventsLib} from "./libraries/EventsLib.sol";
import {SafeERC20Permit2Lib} from "./libraries/SafeERC20Permit2Lib.sol";
import {UtilsLib, WAD} from "./libraries/UtilsLib.sol";
import {SafeCast} from "openzeppelin-contracts/utils/math/SafeCast.sol";
import {IERC20Metadata} from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {Context} from "openzeppelin-contracts/utils/Context.sol";
import {ReentrancyGuard} from "openzeppelin-contracts/utils/ReentrancyGuard.sol";
import {Ownable2Step, Ownable} from "openzeppelin-contracts/access/Ownable2Step.sol";
import {
IERC20,
IERC4626,
ERC20,
ERC4626,
Math,
SafeERC20
} from "openzeppelin-contracts/token/ERC20/extensions/ERC4626.sol";
import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol";
/// @title EulerEarn
/// @author Forked with gratitude from Morpho Labs. Inspired by Silo Labs.
/// @custom:contact security@morpho.org
/// @custom:contact security@euler.xyz
/// @notice ERC4626 compliant vault allowing users to deposit assets to any ERC4626 strategy vault allowed by the factory.
contract EulerEarn is ReentrancyGuard, ERC4626, Ownable2Step, EVCUtil, IEulerEarnStaticTyping {
using Math for uint256;
using UtilsLib for uint256;
using SafeCast for uint256;
using SafeERC20 for IERC20;
using SafeERC20Permit2Lib for IERC20;
using PendingLib for MarketConfig;
using PendingLib for PendingUint136;
using PendingLib for PendingAddress;
/* IMMUTABLES */
/// @inheritdoc IEulerEarnBase
address public immutable permit2Address;
/// @inheritdoc IEulerEarnBase
address public immutable creator;
/* STORAGE */
/// @inheritdoc IEulerEarnBase
address public curator;
/// @inheritdoc IEulerEarnBase
mapping(address => bool) public isAllocator;
/// @inheritdoc IEulerEarnBase
address public guardian;
/// @inheritdoc IEulerEarnStaticTyping
mapping(IERC4626 => MarketConfig) public config;
/// @inheritdoc IEulerEarnBase
uint256 public timelock;
/// @inheritdoc IEulerEarnStaticTyping
PendingAddress public pendingGuardian;
/// @inheritdoc IEulerEarnStaticTyping
mapping(IERC4626 => PendingUint136) public pendingCap;
/// @inheritdoc IEulerEarnStaticTyping
PendingUint136 public pendingTimelock;
/// @inheritdoc IEulerEarnBase
uint96 public fee;
/// @inheritdoc IEulerEarnBase
address public feeRecipient;
/// @inheritdoc IEulerEarnBase
IERC4626[] public supplyQueue;
/// @inheritdoc IEulerEarnBase
IERC4626[] public withdrawQueue;
/// @inheritdoc IEulerEarnBase
uint256 public lastTotalAssets;
/// @inheritdoc IEulerEarnBase
uint256 public lostAssets;
/// @dev "Overrides" the ERC20's storage variable to be able to modify it.
string private _name;
/// @dev "Overrides" the ERC20's storage variable to be able to modify it.
string private _symbol;
/* CONSTRUCTOR */
/// @dev Initializes the contract.
/// @param owner The owner of the contract.
/// @param evc The EVC address.
/// @param permit2 The address of the Permit2 contract.
/// @param initialTimelock The initial timelock.
/// @param _asset The address of the underlying asset.
/// @param __name The name of the Earn vault.
/// @param __symbol The symbol of the Earn vault.
/// @dev We pass "" as name and symbol to the ERC20 because these are overriden in this contract.
/// This means that the contract deviates slightly from the ERC2612 standard.
constructor(
address owner,
address evc,
address permit2,
uint256 initialTimelock,
address _asset,
string memory __name,
string memory __symbol
) ERC4626(IERC20(_asset)) ERC20("", "") Ownable(owner) EVCUtil(evc) {
if (initialTimelock != 0) _checkTimelockBounds(initialTimelock);
_setTimelock(initialTimelock);
_name = __name;
emit EventsLib.SetName(__name);
_symbol = __symbol;
emit EventsLib.SetSymbol(__symbol);
permit2Address = permit2;
creator = msg.sender;
}
/* MODIFIERS */
/// @dev Reverts if the caller doesn't have the curator role.
modifier onlyCuratorRole() {
address msgSender = _msgSenderOnlyEVCAccountOwner();
if (msgSender != curator && msgSender != owner()) revert ErrorsLib.NotCuratorRole();
_;
}
/// @dev Reverts if the caller doesn't have the allocator role.
modifier onlyAllocatorRole() {
address msgSender = _msgSenderOnlyEVCAccountOwner();
if (!isAllocator[msgSender] && msgSender != curator && msgSender != owner()) {
revert ErrorsLib.NotAllocatorRole();
}
_;
}
/// @dev Reverts if the caller doesn't have the guardian role.
modifier onlyGuardianRole() {
address msgSender = _msgSenderOnlyEVCAccountOwner();
if (msgSender != owner() && msgSender != guardian) revert ErrorsLib.NotGuardianRole();
_;
}
/// @dev Reverts if the caller doesn't have the curator nor the guardian role.
modifier onlyCuratorOrGuardianRole() {
address msgSender = _msgSenderOnlyEVCAccountOwner();
if (msgSender != guardian && msgSender != curator && msgSender != owner()) {
revert ErrorsLib.NotCuratorNorGuardianRole();
}
_;
}
/// @dev Makes sure conditions are met to accept a pending value.
/// @dev Reverts if:
/// - there's no pending value;
/// - the timelock has not elapsed since the pending value has been submitted.
modifier afterTimelock(uint256 validAt) {
if (validAt == 0) revert ErrorsLib.NoPendingValue();
if (block.timestamp < validAt) revert ErrorsLib.TimelockNotElapsed();
_;
}
/* ONLY OWNER FUNCTIONS */
/// @inheritdoc IEulerEarnBase
function setName(string memory newName) external onlyOwner {
_name = newName;
emit EventsLib.SetName(newName);
}
/// @inheritdoc IEulerEarnBase
function setSymbol(string memory newSymbol) external onlyOwner {
_symbol = newSymbol;
emit EventsLib.SetSymbol(newSymbol);
}
/// @inheritdoc IEulerEarnBase
function setCurator(address newCurator) external onlyOwner {
if (newCurator == curator) revert ErrorsLib.AlreadySet();
curator = newCurator;
emit EventsLib.SetCurator(newCurator);
}
/// @inheritdoc IEulerEarnBase
function setIsAllocator(address newAllocator, bool newIsAllocator) external onlyOwner {
if (isAllocator[newAllocator] == newIsAllocator) revert ErrorsLib.AlreadySet();
isAllocator[newAllocator] = newIsAllocator;
emit EventsLib.SetIsAllocator(newAllocator, newIsAllocator);
}
/// @inheritdoc IEulerEarnBase
function submitTimelock(uint256 newTimelock) external onlyOwner {
if (newTimelock == timelock) revert ErrorsLib.AlreadySet();
if (pendingTimelock.validAt != 0) revert ErrorsLib.AlreadyPending();
_checkTimelockBounds(newTimelock);
if (newTimelock > timelock) {
_setTimelock(newTimelock);
} else {
// Safe "unchecked" cast because newTimelock <= MAX_TIMELOCK.
pendingTimelock.update(uint136(newTimelock), timelock);
emit EventsLib.SubmitTimelock(newTimelock);
}
}
/// @inheritdoc IEulerEarnBase
function setFee(uint256 newFee) external nonReentrant onlyOwner {
if (newFee == fee) revert ErrorsLib.AlreadySet();
if (newFee > ConstantsLib.MAX_FEE) revert ErrorsLib.MaxFeeExceeded();
if (newFee != 0 && feeRecipient == address(0)) revert ErrorsLib.ZeroFeeRecipient();
// Accrue interest and fee using the previous fee set before changing it.
_accrueInterest();
// Safe "unchecked" cast because newFee <= MAX_FEE.
fee = uint96(newFee);
emit EventsLib.SetFee(_msgSender(), fee);
}
/// @inheritdoc IEulerEarnBase
function setFeeRecipient(address newFeeRecipient) external nonReentrant onlyOwner {
if (newFeeRecipient == feeRecipient) revert ErrorsLib.AlreadySet();
if (newFeeRecipient == address(0) && fee != 0) revert ErrorsLib.ZeroFeeRecipient();
// Accrue interest and fee to the previous fee recipient set before changing it.
_accrueInterest();
feeRecipient = newFeeRecipient;
emit EventsLib.SetFeeRecipient(newFeeRecipient);
}
/// @inheritdoc IEulerEarnBase
function submitGuardian(address newGuardian) external onlyOwner {
if (newGuardian == guardian) revert ErrorsLib.AlreadySet();
if (pendingGuardian.validAt != 0) revert ErrorsLib.AlreadyPending();
if (guardian == address(0)) {
_setGuardian(newGuardian);
} else {
pendingGuardian.update(newGuardian, timelock);
emit EventsLib.SubmitGuardian(newGuardian);
}
}
/* ONLY CURATOR FUNCTIONS */
/// @inheritdoc IEulerEarnBase
function submitCap(IERC4626 id, uint256 newSupplyCap) external nonReentrant onlyCuratorRole {
if (id.asset() != asset()) revert ErrorsLib.InconsistentAsset(id);
if (pendingCap[id].validAt != 0) revert ErrorsLib.AlreadyPending();
if (config[id].removableAt != 0) revert ErrorsLib.PendingRemoval();
// For the sake of backwards compatibility, the max allowed cap can either be set to type(uint184).max or type(uint136).max.
newSupplyCap = newSupplyCap == type(uint184).max ? type(uint136).max : newSupplyCap;
uint256 supplyCap = config[id].cap;
if (newSupplyCap == supplyCap) revert ErrorsLib.AlreadySet();
if (newSupplyCap < supplyCap) {
_setCap(id, newSupplyCap.toUint136());
} else {
if (!IEulerEarnFactory(creator).isStrategyAllowed(address(id))) revert ErrorsLib.UnauthorizedMarket(id);
pendingCap[id].update(newSupplyCap.toUint136(), timelock);
emit EventsLib.SubmitCap(_msgSender(), id, newSupplyCap);
}
}
/// @inheritdoc IEulerEarnBase
function submitMarketRemoval(IERC4626 id) external onlyCuratorRole {
if (config[id].removableAt != 0) revert ErrorsLib.AlreadyPending();
if (config[id].cap != 0) revert ErrorsLib.NonZeroCap();
if (!config[id].enabled) revert ErrorsLib.MarketNotEnabled(id);
if (pendingCap[id].validAt != 0) revert ErrorsLib.PendingCap(id);
// Safe "unchecked" cast because timelock <= MAX_TIMELOCK.
config[id].removableAt = uint64(block.timestamp + timelock);
emit EventsLib.SubmitMarketRemoval(_msgSender(), id);
}
/* ONLY ALLOCATOR FUNCTIONS */
/// @inheritdoc IEulerEarnBase
function setSupplyQueue(IERC4626[] calldata newSupplyQueue) external onlyAllocatorRole {
uint256 length = newSupplyQueue.length;
if (length > ConstantsLib.MAX_QUEUE_LENGTH) revert ErrorsLib.MaxQueueLengthExceeded();
for (uint256 i; i < length; ++i) {
if (config[newSupplyQueue[i]].cap == 0) revert ErrorsLib.UnauthorizedMarket(newSupplyQueue[i]);
}
supplyQueue = newSupplyQueue;
emit EventsLib.SetSupplyQueue(_msgSender(), newSupplyQueue);
}
/// @inheritdoc IEulerEarnBase
function updateWithdrawQueue(uint256[] calldata indexes) external onlyAllocatorRole {
uint256 newLength = indexes.length;
uint256 currLength = withdrawQueue.length;
bool[] memory seen = new bool[](currLength);
IERC4626[] memory newWithdrawQueue = new IERC4626[](newLength);
for (uint256 i; i < newLength; ++i) {
uint256 prevIndex = indexes[i];
// If prevIndex >= currLength, it will revert with native "Index out of bounds".
IERC4626 id = withdrawQueue[prevIndex];
if (seen[prevIndex]) revert ErrorsLib.DuplicateMarket(id);
seen[prevIndex] = true;
newWithdrawQueue[i] = id;
}
for (uint256 i; i < currLength; ++i) {
if (!seen[i]) {
IERC4626 id = withdrawQueue[i];
if (config[id].cap != 0) revert ErrorsLib.InvalidMarketRemovalNonZeroCap(id);
if (pendingCap[id].validAt != 0) revert ErrorsLib.PendingCap(id);
if (expectedSupplyAssets(id) != 0) {
if (config[id].removableAt == 0) revert ErrorsLib.InvalidMarketRemovalNonZeroSupply(id);
if (block.timestamp < config[id].removableAt) {
revert ErrorsLib.InvalidMarketRemovalTimelockNotElapsed(id);
}
}
delete config[id];
}
}
withdrawQueue = newWithdrawQueue;
emit EventsLib.SetWithdrawQueue(_msgSender(), newWithdrawQueue);
}
/// @inheritdoc IEulerEarnBase
function reallocate(MarketAllocation[] calldata allocations) external nonReentrant onlyAllocatorRole {
address msgSender = _msgSender();
uint256 totalSupplied;
uint256 totalWithdrawn;
for (uint256 i; i < allocations.length; ++i) {
MarketAllocation memory allocation = allocations[i];
IERC4626 id = allocation.id;
if (!config[id].enabled) revert ErrorsLib.MarketNotEnabled(id);
uint256 supplyShares = config[id].balance;
uint256 supplyAssets = id.previewRedeem(supplyShares);
uint256 withdrawn = supplyAssets.zeroFloorSub(allocation.assets);
if (withdrawn > 0) {
// Guarantees that unknown frontrunning donations can be withdrawn, in order to disable a market.
uint256 shares;
if (allocation.assets == 0) {
shares = supplyShares;
withdrawn = 0;
}
uint256 withdrawnAssets;
uint256 withdrawnShares;
if (shares == 0) {
withdrawnAssets = withdrawn;
withdrawnShares = id.withdraw(withdrawn, address(this), address(this));
} else {
withdrawnAssets = id.redeem(shares, address(this), address(this));
withdrawnShares = shares;
}
config[id].balance = uint112(supplyShares - withdrawnShares);
emit EventsLib.ReallocateWithdraw(msgSender, id, withdrawnAssets, withdrawnShares);
totalWithdrawn += withdrawnAssets;
} else {
uint256 suppliedAssets = allocation.assets == type(uint256).max
? totalWithdrawn.zeroFloorSub(totalSupplied)
: allocation.assets.zeroFloorSub(supplyAssets);
if (suppliedAssets == 0) continue;
uint256 supplyCap = config[id].cap;
if (supplyAssets + suppliedAssets > supplyCap) revert ErrorsLib.SupplyCapExceeded(id);
// The vaults's underlying asset is guaranteed to be the vault's asset because it has a non-zero supply cap.
uint256 suppliedShares = id.deposit(suppliedAssets, address(this));
config[id].balance = (supplyShares + suppliedShares).toUint112();
emit EventsLib.ReallocateSupply(msgSender, id, suppliedAssets, suppliedShares);
totalSupplied += suppliedAssets;
}
}
if (totalWithdrawn != totalSupplied) revert ErrorsLib.InconsistentReallocation();
}
/* REVOKE FUNCTIONS */
/// @inheritdoc IEulerEarnBase
function revokePendingTimelock() external onlyGuardianRole {
delete pendingTimelock;
emit EventsLib.RevokePendingTimelock(_msgSender());
}
/// @inheritdoc IEulerEarnBase
function revokePendingGuardian() external onlyGuardianRole {
delete pendingGuardian;
emit EventsLib.RevokePendingGuardian(_msgSender());
}
/// @inheritdoc IEulerEarnBase
function revokePendingCap(IERC4626 id) external onlyCuratorOrGuardianRole {
delete pendingCap[id];
emit EventsLib.RevokePendingCap(_msgSender(), id);
}
/// @inheritdoc IEulerEarnBase
function revokePendingMarketRemoval(IERC4626 id) external onlyCuratorOrGuardianRole {
delete config[id].removableAt;
emit EventsLib.RevokePendingMarketRemoval(_msgSender(), id);
}
/* EXTERNAL */
/// @inheritdoc IEulerEarnBase
function supplyQueueLength() external view returns (uint256) {
return supplyQueue.length;
}
/// @inheritdoc IEulerEarnBase
function withdrawQueueLength() external view returns (uint256) {
return withdrawQueue.length;
}
/// @inheritdoc IEulerEarnBase
function maxWithdrawFromStrategy(IERC4626 id) public view returns (uint256) {
return UtilsLib.min(id.maxWithdraw(address(this)), expectedSupplyAssets(id));
}
/// @inheritdoc IEulerEarnBase
function expectedSupplyAssets(IERC4626 id) public view returns (uint256) {
return id.previewRedeem(config[id].balance);
}
/// @inheritdoc IEulerEarnBase
function acceptTimelock() external afterTimelock(pendingTimelock.validAt) {
_setTimelock(pendingTimelock.value);
}
/// @inheritdoc IEulerEarnBase
function acceptGuardian() external afterTimelock(pendingGuardian.validAt) {
_setGuardian(pendingGuardian.value);
}
/// @inheritdoc IEulerEarnBase
function acceptCap(IERC4626 id) external afterTimelock(pendingCap[id].validAt) {
if (!IEulerEarnFactory(creator).isStrategyAllowed(address(id))) revert ErrorsLib.UnauthorizedMarket(id);
// Safe "unchecked" cast because pendingCap <= type(uint136).max.
_setCap(id, uint136(pendingCap[id].value));
}
/* ERC4626 (PUBLIC) */
/// @inheritdoc IERC20Metadata
function name() public view override(IERC20Metadata, ERC20) returns (string memory) {
return _name;
}
/// @inheritdoc IERC20Metadata
function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) {
return _symbol;
}
/// @inheritdoc IERC4626
/// @dev Warning: May be higher than the actual max deposit due to duplicate vaults in the supplyQueue.
/// @dev If deposit would throw ZeroShares error, function returns 0.
function maxDeposit(address) public view override returns (uint256) {
uint256 suppliable = _maxDeposit();
return _convertToShares(suppliable, Math.Rounding.Floor) == 0 ? 0 : suppliable;
}
/// @inheritdoc IERC4626
/// @dev Warning: May be higher than the actual max mint due to duplicate vaults in the supplyQueue.
function maxMint(address) public view override returns (uint256) {
uint256 suppliable = _maxDeposit();
return _convertToShares(suppliable, Math.Rounding.Floor);
}
/// @inheritdoc IERC4626
/// @dev Warning: May be lower than the actual amount of assets that can be withdrawn by `owner` due to conversion
/// roundings between shares and assets.
function maxWithdraw(address owner) public view override returns (uint256 assets) {
(assets,,) = _maxWithdraw(owner);
}
/// @inheritdoc IERC4626
/// @dev Warning: May be lower than the actual amount of shares that can be redeemed by `owner` due to conversion
/// roundings between shares and assets.
function maxRedeem(address owner) public view override returns (uint256) {
(uint256 assets, uint256 newTotalSupply, uint256 newTotalAssets) = _maxWithdraw(owner);
return _convertToSharesWithTotals(assets, newTotalSupply, newTotalAssets, Math.Rounding.Floor);
}
/// @inheritdoc IERC4626
function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares) {
_accrueInterest();
shares = _convertToSharesWithTotals(assets, totalSupply(), lastTotalAssets, Math.Rounding.Floor);
if (shares == 0) revert ErrorsLib.ZeroShares();
_deposit(_msgSender(), receiver, assets, shares);
}
/// @inheritdoc IERC4626
function mint(uint256 shares, address receiver) public override nonReentrant returns (uint256 assets) {
_accrueInterest();
assets = _convertToAssetsWithTotals(shares, totalSupply(), lastTotalAssets, Math.Rounding.Ceil);
_deposit(_msgSender(), receiver, assets, shares);
}
/// @inheritdoc IERC4626
function withdraw(uint256 assets, address receiver, address owner)
public
override
nonReentrant
returns (uint256 shares)
{
_accrueInterest();
// Do not call expensive `maxWithdraw` and optimistically withdraw assets.
shares = _convertToSharesWithTotals(assets, totalSupply(), lastTotalAssets, Math.Rounding.Ceil);
_withdraw(_msgSender(), receiver, owner, assets, shares);
}
/// @inheritdoc IERC4626
function redeem(uint256 shares, address receiver, address owner)
public
override
nonReentrant
returns (uint256 assets)
{
_accrueInterest();
// Do not call expensive `maxRedeem` and optimistically redeem shares.
assets = _convertToAssetsWithTotals(shares, totalSupply(), lastTotalAssets, Math.Rounding.Floor);
// Since losses are not realized, exchange rate is never < 1 and zero assets check is not needed.
_withdraw(_msgSender(), receiver, owner, assets, shares);
}
/// @inheritdoc IERC4626
/// @dev totalAssets is the sum of the vault's assets on the strategy vaults plus the lost assets (see corresponding
/// docs in IEulerEarn.sol).
function totalAssets() public view override returns (uint256) {
(, uint256 newTotalAssets,) = _accruedFeeAndAssets();
return newTotalAssets;
}
/* ERC4626 (INTERNAL) */
/// @dev Returns the maximum amount of asset (`assets`) that the `owner` can withdraw from the vault, as well as the
/// new vault's total supply (`newTotalSupply`) and total assets (`newTotalAssets`).
function _maxWithdraw(address owner)
internal
view
returns (uint256 assets, uint256 newTotalSupply, uint256 newTotalAssets)
{
uint256 feeShares;
(feeShares, newTotalAssets,) = _accruedFeeAndAssets();
newTotalSupply = totalSupply() + feeShares;
assets = _convertToAssetsWithTotals(balanceOf(owner), newTotalSupply, newTotalAssets, Math.Rounding.Floor);
assets -= _simulateWithdrawStrategy(assets);
}
/// @dev Returns the maximum amount of assets that the Earn vault can supply to the strategy vaults.
function _maxDeposit() internal view returns (uint256 totalSuppliable) {
for (uint256 i; i < supplyQueue.length; ++i) {
IERC4626 id = supplyQueue[i];
uint256 supplyCap = config[id].cap;
if (supplyCap == 0) continue;
uint256 supplyAssets = expectedSupplyAssets(id);
totalSuppliable += UtilsLib.min(supplyCap.zeroFloorSub(supplyAssets), id.maxDeposit(address(this)));
}
}
/// @inheritdoc ERC4626
/// @dev The accrual of performance fees is taken into account in the conversion.
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) {
(uint256 feeShares, uint256 newTotalAssets,) = _accruedFeeAndAssets();
return _convertToSharesWithTotals(assets, totalSupply() + feeShares, newTotalAssets, rounding);
}
/// @inheritdoc ERC4626
/// @dev The accrual of performance fees is taken into account in the conversion.
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
(uint256 feeShares, uint256 newTotalAssets,) = _accruedFeeAndAssets();
return _convertToAssetsWithTotals(shares, totalSupply() + feeShares, newTotalAssets, rounding);
}
/// @dev Returns the amount of shares that the vault would exchange for the amount of `assets` provided.
/// @dev It assumes that the arguments `newTotalSupply` and `newTotalAssets` are up to date.
function _convertToSharesWithTotals(
uint256 assets,
uint256 newTotalSupply,
uint256 newTotalAssets,
Math.Rounding rounding
) internal pure returns (uint256) {
return assets.mulDiv(
newTotalSupply + ConstantsLib.VIRTUAL_AMOUNT, newTotalAssets + ConstantsLib.VIRTUAL_AMOUNT, rounding
);
}
/// @dev Returns the amount of assets that the vault would exchange for the amount of `shares` provided.
/// @dev It assumes that the arguments `newTotalSupply` and `newTotalAssets` are up to date.
function _convertToAssetsWithTotals(
uint256 shares,
uint256 newTotalSupply,
uint256 newTotalAssets,
Math.Rounding rounding
) internal pure returns (uint256) {
return shares.mulDiv(
newTotalAssets + ConstantsLib.VIRTUAL_AMOUNT, newTotalSupply + ConstantsLib.VIRTUAL_AMOUNT, rounding
);
}
/// @inheritdoc ERC4626
/// @dev Used in mint or deposit to deposit the underlying asset to strategy vaults.
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
IERC20(asset()).safeTransferFromWithPermit2(caller, address(this), assets, permit2Address);
_mint(receiver, shares);
emit Deposit(caller, receiver, assets, shares);
_supplyStrategy(assets);
// `lastTotalAssets + assets` may be a little above `totalAssets()`.
// This can lead to a small accrual of `lostAssets` at the next interaction.
_updateLastTotalAssets(lastTotalAssets + assets);
}
/// @inheritdoc ERC4626
/// @dev Used in redeem or withdraw to withdraw the underlying asset from the strategy vaults.
/// @dev Depending on 3 cases, reverts when withdrawing "too much" with:
/// 1. NotEnoughLiquidity when withdrawing more than available liquidity.
/// 2. ERC20InsufficientAllowance when withdrawing more than `caller`'s allowance.
/// 3. ERC20InsufficientBalance when withdrawing more than `owner`'s balance.
/// @dev The function prevents sending assets to addresses which are known to be EVC sub-accounts
function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
internal
override
{
// assets sent to EVC sub-accounts would be lost, as the private key for a sub-account is not known
address evcOwner = evc.getAccountOwner(receiver);
if (evcOwner != address(0) && evcOwner != receiver) {
revert ErrorsLib.BadAssetReceiver();
}
// `lastTotalAssets - assets` may be a little above `totalAssets()`.
// This can lead to a small accrual of `lostAssets` at the next interaction.
// clamp at 0 so the error raised is the more informative NotEnoughLiquidity.
_updateLastTotalAssets(lastTotalAssets.zeroFloorSub(assets));
_withdrawStrategy(assets);
super._withdraw(caller, receiver, owner, assets, shares);
}
/* INTERNAL */
/// @notice Retrieves the message sender in the context of the EVC.
/// @dev This function returns the account on behalf of which the current operation is being performed, which is
/// either msg.sender or the account authenticated by the EVC.
/// @return The address of the message sender.
function _msgSender() internal view virtual override(EVCUtil, Context) returns (address) {
return EVCUtil._msgSender();
}
/// @dev Reverts if `newTimelock` is not within the bounds.
function _checkTimelockBounds(uint256 newTimelock) internal pure {
if (newTimelock > ConstantsLib.MAX_TIMELOCK) revert ErrorsLib.AboveMaxTimelock();
if (newTimelock < ConstantsLib.POST_INITIALIZATION_MIN_TIMELOCK) revert ErrorsLib.BelowMinTimelock();
}
/// @dev Sets `timelock` to `newTimelock`.
function _setTimelock(uint256 newTimelock) internal {
timelock = newTimelock;
emit EventsLib.SetTimelock(_msgSender(), newTimelock);
delete pendingTimelock;
}
/// @dev Sets `guardian` to `newGuardian`.
function _setGuardian(address newGuardian) internal {
guardian = newGuardian;
emit EventsLib.SetGuardian(_msgSender(), newGuardian);
delete pendingGuardian;
}
/// @dev Sets the cap of the vault to `supplyCap`.
function _setCap(IERC4626 id, uint136 supplyCap) internal {
address msgSender = _msgSender();
MarketConfig storage marketConfig = config[id];
(bool success, bytes memory result) = address(id).staticcall(abi.encodeCall(this.permit2Address, ()));
address permit2 = success && result.length >= 32 ? abi.decode(result, (address)) : address(0);
if (supplyCap > 0) {
IERC20(asset()).forceApproveMaxWithPermit2(address(id), permit2);
if (!marketConfig.enabled) {
withdrawQueue.push(id);
if (withdrawQueue.length > ConstantsLib.MAX_QUEUE_LENGTH) revert ErrorsLib.MaxQueueLengthExceeded();
marketConfig.enabled = true;
marketConfig.balance = id.balanceOf(address(this)).toUint112();
// Take into account assets of the new vault without applying a fee.
_updateLastTotalAssets(lastTotalAssets + expectedSupplyAssets(id));
emit EventsLib.SetWithdrawQueue(msgSender, withdrawQueue);
}
marketConfig.removableAt = 0;
} else {
IERC20(asset()).revokeApprovalWithPermit2(address(id), permit2);
}
marketConfig.cap = supplyCap;
emit EventsLib.SetCap(msgSender, id, supplyCap);
delete pendingCap[id];
}
/* LIQUIDITY ALLOCATION */
/// @dev Supplies `assets` to the strategy vaults.
function _supplyStrategy(uint256 assets) internal {
for (uint256 i; i < supplyQueue.length; ++i) {
IERC4626 id = supplyQueue[i];
uint256 supplyCap = config[id].cap;
if (supplyCap == 0) continue;
uint256 supplyAssets = expectedSupplyAssets(id);
uint256 toSupply =
UtilsLib.min(UtilsLib.min(supplyCap.zeroFloorSub(supplyAssets), id.maxDeposit(address(this))), assets);
if (toSupply > 0) {
// Using try/catch to skip vaults that revert.
try id.deposit(toSupply, address(this)) returns (uint256 suppliedShares) {
config[id].balance = (config[id].balance + suppliedShares).toUint112();
assets -= toSupply;
} catch {}
}
if (assets == 0) return;
}
if (assets != 0) revert ErrorsLib.AllCapsReached();
}
/// @dev Withdraws `assets` from the strategy vaults.
function _withdrawStrategy(uint256 assets) internal {
for (uint256 i; i < withdrawQueue.length; ++i) {
IERC4626 id = withdrawQueue[i];
uint256 toWithdraw = UtilsLib.min(maxWithdrawFromStrategy(id), assets);
if (toWithdraw > 0) {
// Using try/catch to skip vaults that revert.
try id.withdraw(toWithdraw, address(this), address(this)) returns (uint256 withdrawnShares) {
config[id].balance = uint112(config[id].balance - withdrawnShares);
assets -= toWithdraw;
} catch {}
}
if (assets == 0) return;
}
if (assets != 0) revert ErrorsLib.NotEnoughLiquidity();
}
/// @dev Simulates a withdraw of `assets` from the strategy vaults.
/// @return The remaining assets to be withdrawn.
function _simulateWithdra
Submitted on: 2025-11-06 11:45:14
Comments
Log in to comment.
No comments yet.