diff --git a/docs/synths.md b/docs/synths.md new file mode 100644 index 00000000..4ca59d3a --- /dev/null +++ b/docs/synths.md @@ -0,0 +1,170 @@ +# Synthetic Assets and Fee Distribution Contracts + +This document describes the new contracts introduced to support synthetic asset creation and automated fee distribution across the Euler Vault Kit ecosystem. These contracts extend the original synthetic asset system described in the Euler Vault Kit whitepaper with cross-chain capabilities and enhanced automation. + +## Overview + +The synthetic asset system consists of seven main contracts that work together to enable cross-chain synthetic asset creation and automated fee collection/distribution: + +1. **ERC20Synth** - A modified synthetic token contract for LayerZero OFT compatibility +2. **FeeFlowControllerEVK** - An enhanced fee auction controller for EVK vaults with automated fee conversion +3. **FeeCollectorUtil** - Base contract for fee collection functionality +4. **FeeCollectorGulper** - Local fee collection and distribution via EulerSavingsRate +5. **OFTFeeCollector** - Cross-chain fee collection orchestrator using LayerZero +6. **OFTGulper** - Cross-chain fee receiver and distributor via LayerZero composer +7. **IRMBasePremium** - Generic interest rate model with base rate and premium rate components + +This system builds upon the original synthetic asset architecture described in the Euler Vault Kit whitepaper, which includes: +- **ESynth** - The original synthetic token contract +- **IRMSynth** - Reactive interest rate model for synthetic assets +- **EulerSavingsRate** - Interest-bearing vault with smearing mechanism +- **PegStabilityModule** - Peg stability mechanism for synthetic assets + +## Contract Descriptions + +### ERC20Synth + +**Purpose**: A synthetic ERC-20 token designed for cross-chain operations with LayerZero OFT compatibility. This contract extends the original ESynth functionality to support cross-chain token bridging. + +**Key Features**: +- Inherits from `ERC20BurnableMintable` (which currently acts as EUL implementation for non-Ethereum chains) +- Role-based access control using `AccessControlEnumerable` +- Minting capacity management per minter with LayerZero compatibility +- Total supply exclusion for vault addresses (same as original ESynth) +- EVC integration for account management +- Cross-chain mint/burn interface compatibility + +**Key Changes from Original ESynth**: +- **Inheritance**: Changed from `Ownable` to `AccessControlEnumerable` for more granular permissions +- **Base Contract**: Now inherits from `ERC20BurnableMintable` instead of `ERC20EVCCompatible` +- **Minting Logic**: If minter capacity equals `type(uint128).max`, capacity is not decreased (for LayerZero infinite minting) +- **Burning Interface**: Added `burnFrom` function for LayerZero interface compatibility +- **EVC Check**: Removed `E_NotEVCCompatible` check as it's not strictly necessary +- **Core Mechanics**: Preserved the original synthetic asset mechanics (minting capacity, total supply exclusion, vault allocation) + +**Roles**: +- `DEFAULT_ADMIN_ROLE`: Can grant/revoke roles and manage ignored addresses +- `MINTER_ROLE`: Can mint tokens up to their assigned capacity +- `REVOKE_MINTER_ROLE`: Can revoke minter role from addresses in emergency situations +- `ALLOCATOR_ROLE`: Can allocate/deallocate tokens to/from vaults + +### FeeFlowControllerEVK + +**Purpose**: An enhanced version of the FeeFlowController specifically designed for EVK vaults with automated fee conversion, optional payment token bridging via LayerZero OFT adapter and hook-based automation. + +**Key Differences from Original FeeFlowController**: +- **Fee Conversion**: Automatically calls `IEVault(assets[i]).convertFees()` for each asset before transfer +- **LayerZero OFT Integration**: Supports bridging the payment token to a remote chain using a LayerZero OFT adapter. If `oftAdapter` is set, payment tokens are sent cross-chain to the configured receiver; otherwise, they are transferred locally. +- **Hook Support**: Added `hookTarget` and `hookTargetSelector` parameters for post-transaction automation + +### FeeCollectorUtil + +**Purpose**: Base contract that provides common fee collection functionality for multiple vaults. + +**Key Features**: +- **Multi-Vault Support**: Manages a list of vaults for fee collection +- **Role-Based Access**: Separate roles for maintenance and collection operations +- **Asset Validation**: Ensures all vaults use the same underlying asset as the fee token +- **Fee Conversion**: Automatically converts fees and redeems vault shares +- **Token Recovery**: Emergency recovery functions for tokens and native currency + +**Roles**: +- `DEFAULT_ADMIN_ROLE`: Can configure the contract and recover tokens +- `MAINTAINER_ROLE`: Can add/remove vaults from the collection list +- `COLLECTOR_ROLE`: Can execute the fee collection process + +### FeeCollectorGulper + +**Purpose**: Collects fees from multiple vaults and deposits them into an EulerSavingsRate contract with automated interest smearing. + +**Key Features**: +- **Inherits from FeeCollectorUtil**: Uses base fee collection functionality +- **EulerSavingsRate Integration**: Deposits collected fees into ESR contract +- **Interest Smearing**: Automatically calls `gulp()` to distribute interest over 2-week periods +- **Local Distribution**: Handles fee distribution without cross-chain transfers + +### OFTFeeCollector + +**Purpose**: Collects fees from multiple vaults and sends them cross-chain via LayerZero OFT adapter. + +**Key Features**: +- **Inherits from FeeCollectorUtil**: Uses base fee collection functionality +- **Cross-Chain Transfer**: Uses LayerZero OFT adapter for cross-chain fee distribution +- **OFT Validation**: Ensures OFT adapter token matches the fee token +- **Composed Messages**: Support for composed messages for automated interest smearing + +### OFTGulper + +**Purpose**: Receives fee tokens cross-chain and deposits them into an EulerSavingsRate vault with automated interest smearing. + +**Key Features**: +- **LayerZero Integration**: Implements `ILayerZeroComposer` for cross-chain message handling +- **Automated Deposits**: Deposits received tokens into EulerSavingsRate vault +- **Interest Smearing**: Calls `gulp()` to distribute interest over a 2-week period +- **Simple Ownership**: Uses `Ownable` for straightforward administration +- **Public Execution**: Anyone can trigger the deposit and gulp process + +### IRMBasePremium + +**Purpose**: Generic interest rate model that provides a base rate plus a premium rate, with the ability to override premium rates for specific vaults. + +**Key Features**: +- **Dual Rate Structure**: Interest rate = base rate + premium rate +- **Vault-Specific Overrides**: Individual vaults can have custom premium rates +- **Role-Based Administration**: Separate roles for rate management +- **EVC Integration**: Uses EVC for account management and authentication +- **Flexible Configuration**: Base and premium rates can be updated independently + +**Roles**: +- `DEFAULT_ADMIN_ROLE`: Can grant/revoke roles and manage the contract +- `RATE_ADMIN_ROLE`: Can update base rate, premium rate, and vault-specific overrides + +## System Integration Overview + +The synthetic asset system creates a multi-chain ecosystem where synthetic tokens maintain their peg through EulerSwap pools rather than traditional peg stability modules. The system operates across multiple networks with a centralized interest distribution mechanism. + +### Multi-Chain Token Deployment + +The `ERC20Synth` contract will be deployed on multiple networks, each maintaining the same synthetic token but operating independently. These tokens inherit from `ERC20BurnableMintable`, making them compatible with LayerZero's OFT standard for seamless cross-chain transfers. Each deployment maintains its own minting capacity and role-based access control, allowing network-specific minters while preserving the core synthetic asset mechanics. + +### Centralized Interest Distribution + +A single canonical `EulerSavingsRate` (ESR) instance operates on Ethereum as the central hub for interest distribution. This ESR contract receives fees from all networks through the cross-chain fee collection system and distributes interest to depositors using its smearing mechanism over two-week periods. The centralized approach ensures consistent interest rates across all networks while maintaining the security and efficiency of the original ESR design. + +### Network-Specific Vault Architecture + +On each network, multiple synthetic asset vaults are created using the EVK system. These vaults are configured with hooks that restrict deposits to only the synthetic token itself, implementing the same security model as the original `ESynth` system. Each vault uses `IRMBasePremium` for flexible interest rate management, allowing different risk profiles and premium rates for different vaults while maintaining a consistent base rate structure. + +The vaults are configured with 100% interest fees, meaning all interest generated from borrowing flows directly to the fee collection system rather than being distributed to depositors. This creates a revenue stream that can be efficiently collected and distributed through the cross-chain infrastructure. + +### Fee Flow Orchestration + +The `FeeFlowControllerEVK` operates on each network, periodically conducting Dutch auctions to sell the accumulated fees. Unlike the original system, this controller automatically calls `convertFees()` on all vaults before transferring assets, ensuring fees are properly converted to transferable tokens. The controller will be configured with the `OFTFeeCollector` as a hook target to automate the fee collection process, seamlessly integrating with the broader fee distribution system. + +### Cross-Chain Fee Collection + +Two complementary systems handle fee collection and distribution: + +**Cross-Chain Collection**: `OFTFeeCollector` contracts on each network collect fees from multiple vaults and send them cross-chain to Ethereum using LayerZero OFT adapters. These collectors inherit from `FeeCollectorUtil`, providing consistent fee collection logic while supporting composed messages for automated interest smearing. + +**Cross-Chain Reception**: `OFTGulper` contracts on Ethereum receive the cross-chain fees and automatically deposit them into the canonical ESR instance. The gulper implements LayerZero's composer interface, allowing it to receive fees and immediately trigger the `gulp()` function to distribute interest over the two-week smearing period. + +### Peg Stability Through EulerSwap + +Instead of using traditional peg stability modules, the system leverages EulerSwap pools to maintain synthetic asset pegs. These pools are created on top of the Euler lending infrastructure, allowing the same assets to simultaneously facilitate swaps, earn lending yield, and serve as collateral. This approach provides deeper liquidity and more efficient peg maintenance compared to traditional AMM mechanisms. + +The EulerSwap integration enables just-in-time liquidity provision, where the protocol can borrow output tokens on-demand using input tokens as collateral. This mechanism can simulate up to 50x the depth of traditional AMMs, particularly effective for stable or pegged asset markets where pricing risk is minimal. + +### Interest Rate Management + +The `IRMBasePremium` system provides flexible interest rate management across all networks. Each IRM instance can serve multiple vaults with different risk profiles, using a dual-rate structure that separates base rates from risk premiums. Vault-specific overrides allow for customized pricing based on individual vault characteristics, while centralized control enables coordinated rate adjustments across the ecosystem. + +The IRM also serves as a crucial peg stability mechanism. When synthetic assets trade below their target price, interest rates are raised, encouraging borrowers to repay their loans and creating buy pressure on the synthetic asset. Conversely, when synthetic assets trade above their target price, interest rates are lowered, reducing the incentive to repay and allowing the price to stabilize. This reactive interest rate adjustment works in conjunction with EulerSwap pools to maintain tight peg stability across all networks. + +### Operational Flow + +The complete system operates through a coordinated flow: vaults generate fees through normal lending operations, the fee flow controller periodically auctions these fees, fee collectors gather the proceeds from multiple vaults, and the cross-chain infrastructure transports these fees to the canonical ESR on Ethereum. The ESR then distributes interest to depositors using its smearing mechanism, while EulerSwap pools maintain synthetic asset pegs through efficient liquidity provision. + +This architecture creates a sophisticated multi-chain synthetic asset system that combines the security of the original Euler Vault Kit with modern cross-chain infrastructure and efficient peg maintenance mechanisms, enabling synthetic assets to operate seamlessly across multiple networks while maintaining consistent interest distribution and peg stability. + +![Synths System Architecture](./synths.png) diff --git a/docs/synths.png b/docs/synths.png new file mode 100644 index 00000000..8bb677da Binary files /dev/null and b/docs/synths.png differ diff --git a/src/ERC20/deployed/ERC20BurnableMintable.sol b/src/ERC20/deployed/ERC20BurnableMintable.sol index 82413624..19bb8e81 100644 --- a/src/ERC20/deployed/ERC20BurnableMintable.sol +++ b/src/ERC20/deployed/ERC20BurnableMintable.sol @@ -45,7 +45,7 @@ contract ERC20BurnableMintable is AccessControlEnumerable, ERC20Burnable, ERC20P /// @notice Mints new tokens and assigns them to an account /// @param _account The address that will receive the minted tokens /// @param _amount The amount of tokens to mint - function mint(address _account, uint256 _amount) external onlyRole(MINTER_ROLE) { + function mint(address _account, uint256 _amount) external virtual onlyRole(MINTER_ROLE) { _mint(_account, _amount); } diff --git a/src/ERC20/deployed/ERC20Synth.sol b/src/ERC20/deployed/ERC20Synth.sol new file mode 100644 index 00000000..b2d6e84d --- /dev/null +++ b/src/ERC20/deployed/ERC20Synth.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {ERC20BurnableMintable} from "./ERC20BurnableMintable.sol"; +import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; +import {Context} from "openzeppelin-contracts/utils/Context.sol"; +import {AccessControl} from "openzeppelin-contracts/access/AccessControl.sol"; +import {IAccessControl} from "openzeppelin-contracts/access/IAccessControl.sol"; +import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; + +/// @title ERC20Synth +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice ERC20-compatible synthetic token with EVC support, role-based minting, burning, and supply management. +/// @dev This contract is designed for token bridging and synthetic asset vaults. Minting is controlled by MINTER_ROLE, +/// and minting capacity is tracked per minter. The REVOKE_MINTER_ROLE can revoke minting rights in emergencies. +/// The contract supports excluding certain addresses from total supply calculations (e.g., vaults). +contract ERC20Synth is ERC20BurnableMintable, EVCUtil { + using EnumerableSet for EnumerableSet.AddressSet; + + /// @notice Struct holding minting capacity and minted amount for a minter. + struct MinterData { + uint128 capacity; + uint128 minted; + } + + /// @notice Role that allows allocation and deallocation to vaults. + bytes32 public constant ALLOCATOR_ROLE = keccak256("ALLOCATOR_ROLE"); + + /// @notice Mapping of minter address to their minting data (capacity and minted amount). + mapping(address => MinterData) public minters; + + /// @notice Set of addresses to ignore for total supply calculations (e.g., vaults, contract itself). + EnumerableSet.AddressSet internal _ignoredForTotalSupply; + + /// @notice Emitted when a minter's capacity is set or updated. + /// @param minter The address of the minter. + /// @param capacity The new minting capacity for the minter. + event MinterCapacitySet(address indexed minter, uint256 capacity); + + /// @notice Error thrown when a minter exceeds their minting capacity. + error CapacityReached(); + + /// @notice Deploys the ESynth contract. + /// @param evc_ Address of the EVC (Ethereum Vault Connector). + /// @param admin_ Address to be granted DEFAULT_ADMIN_ROLE. + /// @param name_ Name of the token. + /// @param symbol_ Symbol of the token. + /// @param decimals_ Number of decimals for the token. + constructor(address evc_, address admin_, string memory name_, string memory symbol_, uint8 decimals_) + ERC20BurnableMintable(admin_, name_, symbol_, decimals_) + EVCUtil(evc_) + { + _ignoredForTotalSupply.add(address(this)); + } + + /// @notice Grants a role to an account. Only callable by EVC account owner. + /// @param role The role to grant. + /// @param account The address to grant the role to. + function grantRole(bytes32 role, address account) + public + virtual + override (AccessControl, IAccessControl) + onlyEVCAccountOwner + { + super.grantRole(role, account); + } + + /// @notice Revokes a role from an account. Only callable by EVC account owner. + /// @param role The role to revoke. + /// @param account The address to revoke the role from. + function revokeRole(bytes32 role, address account) + public + virtual + override (AccessControl, IAccessControl) + onlyEVCAccountOwner + { + super.revokeRole(role, account); + } + + /// @notice Renounces a role for the calling account. Only callable by EVC account owner. + /// @param role The role to renounce. + /// @param callerConfirmation The address of the caller (must match msg.sender). + function renounceRole(bytes32 role, address callerConfirmation) + public + virtual + override (AccessControl, IAccessControl) + onlyEVCAccountOwner + { + super.renounceRole(role, callerConfirmation); + } + + /// @notice Sets the minting capacity for a minter and grants MINTER_ROLE if not already granted. + /// @param minter The address of the minter. + /// @param capacity The new minting capacity for the minter. + function setCapacity(address minter, uint128 capacity) external onlyEVCAccountOwner onlyRole(DEFAULT_ADMIN_ROLE) { + _grantRole(MINTER_ROLE, minter); + minters[minter].capacity = capacity; + emit MinterCapacitySet(minter, capacity); + } + + /// @notice Mints new tokens to a specified account, respecting the minter's capacity. + /// @param account The address to receive the minted tokens. + /// @param amount The amount of tokens to mint. + function mint(address account, uint256 amount) external override onlyEVCAccountOwner onlyRole(MINTER_ROLE) { + address sender = _msgSender(); + MinterData memory minterCache = minters[sender]; + + if (amount == 0) return; + + // If the minter has a finite capacity, check for overflow and capacity. + if ( + minterCache.capacity != type(uint128).max + && ( + amount > type(uint128).max - minterCache.minted + || minterCache.capacity < uint256(minterCache.minted) + amount + ) + ) { + revert CapacityReached(); + } + + // Only update minted amount if the minter has a finite capacity. + if (minterCache.capacity != type(uint128).max) { + minterCache.minted += uint128(amount); // safe to down-cast because amount <= capacity <= max uint128 + minters[sender] = minterCache; + } + + _mint(account, amount); + } + + /// @notice Burns tokens from the caller's balance and decreases their minted amount. + /// @param amount The amount of tokens to burn. + function burn(uint256 amount) public override { + if (amount == 0) return; + address sender = _msgSender(); + _decreaseMinted(sender, amount); + _burn(sender, amount); + } + + /// @notice Burns tokens from another account, using allowance if required, and decreases their minted amount. + /// @param account The account to burn tokens from. + /// @param amount The amount of tokens to burn. + function burnFrom(address account, uint256 amount) public override { + if (amount == 0) return; + + address sender = _msgSender(); + + // Allowance check: required unless burning from self, or admin burning from contract itself. + if (account != sender && !(account == address(this) && hasRole(DEFAULT_ADMIN_ROLE, sender))) { + _spendAllowance(account, sender, amount); + } + + _decreaseMinted(account, amount); + _burn(account, amount); + } + + /// @notice Allocates tokens from this contract to a vault and adds the vault to ignored supply. + /// @param vault The vault address to allocate to. + /// @param amount The amount of tokens to allocate. + function allocate(address vault, uint256 amount) external onlyEVCAccountOwner onlyRole(ALLOCATOR_ROLE) { + _ignoredForTotalSupply.add(vault); + _approve(address(this), vault, amount, true); + IEVault(vault).deposit(amount, address(this)); + } + + /// @notice Deallocates tokens from a vault back to this contract. + /// @param vault The vault address to deallocate from. + /// @param amount The amount of tokens to deallocate. + function deallocate(address vault, uint256 amount) external onlyEVCAccountOwner onlyRole(ALLOCATOR_ROLE) { + IEVault(vault).withdraw(amount, address(this), address(this)); + } + + /// @notice Adds an account to the set of addresses ignored for total supply. + /// @param account The address to add. + /// @return success True if the account was added, false if it was already present. + function addIgnoredForTotalSupply(address account) + external + onlyEVCAccountOwner + onlyRole(DEFAULT_ADMIN_ROLE) + returns (bool success) + { + return _ignoredForTotalSupply.add(account); + } + + /// @notice Removes an account from the set of addresses ignored for total supply. + /// @param account The address to remove. + /// @return success True if the account was removed, false if it was not present. + function removeIgnoredForTotalSupply(address account) + external + onlyEVCAccountOwner + onlyRole(DEFAULT_ADMIN_ROLE) + returns (bool success) + { + return _ignoredForTotalSupply.remove(account); + } + + /// @notice Checks if an account is ignored for total supply. + /// @param account The address to check. + /// @return isIgnored True if the account is ignored, false otherwise. + function isIgnoredForTotalSupply(address account) external view returns (bool isIgnored) { + return _ignoredForTotalSupply.contains(account); + } + + /// @notice Returns all accounts ignored for total supply. + /// @return accounts Array of ignored addresses. + function getAllIgnoredForTotalSupply() external view returns (address[] memory accounts) { + return _ignoredForTotalSupply.values(); + } + + /// @notice Returns the total supply, excluding balances of ignored accounts. + /// @return total The effective total supply. + function totalSupply() public view override returns (uint256 total) { + total = super.totalSupply(); + + uint256 ignoredLength = _ignoredForTotalSupply.length(); + for (uint256 i = 0; i < ignoredLength; ++i) { + total -= balanceOf(_ignoredForTotalSupply.at(i)); + } + return total; + } + + /// @notice Decreases the minted amount for an account, resetting to zero if burning more than minted. + /// @param account The account whose minted amount to decrease. + /// @param amount The amount to decrease. + function _decreaseMinted(address account, uint256 amount) internal { + MinterData memory minterCache = minters[account]; + + // If burning more than minted, reset minted to 0 + unchecked { + // down-casting is safe because amount < minted <= max uint128 + minterCache.minted = minterCache.minted > amount ? minterCache.minted - uint128(amount) : 0; + } + minters[account] = minterCache; + } + + /// @notice Retrieves the message sender in the context of the EVC. + /// @return msgSender The address of the message sender. + function _msgSender() internal view virtual override (EVCUtil, Context) returns (address msgSender) { + return EVCUtil._msgSender(); + } +} diff --git a/src/FeeFlow/FeeFlowControllerEVK.sol b/src/FeeFlow/FeeFlowControllerEVK.sol new file mode 100644 index 00000000..eb3fb483 --- /dev/null +++ b/src/FeeFlow/FeeFlowControllerEVK.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.24; + +import {IERC20, SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; +import {IOFT, SendParam} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol"; +import {MessagingFee} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OAppSender.sol"; +import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; + +/// @title FeeFlowControllerEVK +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://eulerlabs.com) +/// @notice Continous back to back dutch auctions selling any asset received by this contract. The EVK version +/// introduces: +/// - convertFees() call iteration over the provided assets array to avoid 10 vault status checks limit of the EVC if +/// called outside of the EVC checks-deferred context +/// - additional call imposed upon the buyer. This way, the protocol can automate certain periodic operations that would +/// otherwise require manual intervention +contract FeeFlowControllerEVK is EVCUtil { + using SafeERC20 for IERC20; + + uint256 public constant MIN_EPOCH_PERIOD = 1 hours; + uint256 public constant MAX_EPOCH_PERIOD = 365 days; + uint256 public constant MIN_PRICE_MULTIPLIER = 1.1e18; // Should at least be 110% of settlement price + uint256 public constant MAX_PRICE_MULTIPLIER = 3e18; // Should not exceed 300% of settlement price + uint256 public constant ABS_MIN_INIT_PRICE = 1e6; // Minimum sane value for init price + uint256 public constant ABS_MAX_INIT_PRICE = type(uint192).max; // chosen so that initPrice * priceMultiplier does + // not exceed uint256 + uint256 public constant PRICE_MULTIPLIER_SCALE = 1e18; + + IERC20 public immutable paymentToken; + address public immutable paymentReceiver; + uint256 public immutable epochPeriod; + uint256 public immutable priceMultiplier; + uint256 public immutable minInitPrice; + + address public immutable oftAdapter; + uint32 public immutable dstEid; + + address public immutable hookTarget; + bytes4 public immutable hookTargetSelector; + + struct Slot0 { + uint8 locked; // 1 if locked, 2 if unlocked + uint16 epochId; // intentionally overflowable + uint192 initPrice; + uint40 startTime; + } + + Slot0 internal slot0; + + event Buy(address indexed buyer, address indexed assetsReceiver, uint256 paymentAmount); + + error Reentrancy(); + error InitPriceBelowMin(); + error InitPriceExceedsMax(); + error EpochPeriodBelowMin(); + error EpochPeriodExceedsMax(); + error PriceMultiplierBelowMin(); + error PriceMultiplierExceedsMax(); + error MinInitPriceBelowMin(); + error MinInitPriceExceedsAbsMaxInitPrice(); + error DeadlinePassed(); + error EmptyAssets(); + error EpochIdMismatch(); + error MaxPaymentTokenAmountExceeded(); + error PaymentReceiverIsThis(); + error InvalidOFTAdapter(); + error EmptyError(); + + modifier nonReentrant() { + if (slot0.locked == 2) revert Reentrancy(); + slot0.locked = 2; + _; + slot0.locked = 1; + } + + modifier nonReentrantView() { + if (slot0.locked == 2) revert Reentrancy(); + _; + } + + /// @dev Initializes the FeeFlowControllerEVK contract with the specified parameters. + /// @param evc The address of the Ethereum Vault Connector (EVC) contract. + /// @param initPrice The initial price for the first epoch. + /// @param paymentToken_ The address of the payment token. + /// @param paymentReceiver_ The address of the payment receiver. + /// @param epochPeriod_ The duration of each epoch period. + /// @param priceMultiplier_ The multiplier for adjusting the price from one epoch to the next. + /// @param minInitPrice_ The minimum allowed initial price for an epoch. + /// @param oftAdapter_ The address of the OFT adapter. + /// @param dstEid_ The LayerZero endpoint ID of the destination chain. + /// @param hookTarget_ The address of the hook target. + /// @param hookTargetSelector_ The selector for the hook target to call on. + /// @notice This constructor performs parameter validation and sets the initial values for the contract. + constructor( + address evc, + uint256 initPrice, + address paymentToken_, + address paymentReceiver_, + uint256 epochPeriod_, + uint256 priceMultiplier_, + uint256 minInitPrice_, + address oftAdapter_, + uint32 dstEid_, + address hookTarget_, + bytes4 hookTargetSelector_ + ) EVCUtil(evc) { + if (initPrice < minInitPrice_) revert InitPriceBelowMin(); + if (initPrice > ABS_MAX_INIT_PRICE) revert InitPriceExceedsMax(); + if (epochPeriod_ < MIN_EPOCH_PERIOD) revert EpochPeriodBelowMin(); + if (epochPeriod_ > MAX_EPOCH_PERIOD) revert EpochPeriodExceedsMax(); + if (priceMultiplier_ < MIN_PRICE_MULTIPLIER) revert PriceMultiplierBelowMin(); + if (priceMultiplier_ > MAX_PRICE_MULTIPLIER) revert PriceMultiplierExceedsMax(); + if (minInitPrice_ < ABS_MIN_INIT_PRICE) revert MinInitPriceBelowMin(); + if (minInitPrice_ > ABS_MAX_INIT_PRICE) revert MinInitPriceExceedsAbsMaxInitPrice(); + if (paymentReceiver_ == address(this)) revert PaymentReceiverIsThis(); + if (oftAdapter_ != address(0) && address(paymentToken_) != IOFT(oftAdapter_).token()) { + revert InvalidOFTAdapter(); + } + + slot0.initPrice = uint192(initPrice); + slot0.startTime = uint40(block.timestamp); + + paymentToken = IERC20(paymentToken_); + paymentReceiver = paymentReceiver_; + epochPeriod = epochPeriod_; + priceMultiplier = priceMultiplier_; + minInitPrice = minInitPrice_; + oftAdapter = oftAdapter_; + dstEid = dstEid_; + hookTarget = hookTarget_; + hookTargetSelector = hookTargetSelector_; + } + + /// @dev Allows a user to buy assets by transferring payment tokens and receiving the assets. + /// @param assets The addresses of the assets to be bought. + /// @param assetsReceiver The address that will receive the bought assets. + /// @param epochId Id of the epoch to buy from, will revert if not the current epoch + /// @param deadline The deadline timestamp for the purchase. + /// @param maxPaymentTokenAmount The maximum amount of payment tokens the user is willing to spend. + /// @return paymentAmount The amount of payment tokens transferred for the purchase. + /// @notice This function performs various checks and transfers the payment tokens to the payment receiver. + /// It also transfers the assets to the assets receiver and sets up a new auction with an updated initial price. + function buy( + address[] calldata assets, + address assetsReceiver, + uint256 epochId, + uint256 deadline, + uint256 maxPaymentTokenAmount + ) external nonReentrant returns (uint256 paymentAmount) { + if (block.timestamp > deadline) revert DeadlinePassed(); + if (assets.length == 0) revert EmptyAssets(); + + Slot0 memory slot0Cache = slot0; + + if (uint16(epochId) != slot0Cache.epochId) revert EpochIdMismatch(); + + address sender = _msgSender(); + + paymentAmount = getPriceFromCache(slot0Cache); + + if (paymentAmount > maxPaymentTokenAmount) revert MaxPaymentTokenAmountExceeded(); + + if (paymentAmount > 0) { + if (oftAdapter == address(0)) { + paymentToken.safeTransferFrom(sender, paymentReceiver, paymentAmount); + } else { + paymentToken.safeTransferFrom(sender, address(this), paymentAmount); + + SendParam memory sendParam = SendParam({ + dstEid: dstEid, + to: bytes32(uint256(uint160(paymentReceiver))), + amountLD: paymentAmount, + minAmountLD: paymentAmount, + extraOptions: "", + composeMsg: "", + oftCmd: "" + }); + MessagingFee memory fee = IOFT(oftAdapter).quoteSend(sendParam, false); + + paymentToken.forceApprove(oftAdapter, paymentAmount); + IOFT(oftAdapter).send{value: fee.nativeFee}(sendParam, fee, address(this)); + } + } + + for (uint256 i = 0; i < assets.length; ++i) { + // Convert fees + IEVault(assets[i]).convertFees(); + + // Transfer full balance to buyer + uint256 balance = IERC20(assets[i]).balanceOf(address(this)); + IERC20(assets[i]).safeTransfer(assetsReceiver, balance); + } + + // Setup new auction + uint256 newInitPrice = paymentAmount * priceMultiplier / PRICE_MULTIPLIER_SCALE; + + if (newInitPrice > ABS_MAX_INIT_PRICE) { + newInitPrice = ABS_MAX_INIT_PRICE; + } else if (newInitPrice < minInitPrice) { + newInitPrice = minInitPrice; + } + + // epochID is allowed to overflow, effectively reusing them + unchecked { + slot0Cache.epochId++; + } + slot0Cache.initPrice = uint192(newInitPrice); + slot0Cache.startTime = uint40(block.timestamp); + + // Write cache in single write + slot0 = slot0Cache; + + emit Buy(sender, assetsReceiver, paymentAmount); + + // Perform the hook call if the hook target is set + if (hookTarget != address(0)) { + // We do not check the success of the call as we allow it silently fail + (bool success,) = hookTarget.call(abi.encode(hookTargetSelector)); + success; + } + + return paymentAmount; + } + + /// @dev Retrieves the current price from the cache based on the elapsed time since the start of the epoch. + /// @param slot0Cache The Slot0 struct containing the initial price and start time of the epoch. + /// @return price The current price calculated based on the elapsed time and the initial price. + /// @notice This function calculates the current price by subtracting a fraction of the initial price based on the + /// elapsed time. + // If the elapsed time exceeds the epoch period, the price will be 0. + function getPriceFromCache(Slot0 memory slot0Cache) internal view returns (uint256) { + uint256 timePassed = block.timestamp - slot0Cache.startTime; + + if (timePassed > epochPeriod) { + return 0; + } + + return slot0Cache.initPrice - slot0Cache.initPrice * timePassed / epochPeriod; + } + + /// @dev Calculates the current price + /// @return price The current price calculated based on the elapsed time and the initial price. + /// @notice Uses the internal function `getPriceFromCache` to calculate the current price. + function getPrice() external view nonReentrantView returns (uint256) { + return getPriceFromCache(slot0); + } + + /// @dev Retrieves Slot0 as a memory struct + /// @return Slot0 The Slot0 value as a Slot0 struct + function getSlot0() external view nonReentrantView returns (Slot0 memory) { + return slot0; + } +} diff --git a/src/IRM/IRMBasePremium.sol b/src/IRM/IRMBasePremium.sol new file mode 100644 index 00000000..93383196 --- /dev/null +++ b/src/IRM/IRMBasePremium.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {AccessControlEnumerable} from "openzeppelin-contracts/access/extensions/AccessControlEnumerable.sol"; +import {AccessControl} from "openzeppelin-contracts/access/AccessControl.sol"; +import {Context} from "openzeppelin-contracts/utils/Context.sol"; +import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; +import {IAccessControl} from "openzeppelin-contracts/access/IAccessControl.sol"; +import {IIRM} from "evk/InterestRateModels/IIRM.sol"; + +/// @title IRMBasePremium +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice Generic interest rate model where the interest rate is the sum of a base rate and a premium rate, +/// with the ability to override the premium rate for specific vaults. +contract IRMBasePremium is AccessControlEnumerable, EVCUtil, IIRM { + /// @notice Struct for storing per-vault premium rate overrides. + /// @param exists Whether an override exists for the vault. + /// @param premiumRate The premium rate to use if override exists. + struct RateOverride { + bool exists; + uint248 premiumRate; + } + + /// @notice Role that allows updating the base rate and premium rates. + bytes32 public constant RATE_ADMIN_ROLE = keccak256("RATE_ADMIN_ROLE"); + + /// @notice The default base interest rate (applied to all vaults). + uint128 public baseRate; + + /// @notice The default premium interest rate (applied to all vaults unless overridden). + uint128 public premiumRate; + + /// @notice Mapping of vault address to its rate override (if any). + mapping(address vault => RateOverride) public _rateOverrides; + + /// @notice Emitted when the base interest rate is changed. + /// @param newBaseRate The new base interest rate. + event BaseRateSet(uint256 newBaseRate); + + /// @notice Emitted when the premium interest rate is changed. + /// @param newPremiumRate The new premium interest rate. + event PremiumRateSet(uint256 newPremiumRate); + + /// @notice Emitted when a rate override is set or cleared for a vault. + /// @param vault The address of the vault. + /// @param exists Whether the override exists. + /// @param premiumRate The premium rate set for the override. + event RateOverrideSet(address indexed vault, bool exists, uint256 premiumRate); + + /// @notice Deploy a new IRMBasePremium interest rate model. + /// @param evc_ The address of the EVC. + /// @param admin_ The address to be granted DEFAULT_ADMIN_ROLE. + /// @param baseRate_ The default base interest rate. + /// @param premiumRate_ The default premium interest rate. + constructor(address evc_, address admin_, uint128 baseRate_, uint128 premiumRate_) EVCUtil(evc_) { + _grantRole(DEFAULT_ADMIN_ROLE, admin_); + baseRate = baseRate_; + premiumRate = premiumRate_; + emit BaseRateSet(baseRate_); + emit PremiumRateSet(premiumRate_); + } + + /// @notice Grants a role to an account. Only callable by EVC account owner. + /// @param role The role to grant. + /// @param account The address to grant the role to. + function grantRole(bytes32 role, address account) + public + virtual + override (AccessControl, IAccessControl) + onlyEVCAccountOwner + { + super.grantRole(role, account); + } + + /// @notice Revokes a role from an account. Only callable by EVC account owner. + /// @param role The role to revoke. + /// @param account The address to revoke the role from. + function revokeRole(bytes32 role, address account) + public + virtual + override (AccessControl, IAccessControl) + onlyEVCAccountOwner + { + super.revokeRole(role, account); + } + + /// @notice Renounces a role for the calling account. Only callable by EVC account owner. + /// @param role The role to renounce. + /// @param callerConfirmation The address of the caller (must match msg.sender). + function renounceRole(bytes32 role, address callerConfirmation) + public + virtual + override (AccessControl, IAccessControl) + onlyEVCAccountOwner + { + super.renounceRole(role, callerConfirmation); + } + + /// @notice Set the default base interest rate. + /// @param baseRate_ The new base interest rate. + function setBaseRate(uint128 baseRate_) public onlyEVCAccountOwner onlyRole(RATE_ADMIN_ROLE) { + baseRate = baseRate_; + emit BaseRateSet(baseRate_); + } + + /// @notice Set the default premium interest rate. + /// @param premiumRate_ The new premium interest rate. + function setPremiumRate(uint128 premiumRate_) public onlyEVCAccountOwner onlyRole(RATE_ADMIN_ROLE) { + premiumRate = premiumRate_; + emit PremiumRateSet(premiumRate_); + } + + /// @notice Set or clear a premium rate override for a specific vault. + /// @param vault The address of the vault. + /// @param exists Whether the override should exist (true to set, false to clear). + /// @param premiumRate_ The premium rate to use if override is enabled. + function setRateOverride(address vault, bool exists, uint128 premiumRate_) + public + onlyEVCAccountOwner + onlyRole(RATE_ADMIN_ROLE) + { + _rateOverrides[vault] = RateOverride({exists: exists, premiumRate: premiumRate_}); + emit RateOverrideSet(vault, exists, premiumRate_); + } + + /// @inheritdoc IIRM + function computeInterestRate(address vault, uint256 cash, uint256 borrows) + external + view + override + returns (uint256) + { + if (msg.sender != vault) revert E_IRMUpdateUnauthorized(); + + return computeInterestRateInternal(vault, cash, borrows); + } + + /// @inheritdoc IIRM + function computeInterestRateView(address vault, uint256 cash, uint256 borrows) + external + view + override + returns (uint256) + { + return computeInterestRateInternal(vault, cash, borrows); + } + + /// @notice Internal function to compute the interest rate for a vault. + /// @param vault The address of the vault. + /// @return The computed interest rate for the vault. + function computeInterestRateInternal(address vault, uint256, uint256) internal view returns (uint256) { + RateOverride memory rateOverride = _rateOverrides[vault]; + return rateOverride.exists + ? uint256(baseRate) + uint256(rateOverride.premiumRate) + : uint256(baseRate) + uint256(premiumRate); + } + + /// @notice Retrieves the message sender in the context of the EVC. + /// @return msgSender The address of the message sender. + function _msgSender() internal view virtual override (EVCUtil, Context) returns (address msgSender) { + return EVCUtil._msgSender(); + } +} diff --git a/src/OFT/OFTFeeCollector.sol b/src/OFT/OFTFeeCollector.sol new file mode 100644 index 00000000..f4058cba --- /dev/null +++ b/src/OFT/OFTFeeCollector.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {IERC20, SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; +import {IOFT, SendParam} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol"; +import {MessagingFee} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OAppSender.sol"; +import {FeeCollectorUtil} from "../Util/FeeCollectorUtil.sol"; + +/// @title OFTFeeCollector +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice Collects and converts fees from multiple vaults, then sends them cross-chain via a LayerZero OFT adapter. +contract OFTFeeCollector is FeeCollectorUtil { + using SafeERC20 for IERC20; + + /// @notice Role that can execute the fee collection process + bytes32 public constant COLLECTOR_ROLE = keccak256("COLLECTOR_ROLE"); + + /// @notice The LayerZero OFT adapter contract used for cross-chain transfers + address public oftAdapter; + + /// @notice The destination address on the target chain to receive collected fees + address public dstAddress; + + /// @notice The LayerZero endpoint ID of the destination chain + uint32 public dstEid; + + /// @notice Whether to use composed message for cross-chain communication + bool public isComposedMsg; + + /// @notice Error thrown when the OFT adapter token is not the same as the fee token + error InvalidOFTAdapter(); + + /// @notice Initializes the OFTFeeCollector contract + /// @param _admin The address that will be granted the DEFAULT_ADMIN_ROLE + /// @param _feeToken The address of the ERC20 token used for fees + constructor(address _admin, address _feeToken) FeeCollectorUtil(_admin, _feeToken) {} + + /// @notice Configures the OFTFeeCollector contract for cross-chain fee transfers + /// @param _oftAdapter The LayerZero OFT adapter contract address + /// @param _dstAddress The destination address on the target chain to receive fees + /// @param _dstEid The LayerZero endpoint ID of the destination chain + /// @param _isComposedMsg Whether to use composed message for cross-chain communication + function configure(address _oftAdapter, address _dstAddress, uint32 _dstEid, bool _isComposedMsg) + public + onlyRole(DEFAULT_ADMIN_ROLE) + { + if (_oftAdapter != address(0) && address(feeToken) != IOFT(_oftAdapter).token()) { + revert InvalidOFTAdapter(); + } + + oftAdapter = _oftAdapter; + dstAddress = _dstAddress; + dstEid = _dstEid; + isComposedMsg = _isComposedMsg; + } + + /// @notice Collects and converts fees from all vaults, then sends them cross-chain to the configured destination. + function collectFees() external virtual override onlyRole(COLLECTOR_ROLE) { + address adapter = oftAdapter; + if (adapter == address(0)) return; + + _convertAndRedeemFees(); + + IERC20 token = feeToken; + uint256 balance = token.balanceOf(address(this)); + if (balance == 0) return; + + SendParam memory sendParam = SendParam({ + dstEid: dstEid, + to: bytes32(uint256(uint160(dstAddress))), + amountLD: balance, + minAmountLD: balance, + extraOptions: "", + composeMsg: isComposedMsg ? abi.encode(0x01) : bytes(""), + oftCmd: "" + }); + MessagingFee memory fee = IOFT(adapter).quoteSend(sendParam, false); + + token.forceApprove(adapter, balance); + IOFT(adapter).send{value: fee.nativeFee}(sendParam, fee, address(this)); + } +} diff --git a/src/OFT/OFTGulper.sol b/src/OFT/OFTGulper.sol new file mode 100644 index 00000000..8eb081df --- /dev/null +++ b/src/OFT/OFTGulper.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; +import {IERC20, SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; +import {EulerSavingsRate} from "evk/Synths/EulerSavingsRate.sol"; +import {ILayerZeroComposer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol"; + +/// @title OFTGulper +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice A contract that receives fee tokens, deposits them into an EulerSavingsRate vault, and triggers interest +/// smearing via gulp. +contract OFTGulper is Ownable, ILayerZeroComposer { + using SafeERC20 for IERC20; + + /// @notice The EulerSavingsRate vault to which fees are deposited and gulped. + EulerSavingsRate public immutable esr; + + /// @notice The ERC20 token used for fees. + IERC20 public immutable feeToken; + + /// @notice Initializes the OFTGulper contract. + /// @param owner_ The address that will be granted ownership of the contract. + /// @param esr_ The address of the EulerSavingsRate vault. + constructor(address owner_, address esr_) Ownable(owner_) { + esr = EulerSavingsRate(esr_); + feeToken = IERC20(esr.asset()); + } + + /// @notice Allows the contract owner to recover any ERC20 tokens sent to this contract. + /// @param token The address of the token to recover. + /// @param to The address to send the recovered tokens to. + /// @param amount The amount of tokens to recover. + function recoverToken(address token, address to, uint256 amount) external onlyOwner { + IERC20(token).safeTransfer(to, amount); + } + + /// @notice Handles incoming composed messages from LayerZero and deposits any fee tokens held by this contract into + /// the ESR vault, then calls gulp. + /// @dev This function can be called at any time hence by anyone hence no need for additional checks or + /// authentication. + function lzCompose(address, bytes32, bytes calldata, address, bytes calldata) external payable override { + uint256 balance = feeToken.balanceOf(address(this)); + if (balance == 0) return; + + feeToken.safeTransfer(address(esr), balance); + esr.gulp(); + } +} diff --git a/src/Util/FeeCollectorGulper.sol b/src/Util/FeeCollectorGulper.sol new file mode 100644 index 00000000..2934273f --- /dev/null +++ b/src/Util/FeeCollectorGulper.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {IERC20, SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; +import {EulerSavingsRate} from "evk/Synths/EulerSavingsRate.sol"; +import {FeeCollectorUtil} from "./FeeCollectorUtil.sol"; + +/// @title FeeCollectorGulper +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice Collects and converts fees from multiple vaults, then deposits them into an EulerSavingsRate contract and +/// calls gulp. +contract FeeCollectorGulper is FeeCollectorUtil { + using SafeERC20 for IERC20; + + /// @notice The EulerSavingsRate contract where collected fees are deposited and gulped. + EulerSavingsRate public immutable esr; + + /// @notice Initializes the FeeCollectorGulper contract. + /// @param _admin The address to be granted the DEFAULT_ADMIN_ROLE. + /// @param _feeToken The address of the ERC20 token used for fees. + /// @param _esr The address of the EulerSavingsRate contract to receive collected fees. + constructor(address _admin, address _feeToken, address _esr) FeeCollectorUtil(_admin, _feeToken) { + esr = EulerSavingsRate(_esr); + } + + /// @notice Collects and converts fees from all vaults in the list, then deposits them into the EulerSavingsRate + /// contract and calls gulp. + function collectFees() external virtual override { + _convertAndRedeemFees(); + + uint256 balance = feeToken.balanceOf(address(this)); + if (balance == 0) return; + + feeToken.safeTransfer(address(esr), balance); + esr.gulp(); + } +} diff --git a/src/Util/FeeCollectorUtil.sol b/src/Util/FeeCollectorUtil.sol new file mode 100644 index 00000000..0b5b6499 --- /dev/null +++ b/src/Util/FeeCollectorUtil.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {AccessControlEnumerable} from "openzeppelin-contracts/access/extensions/AccessControlEnumerable.sol"; +import {IERC20, SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; +import {IEVault} from "evk/EVault/IEVault.sol"; + +/// @title FeeCollectorUtil +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice A contract that collects and converts fees from multiple vaults. +contract FeeCollectorUtil is AccessControlEnumerable { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + /// @notice Role that can add and remove vaults from the list + bytes32 public constant MAINTAINER_ROLE = keccak256("MAINTAINER_ROLE"); + + /// @notice The ERC20 token used for fees + IERC20 public feeToken; + + /// @notice Internal set of vault addresses from which fees are collected + EnumerableSet.AddressSet internal _vaultsList; + + /// @notice Emitted when a vault is added to the list + event VaultAdded(address indexed vault); + + /// @notice Emitted when a vault is removed from the list + event VaultRemoved(address indexed vault); + + /// @notice Error thrown when a vault asset is not the same as the fee token + error InvalidVault(); + + /// @notice Initializes the FeeCollectorUtil contract + /// @param _admin The address that will be granted the DEFAULT_ADMIN_ROLE + /// @param _feeToken The address of the ERC20 token used for fees + constructor(address _admin, address _feeToken) { + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + feeToken = IERC20(_feeToken); + } + + /// @notice Allows recovery of any ERC20 tokens or native currency sent to this contract + /// @param token The address of the token to recover. If address(0), the native currency is recovered. + /// @param to The address to send the tokens to + /// @param amount The amount of tokens to recover + function recoverToken(address token, address to, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (token == address(0)) { + (bool success,) = to.call{value: amount}(""); + require(success, "Native currency recovery failed"); + } else { + IERC20(token).safeTransfer(to, amount); + } + } + + /// @notice Adds a vault to the list + /// @param vault The address of the vault to add + /// @return success True if the vault was successfully added, false if it was already in the list + function addToVaultsList(address vault) external onlyRole(MAINTAINER_ROLE) returns (bool) { + if (IEVault(vault).asset() != address(feeToken)) revert InvalidVault(); + + bool success = _vaultsList.add(vault); + if (success) emit VaultAdded(vault); + return success; + } + + /// @notice Removes a vault from the list + /// @param vault The address of the vault to remove + /// @return success True if the vault was successfully removed, false if it was not in the list + function removeFromVaultsList(address vault) external onlyRole(MAINTAINER_ROLE) returns (bool) { + bool success = _vaultsList.remove(vault); + if (success) emit VaultRemoved(vault); + return success; + } + + /// @notice Collects and converts fees from all vaults in the list + function collectFees() external virtual { + _convertAndRedeemFees(); + } + + /// @notice Checks if a vault is in the list + /// @param vault The address of the vault to check + /// @return True if the vault is in the list, false otherwise + function isInVaultsList(address vault) external view returns (bool) { + return _vaultsList.contains(vault); + } + + /// @notice Returns the complete list of vault addresses + /// @return An array containing all vault addresses in the list + function getVaultsList() external view returns (address[] memory) { + return _vaultsList.values(); + } + + /// @dev Internal function to convert and redeem fees from all vaults in the list + function _convertAndRedeemFees() internal { + uint256 length = _vaultsList.length(); + for (uint256 i = 0; i < length; ++i) { + address vault = _vaultsList.at(i); + try IEVault(vault).convertFees() {} catch {} + try IEVault(vault).redeem(type(uint256).max, address(this), address(this)) {} catch {} + } + } +}