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": {
"contracts/EagleOVault.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import { IStrategy } from "./interfaces/IStrategy.sol";
interface AggregatorV3Interface {
function decimals() external view returns (uint8);
function latestRoundData() external view returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
}
/**
* @title EagleOVault
* @notice LayerZero OVault-compatible ERC-4626 vault with synchronous redemptions
*
* @dev LAYERZERO OVAULT COMPATIBLE - Synchronous Operations
* - totalAssets() returns WLFI units (strict ERC-4626)
* - deposit/mint/withdraw/redeem are SYNCHRONOUS (immediate transfers)
* - Compatible with VaultComposerSync for omnichain deposits/redemptions
* - USD1 converted to WLFI-equivalent for accounting
* - depositDual swaps USD1→WLFI before minting shares
* - No totalSupply() override; locked shares tracked separately
*
*/
contract EagleOVault is ERC4626, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
// =================================
// CONSTANTS
// =================================
/// @notice Maximum performance fee (50%)
uint16 public constant MAX_FEE = 5_000;
/// @notice Basis points denominator
uint256 internal constant MAX_BPS = 10_000;
/// @notice Extended precision for profit unlocking rate
uint256 internal constant MAX_BPS_EXTENDED = 1_000_000_000_000;
/// @notice Seconds per year
uint256 internal constant SECONDS_PER_YEAR = 31_556_952;
// =================================
// STATE VARIABLES
// =================================
/// @notice Token contracts
IERC20 public immutable USD1_TOKEN;
IERC20 public immutable WLFI_TOKEN;
/// @notice Oracle contracts
AggregatorV3Interface public immutable USD1_PRICE_FEED;
IUniswapV3Pool public immutable WLFI_USD1_POOL;
ISwapRouter public immutable UNISWAP_ROUTER;
/// @notice Current token balances held directly by vault
uint256 public wlfiBalance;
uint256 public usd1Balance;
/// @notice Strategy management
mapping(address => bool) public activeStrategies;
mapping(address => uint256) public strategyWeights;
address[] public strategyList;
uint256 public totalStrategyWeight;
uint256 public constant MAX_STRATEGIES = 5;
// =================================
// ADVANCED FEATURES
// =================================
/// @notice Access control roles
address public management;
address public pendingManagement;
address public keeper; // Can call report() and tend()
address public emergencyAdmin; // Can shutdown
/// @notice Performance fees
uint16 public performanceFee; // In basis points
address public performanceFeeRecipient;
/// @notice Profit unlocking (prevents PPS manipulation)
/// @dev Locked shares are tracked separately, not excluded from totalSupply()
uint256 public profitUnlockingRate; // Shares to unlock per second
uint96 public fullProfitUnlockDate; // When all profits unlocked
uint32 public profitMaxUnlockTime; // Max time to unlock
uint256 public totalLockedShares; // Shares locked from last report (held by vault)
/// @notice Reporting
uint96 public lastReport;
uint256 public totalAssetsAtLastReport; // In WLFI units
/// @notice Shutdown flag
bool public isShutdown;
// =================================
// LEGACY STATE
// =================================
uint256 public maxTotalSupply = 50_000_000e18;
uint32 public twapInterval = 1800;
uint256 public maxPriceAge = 86400;
uint256 public deploymentThreshold = 1000e18; // Keep 1000 WLFI idle for redemptions (Recommendation #4)
uint256 public minDeploymentInterval = 5 minutes;
uint256 public lastDeployment;
bool public paused;
mapping(address => bool) public authorized;
uint256 public lastRebalance;
/// @notice Slippage tolerance for USD1→WLFI swaps (in basis points)
uint256 public swapSlippageBps = 50; // 0.5% default
// =================================
// EVENTS
// =================================
// Standard ERC4626 events (Deposit/Withdraw) emitted via OZ base
/// @notice Dual deposit event (now swaps USD1 first)
event DualDeposit(
address indexed user,
uint256 wlfiAmount,
uint256 usd1Amount,
uint256 usd1SwappedToWlfi,
uint256 totalWlfiDeposited,
uint256 shares
);
event USD1Swapped(
uint256 usd1In,
uint256 wlfiOut,
uint256 wlfiExpected,
uint256 minWlfiOut,
uint256 slippageBps
);
event Reported(
uint256 profit,
uint256 loss,
uint256 performanceFees,
uint256 totalAssets
);
event UpdateKeeper(address indexed newKeeper);
event UpdateEmergencyAdmin(address indexed newEmergencyAdmin);
event UpdatePerformanceFee(uint16 newPerformanceFee);
event UpdatePerformanceFeeRecipient(address indexed newRecipient);
event UpdateProfitMaxUnlockTime(uint256 newProfitMaxUnlockTime);
event UpdateManagement(address indexed newManagement);
event UpdatePendingManagement(address indexed newPendingManagement);
event StrategyShutdown();
event BalancesSynced(uint256 wlfiBalance, uint256 usd1Balance);
event StrategyAdded(address indexed strategy, uint256 weight);
event StrategyRemoved(address indexed strategy);
event StrategyDeployed(address indexed strategy, uint256 wlfiDeployed, uint256 usd1Deployed);
event Rebalanced(uint256 newWlfiBalance, uint256 newUsd1Balance);
event EmergencyPause(bool paused);
event CapitalInjected(address indexed from, uint256 wlfiAmount, uint256 usd1Amount);
event EmergencyWithdraw(address indexed to, uint256 wlfiAmount, uint256 usd1Amount);
event SwapSlippageUpdated(uint256 newSlippageBps);
// =================================
// ERRORS
// =================================
error ZeroAddress();
error Unauthorized();
error Paused();
error InvalidAmount();
error InsufficientBalance();
error SlippageExceeded();
error StrategyAlreadyActive();
error StrategyNotActive();
error MaxStrategiesReached();
error InvalidWeight();
error InvalidPrice();
error StalePrice();
error VaultIsShutdown();
error VaultNotShutdown();
// =================================
// CONSTRUCTOR
// =================================
constructor(
address _wlfiToken,
address _usd1Token,
address _usd1PriceFeed,
address _wlfiUsd1Pool,
address _uniswapRouter,
address _owner
)
ERC20("Eagle Vault Shares", "vEAGLE")
ERC4626(IERC20(_wlfiToken))
Ownable(_owner)
{
if (_wlfiToken == address(0) || _usd1Token == address(0) ||
_usd1PriceFeed == address(0) || _wlfiUsd1Pool == address(0) ||
_uniswapRouter == address(0)) {
revert ZeroAddress();
}
WLFI_TOKEN = IERC20(_wlfiToken);
USD1_TOKEN = IERC20(_usd1Token);
USD1_PRICE_FEED = AggregatorV3Interface(_usd1PriceFeed);
WLFI_USD1_POOL = IUniswapV3Pool(_wlfiUsd1Pool);
UNISWAP_ROUTER = ISwapRouter(_uniswapRouter);
// Initialize roles
management = _owner;
keeper = _owner;
emergencyAdmin = _owner;
performanceFeeRecipient = _owner;
performanceFee = 1000; // 10% default
profitMaxUnlockTime = 7 days;
authorized[_owner] = true;
lastDeployment = block.timestamp;
lastRebalance = block.timestamp;
lastReport = uint96(block.timestamp);
}
receive() external payable {}
// =================================
// MODIFIERS
// =================================
modifier onlyManagement() {
if (msg.sender != management && msg.sender != owner()) revert Unauthorized();
_;
}
modifier onlyKeepers() {
if (msg.sender != keeper && msg.sender != management && msg.sender != owner()) {
revert Unauthorized();
}
_;
}
modifier onlyEmergencyAuthorized() {
if (msg.sender != emergencyAdmin && msg.sender != management && msg.sender != owner()) {
revert Unauthorized();
}
_;
}
modifier whenNotPaused() {
if (paused) revert Paused();
_;
}
modifier whenNotShutdown() {
if (isShutdown) revert VaultIsShutdown();
_;
}
// =================================
// PRICE ORACLE FUNCTIONS (WLFI-centric)
// =================================
/**
* @notice Get USD1 price in USD (1e18 = $1)
*/
function getUSD1Price() public view returns (uint256 price) {
(
uint80 roundId,
int256 answer,
,
uint256 updatedAt,
uint80 answeredInRound
) = USD1_PRICE_FEED.latestRoundData();
if (answer <= 0) revert InvalidPrice();
if (updatedAt == 0) revert StalePrice();
if (answeredInRound < roundId) revert StalePrice();
if (block.timestamp - updatedAt > maxPriceAge) revert StalePrice();
uint8 decimals = USD1_PRICE_FEED.decimals();
price = uint256(answer) * (10 ** (18 - decimals));
// Sanity check: USD1 should be close to $1
if (price < 0.95e18 || price > 1.05e18) revert InvalidPrice();
}
/**
* @notice Get WLFI price in USD (1e18 = $1)
*/
function getWLFIPrice() public view returns (uint256 price) {
uint256 usd1InUSD = getUSD1Price();
uint256 wlfiInUsd1;
if (twapInterval > 0) {
try this._getTWAPPrice() returns (uint256 twapPrice) {
wlfiInUsd1 = twapPrice;
} catch {
wlfiInUsd1 = _getSpotPrice();
}
} else {
wlfiInUsd1 = _getSpotPrice();
}
// WLFI price in USD = (WLFI in USD1) * (USD1 in USD)
price = (wlfiInUsd1 * usd1InUSD) / 1e18;
}
function _getTWAPPrice() external view returns (uint256) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = twapInterval;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) = WLFI_USD1_POOL.observe(secondsAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(twapInterval)));
// Simple tick to price conversion (adjust based on pool token order)
if (arithmeticMeanTick > -1000 && arithmeticMeanTick < 1000) {
uint256 basePrice = 1e18;
int256 adjustment = int256(arithmeticMeanTick) * 1e14;
uint256 rawPrice = uint256(int256(basePrice) + adjustment);
return rawPrice > 0 ? (1e18 * 1e18) / rawPrice : 1e18;
} else {
return _getSpotPrice();
}
}
function _getSpotPrice() internal view returns (uint256 price) {
(uint160 sqrtPriceX96,,,,,,) = WLFI_USD1_POOL.slot0();
uint256 numerator = uint256(sqrtPriceX96) * uint256(sqrtPriceX96);
uint256 denominator = 1 << 192;
uint256 rawPrice = (numerator * 1e18) / denominator;
// Invert if needed (depends on pool token order)
price = rawPrice > 0 ? (1e18 * 1e18) / rawPrice : 1e15;
if (price == 0) price = 1e15;
}
/**
* @notice Get WLFI per 1 USD1 (1e18 scale)
* @dev Helper for converting USD1 amounts to WLFI-equivalent
*/
function wlfiPerUsd1() public view returns (uint256) {
uint256 wlfiPriceUSD = getWLFIPrice(); // 1e18 = $1
uint256 usd1PriceUSD = getUSD1Price(); // ~1e18 = $1
// WLFI per USD1 = (USD1 price) / (WLFI price)
return (usd1PriceUSD * 1e18) / wlfiPriceUSD;
}
/**
* @notice Convert USD1 amount to WLFI-equivalent
* @param usd1Amount Amount of USD1
* @return WLFI-equivalent amount (1e18 decimals)
*/
function wlfiEquivalent(uint256 usd1Amount) public view returns (uint256) {
if (usd1Amount == 0) return 0;
return (usd1Amount * wlfiPerUsd1()) / 1e18;
}
/**
* @notice Get price difference between oracle and pool price
* @dev Useful for monitoring oracle/pool price mismatch (Issue #3 from WLFI_DENOMINATION_IMPACT.md)
* @return deltaBps Price difference in basis points (positive = oracle higher than pool)
*/
function getOraclePoolPriceDelta() public view returns (int256 deltaBps) {
uint256 oraclePrice = wlfiPerUsd1();
uint256 poolPrice = _getSpotPrice();
if (oraclePrice == 0) return 0;
int256 diff = int256(oraclePrice) - int256(poolPrice);
deltaBps = (diff * 10000) / int256(oraclePrice);
}
// =================================
// PROFIT UNLOCKING
// =================================
/**
* @notice Calculate unlocked shares since last report
* @dev Prevents PPS manipulation by gradual unlock
*/
function unlockedShares() public view returns (uint256) {
if (fullProfitUnlockDate <= block.timestamp || fullProfitUnlockDate == 0) {
return totalLockedShares;
}
uint256 timeSinceLastReport = block.timestamp - lastReport;
uint256 unlockedAmount = (profitUnlockingRate * timeSinceLastReport) / MAX_BPS_EXTENDED;
return unlockedAmount > totalLockedShares ? totalLockedShares : unlockedAmount;
}
/**
* @notice Get locked (not yet unlocked) shares
* @dev These shares are held by the vault and don't affect ERC20 totalSupply()
*/
function lockedShares() public view returns (uint256) {
return totalLockedShares - unlockedShares();
}
// =================================
// ERC4626 CORE OVERRIDES (WLFI-denominated, SYNCHRONOUS)
// =================================
/**
* @notice Total assets controlled by vault in WLFI units
* @dev Returns WLFI units, not USD value (ERC-4626 compliant)
* USD1 holdings are converted to WLFI-equivalent
*/
function totalAssets() public view override returns (uint256) {
// Direct WLFI holdings
uint256 wlfi = wlfiBalance;
// USD1 converted to WLFI-equivalent
uint256 usd1InWlfi = wlfiEquivalent(usd1Balance);
// Strategy holdings (WLFI + USD1-equivalent)
uint256 len = strategyList.length;
for (uint256 i; i < len; i++) {
if (activeStrategies[strategyList[i]]) {
(uint256 sWlfi, uint256 sUsd1) = IStrategy(strategyList[i]).getTotalAmounts();
wlfi += sWlfi;
usd1InWlfi += wlfiEquivalent(sUsd1);
}
}
return wlfi + usd1InWlfi;
}
/**
* @notice Standard ERC4626 deposit (WLFI only)
* @dev LayerZero OVault compatible - synchronous operation
*/
function deposit(uint256 assets, address receiver)
public
override
nonReentrant
whenNotPaused
whenNotShutdown
returns (uint256 shares)
{
if (assets == 0) revert InvalidAmount();
if (receiver == address(0)) revert ZeroAddress();
// Standard ERC4626: preview shares first
shares = previewDeposit(assets);
if (shares == 0) revert InvalidAmount();
if (totalSupply() + shares > maxTotalSupply) revert InvalidAmount();
// Pull WLFI
WLFI_TOKEN.safeTransferFrom(msg.sender, address(this), assets);
wlfiBalance += assets;
// Mint shares
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
/**
* @notice Standard ERC4626 mint (WLFI only)
*/
function mint(uint256 shares, address receiver)
public
override
nonReentrant
whenNotPaused
whenNotShutdown
returns (uint256 assets)
{
if (shares == 0) revert InvalidAmount();
if (receiver == address(0)) revert ZeroAddress();
// Standard ERC4626: preview assets first
assets = previewMint(shares);
if (assets == 0) revert InvalidAmount();
if (totalSupply() + shares > maxTotalSupply) revert InvalidAmount();
// Pull WLFI
WLFI_TOKEN.safeTransferFrom(msg.sender, address(this), assets);
wlfiBalance += assets;
// Mint shares
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
/**
* @notice Standard ERC4626 redeem - SYNCHRONOUS (LayerZero OVault compatible)
* @dev Transfers WLFI immediately in same transaction
* @return assets WLFI assets transferred immediately
*/
function redeem(uint256 shares, address receiver, address owner)
public
override
nonReentrant
returns (uint256 assets)
{
if (shares == 0) revert InvalidAmount();
if (receiver == address(0)) revert ZeroAddress();
// Standard ERC4626 approval check
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
// Calculate assets
assets = previewRedeem(shares);
if (assets == 0) revert InvalidAmount();
// Burn shares
_burn(owner, shares);
// Ensure we have enough WLFI (pull from strategies if needed)
_ensureWlfi(assets);
// Transfer WLFI immediately (SYNCHRONOUS - critical for OVault)
wlfiBalance -= assets;
WLFI_TOKEN.safeTransfer(receiver, assets);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
/**
* @notice Standard ERC4626 withdraw - SYNCHRONOUS (LayerZero OVault compatible)
* @dev Transfers WLFI immediately in same transaction
* @return shares Shares burned
*/
function withdraw(uint256 assets, address receiver, address owner)
public
override
nonReentrant
returns (uint256 shares)
{
if (assets == 0) revert InvalidAmount();
if (receiver == address(0)) revert ZeroAddress();
// Calculate shares needed (round up)
shares = previewWithdraw(assets);
if (shares == 0) revert InvalidAmount();
// Standard ERC4626 approval check
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
// Burn shares
_burn(owner, shares);
// Ensure we have enough WLFI (pull from strategies if needed)
_ensureWlfi(assets);
// Transfer WLFI immediately (SYNCHRONOUS - critical for OVault)
wlfiBalance -= assets;
WLFI_TOKEN.safeTransfer(receiver, assets);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
/**
* @notice Preview deposit (standard ERC4626)
*/
function previewDeposit(uint256 assets) public view override returns (uint256 shares) {
uint256 supply = totalSupply();
if (supply == 0) {
// Bootstrap: 1 WLFI = 1 share
shares = assets;
} else {
shares = (assets * supply) / totalAssets();
}
}
/**
* @notice Preview mint (standard ERC4626)
*/
function previewMint(uint256 shares) public view override returns (uint256 assets) {
uint256 supply = totalSupply();
if (supply == 0) {
assets = shares;
} else {
// Round up for mint (user pays ceiling)
assets = (shares * totalAssets() + supply - 1) / supply;
}
}
/**
* @notice Preview redeem (standard ERC4626)
*/
function previewRedeem(uint256 shares) public view override returns (uint256 assets) {
uint256 supply = totalSupply();
if (supply == 0) return 0;
assets = (shares * totalAssets()) / supply;
}
/**
* @notice Preview withdraw (standard ERC4626)
*/
function previewWithdraw(uint256 assets) public view override returns (uint256 shares) {
uint256 supply = totalSupply();
if (supply == 0) return 0;
// Round up for withdraw (user burns ceiling)
shares = (assets * supply + totalAssets() - 1) / totalAssets();
}
/**
* @notice Max deposit (standard ERC4626)
*/
function maxDeposit(address) public view override returns (uint256) {
if (paused || isShutdown) return 0;
uint256 currentSupply = totalSupply();
if (currentSupply >= maxTotalSupply) return 0;
uint256 remainingShares = maxTotalSupply - currentSupply;
uint256 supply = totalSupply();
if (supply == 0) return remainingShares;
return (remainingShares * totalAssets()) / supply;
}
/**
* @notice Max mint (standard ERC4626)
*/
function maxMint(address) public view override returns (uint256) {
if (paused || isShutdown) return 0;
uint256 currentSupply = totalSupply();
if (currentSupply >= maxTotalSupply) return 0;
return maxTotalSupply - currentSupply;
}
/**
* @notice Max withdraw (standard ERC4626)
*/
function maxWithdraw(address owner) public view override returns (uint256) {
if (paused) return 0;
uint256 userShares = balanceOf(owner);
if (userShares == 0) return 0;
return previewRedeem(userShares);
}
/**
* @notice Max redeem (standard ERC4626)
*/
function maxRedeem(address owner) public view override returns (uint256) {
if (paused) return 0;
return balanceOf(owner);
}
// =================================
// ENSURE WLFI HELPER (For Synchronous Redemptions)
// =================================
/**
* @notice Ensure vault has enough WLFI for redemptions
* @dev Internal function to source WLFI from strategies and swap USD1
* @param wlfiNeeded Amount of WLFI needed
*/
function _ensureWlfi(uint256 wlfiNeeded) internal {
// Check if we already have enough
if (wlfiBalance >= wlfiNeeded) return;
uint256 deficit = wlfiNeeded - wlfiBalance;
// Step 1: Withdraw WLFI from strategies
uint256 wlfiFromStrategies = _withdrawWlfiFromStrategies(deficit);
deficit = deficit > wlfiFromStrategies ? deficit - wlfiFromStrategies : 0;
// Step 2: If still short, swap USD1 → WLFI
if (deficit > 0 && usd1Balance > 0) {
// Calculate how much USD1 we need to swap
uint256 usd1Needed = (deficit * 1e18) / wlfiPerUsd1();
// Cap at available USD1
if (usd1Needed > usd1Balance) {
usd1Needed = usd1Balance;
}
if (usd1Needed > 0) {
_swapUSD1ForWLFI(usd1Needed);
}
}
// Final check
if (wlfiBalance < wlfiNeeded) {
revert InsufficientBalance();
}
}
/**
* @notice Swap USD1 for WLFI using Uniswap V3
* @param usd1Amount Amount of USD1 to swap
* @return wlfiOut Amount of WLFI received
*/
function _swapUSD1ForWLFI(uint256 usd1Amount)
internal
returns (uint256 wlfiOut)
{
if (usd1Amount == 0) return 0;
if (usd1Amount > usd1Balance) revert InsufficientBalance();
// Calculate minimum WLFI output based on oracle price and slippage
uint256 expectedWlfi = wlfiEquivalent(usd1Amount);
uint256 minWlfiOut = (expectedWlfi * (MAX_BPS - swapSlippageBps)) / MAX_BPS;
// Approve router
USD1_TOKEN.forceApprove(address(UNISWAP_ROUTER), usd1Amount);
// Prepare swap params
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: address(USD1_TOKEN),
tokenOut: address(WLFI_TOKEN),
fee: 3000, // 0.3% pool
recipient: address(this),
deadline: block.timestamp,
amountIn: usd1Amount,
amountOutMinimum: minWlfiOut,
sqrtPriceLimitX96: 0
});
// Execute swap
wlfiOut = UNISWAP_ROUTER.exactInputSingle(params);
// Update balances
usd1Balance -= usd1Amount;
wlfiBalance += wlfiOut;
// Calculate actual slippage
uint256 actualSlippage = expectedWlfi > wlfiOut
? ((expectedWlfi - wlfiOut) * MAX_BPS) / expectedWlfi
: 0;
emit USD1Swapped(usd1Amount, wlfiOut, expectedWlfi, minWlfiOut, actualSlippage);
}
// =================================
// DUAL DEPOSIT (Non-standard helper)
// =================================
/**
* @notice Dual-token deposit (WLFI + USD1)
* @dev Swaps USD1 → WLFI first, then calls standard deposit
* @param wlfiAmount Amount of WLFI to deposit
* @param usd1Amount Amount of USD1 to deposit (will be swapped)
* @param receiver Address to receive shares
*/
function depositDual(
uint256 wlfiAmount,
uint256 usd1Amount,
address receiver
) external nonReentrant whenNotPaused whenNotShutdown returns (uint256 shares) {
if (wlfiAmount == 0 && usd1Amount == 0) revert InvalidAmount();
if (receiver == address(0)) revert ZeroAddress();
uint256 totalWlfiToDeposit = wlfiAmount;
uint256 usd1SwappedToWlfi = 0;
// Pull WLFI if provided
if (wlfiAmount > 0) {
WLFI_TOKEN.safeTransferFrom(msg.sender, address(this), wlfiAmount);
wlfiBalance += wlfiAmount;
}
// Pull USD1 and swap to WLFI
if (usd1Amount > 0) {
USD1_TOKEN.safeTransferFrom(msg.sender, address(this), usd1Amount);
usd1Balance += usd1Amount;
// Swap USD1 → WLFI
usd1SwappedToWlfi = _swapUSD1ForWLFI(usd1Amount);
totalWlfiToDeposit += usd1SwappedToWlfi;
}
// Calculate shares based on total WLFI
shares = previewDeposit(totalWlfiToDeposit);
if (shares == 0) revert InvalidAmount();
if (totalSupply() + shares > maxTotalSupply) revert InvalidAmount();
// Mint shares
_mint(receiver, shares);
emit DualDeposit(
msg.sender,
wlfiAmount,
usd1Amount,
usd1SwappedToWlfi,
totalWlfiToDeposit,
shares
);
// Also emit standard Deposit event for ERC4626 compliance
emit Deposit(msg.sender, receiver, totalWlfiToDeposit, shares);
}
// =================================
// REPORT FUNCTION (WLFI-denominated)
// =================================
/**
* @notice Report profit/loss and charge fees
* @dev Now works in WLFI units
*/
function report() external nonReentrant onlyKeepers returns (uint256 profit, uint256 loss) {
uint256 currentTotalAssets = totalAssets(); // In WLFI units
uint256 previousTotalAssets = totalAssetsAtLastReport; // In WLFI units
if (currentTotalAssets > previousTotalAssets) {
profit = currentTotalAssets - previousTotalAssets;
// Charge performance fee
uint256 performanceFees = 0;
if (performanceFee > 0 && profit > 0) {
performanceFees = (profit * performanceFee) / MAX_BPS;
if (performanceFees > 0 && performanceFeeRecipient != address(0)) {
// Mint fee shares
uint256 supply = totalSupply();
uint256 feeShares = supply > 0 ? (performanceFees * supply) / currentTotalAssets : performanceFees;
_mint(performanceFeeRecipient, feeShares);
}
}
// Lock remaining profit (mint to vault)
uint256 profitAfterFees = profit - performanceFees;
if (profitAfterFees > 0 && profitMaxUnlockTime > 0) {
uint256 supply = totalSupply();
uint256 profitShares = supply > 0 ? (profitAfterFees * supply) / currentTotalAssets : profitAfterFees;
// Mint locked shares to vault
_mint(address(this), profitShares);
totalLockedShares += profitShares;
fullProfitUnlockDate = uint96(block.timestamp + profitMaxUnlockTime);
profitUnlockingRate = (profitShares * MAX_BPS_EXTENDED) / profitMaxUnlockTime;
}
emit Reported(profit, 0, performanceFees, currentTotalAssets);
} else {
loss = previousTotalAssets - currentTotalAssets;
// Offset loss with locked shares
if (loss > 0 && totalLockedShares > 0) {
uint256 supply = totalSupply();
uint256 lossShares = supply > 0 ? (loss * supply) / currentTotalAssets : 0;
uint256 sharesToBurn = lossShares > totalLockedShares ? totalLockedShares : lossShares;
if (sharesToBurn > 0) {
_burn(address(this), sharesToBurn);
totalLockedShares -= sharesToBurn;
}
}
emit Reported(0, loss, 0, currentTotalAssets);
}
lastReport = uint96(block.timestamp);
totalAssetsAtLastReport = currentTotalAssets;
}
// =================================
// TEND FUNCTION
// =================================
/**
* @notice Perform maintenance without full report
*/
function tend() external nonReentrant onlyKeepers {
uint256 idleWlfi = wlfiBalance;
uint256 idleUsd1 = usd1Balance;
if (idleWlfi > 0 || idleUsd1 > 0) {
_deployToStrategies(idleWlfi, idleUsd1);
}
}
function tendTrigger() external view returns (bool) {
// Check if idle balance exceeds threshold (in WLFI-equivalent)
uint256 idleWlfi = wlfiBalance + wlfiEquivalent(usd1Balance);
return idleWlfi > deploymentThreshold && totalStrategyWeight > 0;
}
// =================================
// STRATEGY MANAGEMENT
// =================================
function addStrategy(address strategy, uint256 weight) external onlyManagement {
if (strategy == address(0)) revert ZeroAddress();
if (activeStrategies[strategy]) revert StrategyAlreadyActive();
if (strategyList.length >= MAX_STRATEGIES) revert MaxStrategiesReached();
if (weight == 0 || weight > 10000) revert InvalidWeight();
if (totalStrategyWeight + weight > 10000) revert InvalidWeight();
require(IStrategy(strategy).isInitialized(), "Strategy not initialized");
activeStrategies[strategy] = true;
strategyWeights[strategy] = weight;
strategyList.push(strategy);
totalStrategyWeight += weight;
emit StrategyAdded(strategy, weight);
}
function removeStrategy(address strategy) external onlyManagement {
if (!activeStrategies[strategy]) revert StrategyNotActive();
// Withdraw all funds from strategy
(uint256 wlfi, uint256 usd1) = IStrategy(strategy).withdraw(type(uint256).max);
wlfiBalance += wlfi;
usd1Balance += usd1;
activeStrategies[strategy] = false;
totalStrategyWeight -= strategyWeights[strategy];
strategyWeights[strategy] = 0;
// Remove from list
uint256 length = strategyList.length;
for (uint256 i = 0; i < length; i++) {
if (strategyList[i] == strategy) {
strategyList[i] = strategyList[length - 1];
strategyList.pop();
break;
}
}
emit StrategyRemoved(strategy);
}
/**
* @notice Deploy idle assets to strategies
*/
function _deployToStrategies(uint256 wlfiAmount, uint256 usd1Amount) internal {
if (totalStrategyWeight == 0) return;
// Calculate total value to deploy (in WLFI-equivalent)
uint256 totalValueWlfi = wlfiAmount + wlfiEquivalent(usd1Amount);
if (totalValueWlfi == 0) return;
uint256 length = strategyList.length;
for (uint256 i = 0; i < length; i++) {
address strategy = strategyList[i];
if (activeStrategies[strategy] && strategyWeights[strategy] > 0) {
// Calculate strategy allocation
uint256 strategyValueWlfi = (totalValueWlfi * strategyWeights[strategy]) / totalStrategyWeight;
// Proportionally split between WLFI and USD1
uint256 strategyWlfi = wlfiAmount > 0 ? (wlfiAmount * strategyValueWlfi) / totalValueWlfi : 0;
uint256 strategyUsd1 = usd1Amount > 0 ? (usd1Amount * strategyValueWlfi) / totalValueWlfi : 0;
// Cap at available balances
if (strategyWlfi > wlfiBalance) strategyWlfi = wlfiBalance;
if (strategyUsd1 > usd1Balance) strategyUsd1 = usd1Balance;
if (strategyWlfi > 0 || strategyUsd1 > 0) {
// Update balances first
if (strategyWlfi > 0) {
wlfiBalance -= strategyWlfi;
}
if (strategyUsd1 > 0) {
usd1Balance -= strategyUsd1;
}
// Approve strategy to pull tokens
if (strategyWlfi > 0) {
WLFI_TOKEN.forceApprove(strategy, strategyWlfi);
}
if (strategyUsd1 > 0) {
USD1_TOKEN.forceApprove(strategy, strategyUsd1);
}
// Call strategy deposit
IStrategy(strategy).deposit(strategyWlfi, strategyUsd1);
emit StrategyDeployed(strategy, strategyWlfi, strategyUsd1);
}
}
}
}
/**
* @notice Withdraw WLFI from strategies
*/
function _withdrawWlfiFromStrategies(uint256 wlfiNeeded)
internal
returns (uint256 wlfiTotal)
{
uint256 remaining = wlfiNeeded;
uint256 length = strategyList.length;
for (uint256 i = 0; i < length && remaining > 0; i++) {
address strategy = strategyList[i];
if (activeStrategies[strategy]) {
(uint256 stratWlfi, uint256 stratUsd1) = IStrategy(strategy).getTotalAmounts();
if (stratWlfi > 0 || stratUsd1 > 0) {
// Calculate how much to withdraw
uint256 stratValueWlfi = stratWlfi + wlfiEquivalent(stratUsd1);
uint256 withdrawValueWlfi = (remaining * strategyWeights[strategy]) / totalStrategyWeight;
if (withdrawValueWlfi > stratValueWlfi) {
withdrawValueWlfi = stratValueWlfi;
}
if (withdrawValueWlfi > 0) {
// Withdraw from strategy
(uint256 wlfi, uint256 usd1) = IStrategy(strategy).withdraw(withdrawValueWlfi);
wlfiBalance += wlfi;
usd1Balance += usd1;
wlfiTotal += wlfi;
// Update remaining
uint256 receivedWlfi = wlfi + wlfiEquivalent(usd1);
remaining = receivedWlfi >= remaining ? 0 : remaining - receivedWlfi;
}
}
}
}
}
function syncBalances() external onlyManagement {
uint256 actualWlfi = WLFI_TOKEN.balanceOf(address(this));
uint256 actualUsd1 = USD1_TOKEN.balanceOf(address(this));
wlfiBalance = actualWlfi;
usd1Balance = actualUsd1;
emit BalancesSynced(actualWlfi, actualUsd1);
}
function forceDeployToStrategies() external onlyManagement nonReentrant {
require(totalStrategyWeight > 0, "No strategies");
_deployToStrategies(wlfiBalance, usd1Balance);
lastDeployment = block.timestamp;
}
// =================================
// EMERGENCY CONTROLS
// =================================
function shutdownStrategy() external onlyEmergencyAuthorized {
isShutdown = true;
emit StrategyShutdown();
}
function emergencyWithdraw(
uint256 wlfiAmount,
uint256 usd1Amount,
address to
) external onlyEmergencyAuthorized {
if (!isShutdown) revert VaultNotShutdown();
if (to == address(0)) revert ZeroAddress();
if (wlfiAmount > 0) {
WLFI_TOKEN.safeTransfer(to, wlfiAmount);
}
if (usd1Amount > 0) {
USD1_TOKEN.safeTransfer(to, usd1Amount);
}
wlfiBalance = WLFI_TOKEN.balanceOf(address(this));
usd1Balance = USD1_TOKEN.balanceOf(address(this));
emit EmergencyWithdraw(to, wlfiAmount, usd1Amount);
}
// =================================
// MANAGEMENT FUNCTIONS
// =================================
function setKeeper(address _keeper) external onlyManagement {
if (_keeper == address(0)) revert ZeroAddress();
keeper = _keeper;
emit UpdateKeeper(_keeper);
}
function setEmergencyAdmin(address _emergencyAdmin) external onlyManagement {
if (_emergencyAdmin == address(0)) revert ZeroAddress();
emergencyAdmin = _emergencyAdmin;
emit UpdateEmergencyAdmin(_emergencyAdmin);
}
function setPerformanceFee(uint16 _performanceFee) external onlyManagement {
if (_performanceFee > MAX_FEE) revert InvalidAmount();
performanceFee = _performanceFee;
emit UpdatePerformanceFee(_performanceFee);
}
function setPerformanceFeeRecipient(address _performanceFeeRecipient) external onlyManagement {
if (_performanceFeeRecipient == address(0)) revert ZeroAddress();
performanceFeeRecipient = _performanceFeeRecipient;
emit UpdatePerformanceFeeRecipient(_performanceFeeRecipient);
}
function setProfitMaxUnlockTime(uint256 _profitMaxUnlockTime) external onlyManagement {
if (_profitMaxUnlockTime > SECONDS_PER_YEAR) revert InvalidAmount();
profitMaxUnlockTime = uint32(_profitMaxUnlockTime);
emit UpdateProfitMaxUnlockTime(_profitMaxUnlockTime);
}
function setPendingManagement(address _management) external onlyManagement {
if (_management == address(0)) revert ZeroAddress();
pendingManagement = _management;
emit UpdatePendingManagement(_management);
}
function acceptManagement() external {
if (msg.sender != pendingManagement) revert Unauthorized();
management = pendingManagement;
pendingManagement = address(0);
emit UpdateManagement(management);
}
function setPaused(bool _paused) external onlyOwner {
paused = _paused;
emit EmergencyPause(_paused);
}
function setSwapSlippage(uint256 _slippageBps) external onlyManagement {
if (_slippageBps > 500) revert InvalidAmount(); // Max 5%
swapSlippageBps = _slippageBps;
emit SwapSlippageUpdated(_slippageBps);
}
// =================================
// UTILITY FUNCTIONS
// =================================
function injectCapital(uint256 wlfiAmount, uint256 usd1Amount) external {
if (wlfiAmount == 0 && usd1Amount == 0) revert InvalidAmount();
if (wlfiAmount > 0) {
WLFI_TOKEN.safeTransferFrom(msg.sender, address(this), wlfiAmount);
wlfiBalance += wlfiAmount;
}
if (usd1Amount > 0) {
USD1_TOKEN.safeTransferFrom(msg.sender, address(this), usd1Amount);
usd1Balance += usd1Amount;
}
emit CapitalInjected(msg.sender, wlfiAmount, usd1Amount);
}
function setDeploymentParams(uint256 _threshold, uint256 _interval) external onlyOwner {
deploymentThreshold = _threshold;
minDeploymentInterval = _interval;
}
function setTWAPInterval(uint32 _interval) external onlyOwner {
require(_interval == 0 || (_interval >= 300 && _interval <= 7200), "Invalid interval");
twapInterval = _interval;
}
function setMaxPriceAge(uint256 _maxPriceAge) external onlyOwner {
require(_maxPriceAge >= 3600 && _maxPriceAge <= 172800, "Invalid age");
maxPriceAge = _maxPriceAge;
}
function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyOwner {
require(_maxTotalSupply >= totalSupply(), "Below current supply");
require(_maxTotalSupply <= 1_000_000_000e18, "Too high");
maxTotalSupply = _maxTotalSupply;
}
function rescueETH() external onlyOwner {
uint256 balance = address(this).balance;
if (balance > 0) {
payable(owner()).transfer(balance);
}
}
function rescueToken(address token, uint256 amount, address to) external onlyOwner {
if (token == address(WLFI_TOKEN) || token == address(USD1_TOKEN)) {
revert("Use emergency functions for vault tokens");
}
if (to == address(0)) revert ZeroAddress();
IERC20(token).safeTransfer(to, amount);
}
// =================================
// VIEW FUNCTIONS
// =================================
function getCurrentPrices() external view returns (
uint256 wlfiPriceUSD,
uint256 usd1PriceUSD
) {
return (getWLFIPrice(), getUSD1Price());
}
function getVaultBalances() external view returns (uint256 wlfi, uint256 usd1) {
return (wlfiBalance, usd1Balance);
}
function getVaultBalancesWlfi() external view returns (
uint256 wlfiAmount,
uint256 usd1InWlfi,
uint256 totalWlfi
) {
wlfiAmount = wlfiBalance;
usd1InWlfi = wlfiEquivalent(usd1Balance);
totalWlfi = wlfiAmount + usd1InWlfi;
}
function getStrategies() external view returns (
address[] memory strategies,
uint256[] memory weights
) {
uint256 length = strategyList.length;
strategies = new address[](length);
weights = new uint256[](length);
for (uint256 i = 0; i < length; i++) {
strategies[i] = strategyList[i];
weights[i] = strategyWeights[strategyList[i]];
}
}
function getStrategyAssets() external view returns (
address[] memory strategies,
uint256[] memory wlfiAmounts,
uint256[] memory usd1Amounts
) {
uint256 length = strategyList.length;
strategies = new address[](length);
wlfiAmounts = new uint256[](length);
usd1Amounts = new uint256[](length);
for (uint256 i = 0; i < length; i++) {
strategies[i] = strategyList[i];
if (activeStrategies[strategyList[i]]) {
(wlfiAmounts[i], usd1Amounts[i]) = IStrategy(strategyList[i]).getTotalAmounts();
}
}
}
function previewDepositDual(uint256 wlfiAmount, uint256 usd1Amount)
external
view
returns (uint256 shares, uint256 totalWlfi)
{
totalWlfi = wlfiAmount + wlfiEquivalent(usd1Amount);
shares = previewDeposit(totalWlfi);
}
}
"
},
"node_modules/@openzeppelin/contracts/access/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
"
},
"node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";
/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
*
* TIP: For a detailed writeup see our guide
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* The default value of {decimals} is 18. To change this, you should override
* this function so it returns a different value.
*
* We have followed general OpenZeppelin Contracts guidelines: functions revert
* instead returning `false` on failure. This behavior is nonetheless
* conventional and does not conflict with the expectations of ERC-20
* applications.
*/
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
mapping(address account => uint256) private _balances;
mapping(address account => mapping(address spender => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
/**
* @dev Sets the values for {name} and {symbol}.
*
* Both values are immutable: they can only be set once during construction.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev Returns the name of the token.
*/
function name() public view virtual returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the default value returned by this function, unless
* it's overridden.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual returns (uint8) {
return 18;
}
/// @inheritdoc IERC20
function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}
/// @inheritdoc IERC20
function balanceOf(address account) public view virtual returns (uint256) {
return _balances[account];
}
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - the caller must have a balance of at least `value`.
*/
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
/// @inheritdoc IERC20
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `value` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, value);
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Skips emitting an {Approval} event indicating an allowance update. This is not
* required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].
*
* NOTE: Does not update the allowance if the current allowance
* is the maximum `uint256`.
*
* Requirements:
*
* - `from` and `to` cannot be the zero address.
* - `from` must have a balance of at least `value`.
* - the caller must have allowance for ``from``'s tokens of at least
* `value`.
*/
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}
/**
* @dev Moves a `value` amount of tokens from `from` to `to`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
/**
* @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
* this function.
*
* Emits a {Transfer} event.
*/
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}
emit Transfer(from, to, value);
}
/**
* @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
* Relies on the `_update` mechanism
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), account, value);
}
/**
* @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
* Relies on the `_update` mechanism.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead
*/
function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
}
/**
* @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*
* Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
*/
function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}
/**
* @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
*
* By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
* `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any
* `Approval` event during `transferFrom` operations.
*
* Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to
* true using the following override:
*
* ```solidity
* function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
* super._approve(owner, spender, value, true);
* }
* ```
*
* Requirements are the same as {_approve}.
*/
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
/**
* @dev Updates `owner`'s allowance for `spender` based on spent `value`.
*
* Does not update the allowance value in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Does not emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance < type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
}
"
},
"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)
pragma solidity >=0.4.16;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
},
"node_modules/@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/extensions/ERC4626.sol)
pragma solidity ^0.8.20;
import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol";
import {SafeERC20} from "../utils/SafeERC20.sol";
import {IERC4626} from "../../../interfaces/IERC4626.sol";
import {Math} from "../../../utils/math/Math.sol";
/**
* @dev Implementation of the ERC-4626 "Tokenized Vault Standard" as defined in
* https://eips.ethereum.org/EIPS/eip-4626[ERC-4626].
*
* This extension allows the minting and burning of "shares" (represented using the ERC-20 inheritance) in exchange for
* underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
* the ERC-20 standard. Any additional extensions included along it would affect the "shares" token represented by this
* contract and not the "assets" token which is an independent contract.
*
* [CAUTION]
* ====
* In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through fr
Submitted on: 2025-10-29 16:51:50
Comments
Log in to comment.
No comments yet.