// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; contract YTAssetVault is Initializable, ERC20Upgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable { using SafeERC20 for IERC20; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } error Forbidden(); error HardCapExceeded(); error InvalidAmount(); error InvalidHardCap(); error InvalidPrice(); error InsufficientUSDC(); error InsufficientYTA(); error StillInLockPeriod(); error RequestNotFound(); error RequestAlreadyProcessed(); error InvalidBatchSize(); error InvalidPriceFeed(); error InvalidChainlinkPrice(); address public factory; address public manager; uint256 public hardCap; uint256 public managedAssets; address public usdcAddress; uint8 public usdcDecimals; uint256 public ytPrice; uint256 public constant PRICE_PRECISION = 1e30; uint256 public constant CHAINLINK_PRICE_PRECISION = 1e8; uint256 public nextRedemptionTime; AggregatorV3Interface internal usdcPriceFeed; struct WithdrawRequest { address user; uint256 ytAmount; uint256 usdcAmount; uint256 requestTime; uint256 queueIndex; bool processed; } mapping(uint256 => WithdrawRequest) public withdrawRequests; mapping(address => uint256[]) private userRequestIds; uint256 public requestIdCounter; uint256 public processedUpToIndex; uint256 public pendingRequestsCount; event HardCapSet(uint256 newHardCap); event ManagerSet(address indexed newManager); event AssetsWithdrawn(address indexed to, uint256 amount); event AssetsDeposited(uint256 amount); event PriceUpdated(uint256 ytPrice, uint256 timestamp); event Buy(address indexed user, uint256 usdcAmount, uint256 ytAmount); event Sell(address indexed user, uint256 ytAmount, uint256 usdcAmount); event NextRedemptionTimeSet(uint256 newRedemptionTime); event WithdrawRequestCreated(uint256 indexed requestId, address indexed user, uint256 ytAmount, uint256 usdcAmount, uint256 queueIndex); event WithdrawRequestProcessed(uint256 indexed requestId, address indexed user, uint256 usdcAmount); event BatchProcessed(uint256 startIndex, uint256 endIndex, uint256 processedCount, uint256 totalUsdcDistributed); modifier onlyFactory() { if (msg.sender != factory) revert Forbidden(); _; } modifier onlyManager() { if (msg.sender != manager) revert Forbidden(); _; } function initialize( string memory _name, string memory _symbol, address _manager, uint256 _hardCap, address _usdc, uint256 _redemptionTime, uint256 _initialYtPrice, address _usdcPriceFeed ) external initializer { __ERC20_init(_name, _symbol); __UUPSUpgradeable_init(); __ReentrancyGuard_init(); __Pausable_init(); if (_usdcPriceFeed == address(0)) revert InvalidPriceFeed(); usdcPriceFeed = AggregatorV3Interface(_usdcPriceFeed); usdcAddress = _usdc; usdcDecimals = IERC20Metadata(usdcAddress).decimals(); factory = msg.sender; manager = _manager; hardCap = _hardCap; ytPrice = _initialYtPrice == 0 ? PRICE_PRECISION : _initialYtPrice; nextRedemptionTime = _redemptionTime; } function _authorizeUpgrade(address newImplementation) internal override onlyFactory {} function _getUSDCPrice() internal view returns (uint256) { ( /* uint80 roundId */, int256 price, /* uint256 startedAt */, /* uint256 updatedAt */, /* uint80 answeredInRound */ ) = usdcPriceFeed.latestRoundData(); if (price <= 0) revert InvalidChainlinkPrice(); return uint256(price); } function _getPriceConversionFactor() internal view returns (uint256) { uint8 ytDecimals = decimals(); uint256 numerator = (10 ** ytDecimals) * PRICE_PRECISION; uint256 denominator = (10 ** usdcDecimals) * CHAINLINK_PRICE_PRECISION; return numerator / denominator; } function setHardCap(uint256 _hardCap) external onlyFactory { if (_hardCap < totalSupply()) revert InvalidHardCap(); hardCap = _hardCap; emit HardCapSet(_hardCap); } function setManager(address _manager) external onlyFactory { manager = _manager; emit ManagerSet(_manager); } function pause() external onlyFactory { _pause(); } function unpause() external onlyFactory { _unpause(); } function setNextRedemptionTime(uint256 _nextRedemptionTime) external onlyFactory { nextRedemptionTime = _nextRedemptionTime; emit NextRedemptionTimeSet(_nextRedemptionTime); } function updatePrices(uint256 _ytPrice) external onlyFactory { if (_ytPrice == 0) revert InvalidPrice(); ytPrice = _ytPrice; emit PriceUpdated(_ytPrice, block.timestamp); } function depositYT(uint256 _usdcAmount) external nonReentrant whenNotPaused returns (uint256 ytAmount) { if (_usdcAmount == 0) revert InvalidAmount(); uint256 usdcPrice = _getUSDCPrice(); uint256 conversionFactor = _getPriceConversionFactor(); ytAmount = (_usdcAmount * usdcPrice * conversionFactor) / ytPrice; if (hardCap > 0 && totalSupply() + ytAmount > hardCap) { revert HardCapExceeded(); } IERC20(usdcAddress).safeTransferFrom(msg.sender, address(this), _usdcAmount); _mint(msg.sender, ytAmount); emit Buy(msg.sender, _usdcAmount, ytAmount); } function withdrawYT(uint256 _ytAmount) external nonReentrant whenNotPaused returns (uint256 requestId) { if (_ytAmount == 0) revert InvalidAmount(); if (balanceOf(msg.sender) < _ytAmount) revert InsufficientYTA(); if (block.timestamp < nextRedemptionTime) { revert StillInLockPeriod(); } uint256 usdcPrice = _getUSDCPrice(); uint256 conversionFactor = _getPriceConversionFactor(); uint256 usdcAmount = (_ytAmount * ytPrice) / (usdcPrice * conversionFactor); _burn(msg.sender, _ytAmount); requestId = requestIdCounter; withdrawRequests[requestId] = WithdrawRequest({ user: msg.sender, ytAmount: _ytAmount, usdcAmount: usdcAmount, requestTime: block.timestamp, queueIndex: requestId, processed: false }); userRequestIds[msg.sender].push(requestId); requestIdCounter++; pendingRequestsCount++; emit WithdrawRequestCreated(requestId, msg.sender, _ytAmount, usdcAmount, requestId); } function processBatchWithdrawals(uint256 _batchSize) external nonReentrant whenNotPaused returns (uint256 processedCount, uint256 totalDistributed) { if (msg.sender != manager && msg.sender != factory) { revert Forbidden(); } if (_batchSize == 0) revert InvalidBatchSize(); uint256 availableUSDC = IERC20(usdcAddress).balanceOf(address(this)); uint256 startIndex = processedUpToIndex; for (uint256 i = processedUpToIndex; i < requestIdCounter && processedCount < _batchSize; i++) { WithdrawRequest storage request = withdrawRequests[i]; if (request.processed) { continue; } if (availableUSDC >= request.usdcAmount) { IERC20(usdcAddress).safeTransfer(request.user, request.usdcAmount); request.processed = true; availableUSDC -= request.usdcAmount; totalDistributed += request.usdcAmount; processedCount++; pendingRequestsCount--; emit WithdrawRequestProcessed(i, request.user, request.usdcAmount); } else { break; } } if (processedCount > 0) { for (uint256 i = processedUpToIndex; i < requestIdCounter; i++) { if (!withdrawRequests[i].processed) { processedUpToIndex = i; break; } if (i == requestIdCounter - 1) { processedUpToIndex = requestIdCounter; } } } emit BatchProcessed(startIndex, processedUpToIndex, processedCount, totalDistributed); } function getUserRequestIds(address _user) external view returns (uint256[] memory) { return userRequestIds[_user]; } function getRequestDetails(uint256 _requestId) external view returns (WithdrawRequest memory request) { if (_requestId >= requestIdCounter) revert RequestNotFound(); return withdrawRequests[_requestId]; } function getPendingRequestsCount() external view returns (uint256) { return pendingRequestsCount; } function getUserPendingRequests(address _user) external view returns (WithdrawRequest[] memory pendingRequests) { uint256[] memory requestIds = userRequestIds[_user]; uint256 pendingCount = 0; for (uint256 i = 0; i < requestIds.length; i++) { if (!withdrawRequests[requestIds[i]].processed) { pendingCount++; } } pendingRequests = new WithdrawRequest[](pendingCount); uint256 index = 0; for (uint256 i = 0; i < requestIds.length; i++) { uint256 requestId = requestIds[i]; if (!withdrawRequests[requestId].processed) { pendingRequests[index] = withdrawRequests[requestId]; index++; } } } function getQueueProgress() external view returns ( uint256 currentIndex, uint256 totalRequests, uint256 pendingRequests ) { currentIndex = processedUpToIndex; totalRequests = requestIdCounter; pendingRequests = pendingRequestsCount; } function getTimeUntilNextRedemption() external view returns (uint256 remainingTime) { if (block.timestamp >= nextRedemptionTime) { return 0; } return nextRedemptionTime - block.timestamp; } function canRedeemNow() external view returns (bool) { return block.timestamp >= nextRedemptionTime; } function withdrawForManagement(address _to, uint256 _amount) external onlyManager nonReentrant whenNotPaused { if (_amount == 0) revert InvalidAmount(); uint256 availableAssets = IERC20(usdcAddress).balanceOf(address(this)); if (_amount > availableAssets) revert InvalidAmount(); managedAssets += _amount; IERC20(usdcAddress).safeTransfer(_to, _amount); emit AssetsWithdrawn(_to, _amount); } function depositManagedAssets(uint256 _amount) external onlyManager nonReentrant whenNotPaused { if (_amount == 0) revert InvalidAmount(); if (_amount >= managedAssets) { managedAssets = 0; } else { managedAssets -= _amount; } IERC20(usdcAddress).safeTransferFrom(msg.sender, address(this), _amount); emit AssetsDeposited(_amount); } function totalAssets() public view returns (uint256) { return IERC20(usdcAddress).balanceOf(address(this)) + managedAssets; } function idleAssets() public view returns (uint256) { return IERC20(usdcAddress).balanceOf(address(this)); } function previewBuy(uint256 _usdcAmount) external view returns (uint256 ytAmount) { uint256 usdcPrice = _getUSDCPrice(); uint256 conversionFactor = _getPriceConversionFactor(); ytAmount = (_usdcAmount * usdcPrice * conversionFactor) / ytPrice; } function previewSell(uint256 _ytAmount) external view returns (uint256 usdcAmount) { uint256 usdcPrice = _getUSDCPrice(); uint256 conversionFactor = _getPriceConversionFactor(); usdcAmount = (_ytAmount * ytPrice) / (usdcPrice * conversionFactor); } function getVaultInfo() external view returns ( uint256 _totalAssets, uint256 _idleAssets, uint256 _managedAssets, uint256 _totalSupply, uint256 _hardCap, uint256 _usdcPrice, uint256 _ytPrice, uint256 _nextRedemptionTime ) { _usdcPrice = _getUSDCPrice(); _totalAssets = totalAssets(); _idleAssets = idleAssets(); _managedAssets = managedAssets; _totalSupply = totalSupply(); _hardCap = hardCap; _ytPrice = ytPrice; _nextRedemptionTime = nextRedemptionTime; } uint256[50] private __gap; }