// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../../interfaces/IUSDY.sol"; import "../../interfaces/IYTPriceFeed.sol"; contract YTVault is Initializable, UUPSUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } error Forbidden(); error OnlyPoolManager(); error NotSwapper(); error EmergencyMode(); error InvalidAddress(); error TokenNotWhitelisted(); error InvalidFee(); error NotInEmergency(); error SlippageTooHigh(); error SwapDisabled(); error InvalidAmount(); error InsufficientPool(); error SameToken(); error AmountExceedsLimit(); error MaxUSDYExceeded(); error InsufficientUSDYAmount(); error InvalidPoolAmount(); error DailyLimitExceeded(); uint256 public constant PRICE_PRECISION = 10 ** 30; uint256 public constant BASIS_POINTS_DIVISOR = 10000; uint256 public constant USDY_DECIMALS = 18; address public gov; address public ytPoolManager; address public priceFeed; address public usdy; mapping(address => bool) public isSwapper; bool public isSwapEnabled; bool public emergencyMode; address[] public allWhitelistedTokens; mapping(address => bool) public whitelistedTokens; mapping(address => bool) public stableTokens; mapping(address => uint256) public tokenDecimals; mapping(address => uint256) public tokenWeights; uint256 public totalTokenWeights; mapping(address => uint256) public poolAmounts; mapping(address => uint256) public tokenBalances; mapping(address => uint256) public usdyAmounts; mapping(address => uint256) public maxUsdyAmounts; uint256 public swapFeeBasisPoints; uint256 public stableSwapFeeBasisPoints; uint256 public taxBasisPoints; uint256 public stableTaxBasisPoints; bool public hasDynamicFees; uint256 public maxSwapSlippageBps; mapping(address => uint256) public maxSwapAmount; event Swap( address indexed account, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, uint256 feeBasisPoints ); event AddLiquidity( address indexed account, address indexed token, uint256 amount, uint256 usdyAmount ); event RemoveLiquidity( address indexed account, address indexed token, uint256 usdyAmount, uint256 amountOut ); event EmergencyModeSet(bool enabled); event SwapEnabledSet(bool enabled); event GovChanged(address indexed oldGov, address indexed newGov); event PoolManagerChanged(address indexed oldManager, address indexed newManager); modifier onlyGov() { if (msg.sender != gov) revert Forbidden(); _; } modifier onlyPoolManager() { if (msg.sender != ytPoolManager) revert OnlyPoolManager(); _; } modifier onlySwapper() { if (!isSwapper[msg.sender] && msg.sender != ytPoolManager) revert NotSwapper(); _; } modifier notInEmergency() { if (emergencyMode) revert EmergencyMode(); _; } function initialize(address _usdy, address _priceFeed) external initializer { if (_usdy == address(0) || _priceFeed == address(0)) revert InvalidAddress(); __ReentrancyGuard_init(); __UUPSUpgradeable_init(); gov = msg.sender; usdy = _usdy; priceFeed = _priceFeed; isSwapEnabled = true; emergencyMode = false; swapFeeBasisPoints = 30; stableSwapFeeBasisPoints = 4; taxBasisPoints = 50; stableTaxBasisPoints = 20; hasDynamicFees = true; maxSwapSlippageBps = 1000; stableTokens[_usdy] = true; } function _authorizeUpgrade(address newImplementation) internal override onlyGov {} function setGov(address _gov) external onlyGov { if (_gov == address(0)) revert InvalidAddress(); address oldGov = gov; gov = _gov; emit GovChanged(oldGov, _gov); } function setPoolManager(address _manager) external onlyGov { if (_manager == address(0)) revert InvalidAddress(); address oldManager = ytPoolManager; ytPoolManager = _manager; emit PoolManagerChanged(oldManager, _manager); } function setSwapper(address _swapper, bool _isActive) external onlyGov { if (_swapper == address(0)) revert InvalidAddress(); isSwapper[_swapper] = _isActive; } function setWhitelistedToken( address _token, uint256 _decimals, uint256 _weight, uint256 _maxUsdyAmount, bool _isStable ) external onlyGov { if (_token == address(0)) revert InvalidAddress(); if (!whitelistedTokens[_token]) { allWhitelistedTokens.push(_token); whitelistedTokens[_token] = true; } totalTokenWeights = totalTokenWeights - tokenWeights[_token] + _weight; tokenDecimals[_token] = _decimals; tokenWeights[_token] = _weight; maxUsdyAmounts[_token] = _maxUsdyAmount; stableTokens[_token] = _isStable; } function clearWhitelistedToken(address _token) external onlyGov { if (!whitelistedTokens[_token]) revert TokenNotWhitelisted(); totalTokenWeights = totalTokenWeights - tokenWeights[_token]; delete whitelistedTokens[_token]; delete stableTokens[_token]; delete tokenDecimals[_token]; delete tokenWeights[_token]; delete maxUsdyAmounts[_token]; } function setSwapFees( uint256 _swapFee, uint256 _stableSwapFee, uint256 _taxBasisPoints, uint256 _stableTaxBasisPoints ) external onlyGov { if (_swapFee > 100 || _stableSwapFee > 50) revert InvalidFee(); swapFeeBasisPoints = _swapFee; stableSwapFeeBasisPoints = _stableSwapFee; taxBasisPoints = _taxBasisPoints; stableTaxBasisPoints = _stableTaxBasisPoints; } function setDynamicFees(bool _hasDynamicFees) external onlyGov { hasDynamicFees = _hasDynamicFees; } function setEmergencyMode(bool _emergencyMode) external onlyGov { emergencyMode = _emergencyMode; emit EmergencyModeSet(_emergencyMode); } function setSwapEnabled(bool _isSwapEnabled) external onlyGov { isSwapEnabled = _isSwapEnabled; emit SwapEnabledSet(_isSwapEnabled); } function withdrawToken(address _token, address _receiver, uint256 _amount) external onlyGov { if (!emergencyMode) revert NotInEmergency(); IERC20(_token).safeTransfer(_receiver, _amount); _updateTokenBalance(_token); } function setMaxSwapSlippageBps(uint256 _slippageBps) external onlyGov { if (_slippageBps > 2000) revert SlippageTooHigh(); maxSwapSlippageBps = _slippageBps; } function setMaxSwapAmount(address _token, uint256 _amount) external onlyGov { maxSwapAmount[_token] = _amount; } function buyUSDY(address _token, address _receiver) external onlyPoolManager nonReentrant notInEmergency returns (uint256) { if (!whitelistedTokens[_token]) revert TokenNotWhitelisted(); if (!isSwapEnabled) revert SwapDisabled(); uint256 tokenAmount = _transferIn(_token); if (tokenAmount == 0) revert InvalidAmount(); uint256 price = _getPrice(_token, false); uint256 usdyAmount = tokenAmount * price / PRICE_PRECISION; usdyAmount = _adjustForDecimals(usdyAmount, _token, usdy); if (usdyAmount == 0) revert InvalidAmount(); uint256 feeBasisPoints = _getSwapFeeBasisPoints(_token, usdy, usdyAmount); uint256 feeAmount = tokenAmount * feeBasisPoints / BASIS_POINTS_DIVISOR; uint256 amountAfterFees = tokenAmount - feeAmount; uint256 usdyAmountAfterFees = amountAfterFees * price / PRICE_PRECISION; usdyAmountAfterFees = _adjustForDecimals(usdyAmountAfterFees, _token, usdy); // 手续费直接留在池子中:全部代币加入poolAmount,但只铸造扣费后的USDY _increasePoolAmount(_token, tokenAmount); _increaseUsdyAmount(_token, usdyAmountAfterFees); IUSDY(usdy).mint(_receiver, usdyAmountAfterFees); emit AddLiquidity(_receiver, _token, tokenAmount, usdyAmountAfterFees); return usdyAmountAfterFees; } function sellUSDY(address _token, address _receiver) external onlyPoolManager nonReentrant notInEmergency returns (uint256) { if (!whitelistedTokens[_token]) revert TokenNotWhitelisted(); if (!isSwapEnabled) revert SwapDisabled(); uint256 usdyAmount = _transferIn(usdy); if (usdyAmount == 0) revert InvalidAmount(); uint256 price = _getPrice(_token, true); uint256 redemptionAmount = usdyAmount * PRICE_PRECISION / price; redemptionAmount = _adjustForDecimals(redemptionAmount, usdy, _token); if (redemptionAmount == 0) revert InvalidAmount(); uint256 feeBasisPoints = _getSwapFeeBasisPoints(usdy, _token, redemptionAmount); uint256 amountOut = redemptionAmount * (BASIS_POINTS_DIVISOR - feeBasisPoints) / BASIS_POINTS_DIVISOR; if (amountOut == 0) revert InvalidAmount(); if (poolAmounts[_token] < amountOut) revert InsufficientPool(); uint256 usdyAmountOut = amountOut * price / PRICE_PRECISION; usdyAmountOut = _adjustForDecimals(usdyAmountOut, _token, usdy); _decreasePoolAmount(_token, amountOut); _decreaseUsdyAmount(_token, usdyAmountOut); IUSDY(usdy).burn(address(this), usdyAmount); IERC20(_token).safeTransfer(_receiver, amountOut); _updateTokenBalance(_token); emit RemoveLiquidity(_receiver, _token, usdyAmount, amountOut); return amountOut; } function swap( address _tokenIn, address _tokenOut, address _receiver ) external onlySwapper nonReentrant notInEmergency returns (uint256) { if (!isSwapEnabled) revert SwapDisabled(); if (!whitelistedTokens[_tokenIn]) revert TokenNotWhitelisted(); if (!whitelistedTokens[_tokenOut]) revert TokenNotWhitelisted(); if (_tokenIn == _tokenOut) revert SameToken(); uint256 amountIn = _transferIn(_tokenIn); if (amountIn == 0) revert InvalidAmount(); if (maxSwapAmount[_tokenIn] > 0) { if (amountIn > maxSwapAmount[_tokenIn]) revert AmountExceedsLimit(); } uint256 priceIn = _getPrice(_tokenIn, false); uint256 priceOut = _getPrice(_tokenOut, true); uint256 usdyAmount = amountIn * priceIn / PRICE_PRECISION; usdyAmount = _adjustForDecimals(usdyAmount, _tokenIn, usdy); uint256 amountOut = usdyAmount * PRICE_PRECISION / priceOut; amountOut = _adjustForDecimals(amountOut, usdy, _tokenOut); uint256 feeBasisPoints = _getSwapFeeBasisPoints(_tokenIn, _tokenOut, usdyAmount); uint256 amountOutAfterFees = amountOut * (BASIS_POINTS_DIVISOR - feeBasisPoints) / BASIS_POINTS_DIVISOR; if (amountOutAfterFees == 0) revert InvalidAmount(); if (poolAmounts[_tokenOut] < amountOutAfterFees) revert InsufficientPool(); _validateSwapSlippage(amountIn, amountOutAfterFees, priceIn, priceOut); _increasePoolAmount(_tokenIn, amountIn); _decreasePoolAmount(_tokenOut, amountOutAfterFees); _increaseUsdyAmount(_tokenIn, usdyAmount); _decreaseUsdyAmount(_tokenOut, usdyAmount); IERC20(_tokenOut).safeTransfer(_receiver, amountOutAfterFees); _updateTokenBalance(_tokenOut); emit Swap(msg.sender, _tokenIn, _tokenOut, amountIn, amountOutAfterFees, feeBasisPoints); return amountOutAfterFees; } function getPrice(address _token, bool _maximise) external view returns (uint256) { return _getPrice(_token, _maximise); } function getMaxPrice(address _token) external view returns (uint256) { return _getPrice(_token, true); } function getMinPrice(address _token) external view returns (uint256) { return _getPrice(_token, false); } function getAllPoolTokens() external view returns (address[] memory) { return allWhitelistedTokens; } function getPoolValue(bool _maximise) external view returns (uint256) { uint256 totalValue = 0; for (uint256 i = 0; i < allWhitelistedTokens.length; i++) { address token = allWhitelistedTokens[i]; if (!whitelistedTokens[token]) continue; uint256 amount = poolAmounts[token]; uint256 price = _getPrice(token, _maximise); uint256 value = amount * price / PRICE_PRECISION; value = _adjustForDecimals(value, token, usdy); totalValue += value; } return totalValue; } function getTargetUsdyAmount(address _token) public view returns (uint256) { uint256 supply = IERC20(usdy).totalSupply(); if (supply == 0) { return 0; } uint256 weight = tokenWeights[_token]; return weight * supply / totalTokenWeights; } function _increaseUsdyAmount(address _token, uint256 _amount) private { usdyAmounts[_token] = usdyAmounts[_token] + _amount; uint256 maxUsdyAmount = maxUsdyAmounts[_token]; if (maxUsdyAmount != 0) { if (usdyAmounts[_token] > maxUsdyAmount) revert MaxUSDYExceeded(); } } function _decreaseUsdyAmount(address _token, uint256 _amount) private { uint256 value = usdyAmounts[_token]; if (value < _amount) revert InsufficientUSDYAmount(); usdyAmounts[_token] = value - _amount; } function getSwapFeeBasisPoints( address _tokenIn, address _tokenOut, uint256 _usdyAmount ) public view returns (uint256) { return _getSwapFeeBasisPoints(_tokenIn, _tokenOut, _usdyAmount); } /** * @notice 获取赎回手续费率(sellUSDY时使用) * @param _token 代币地址 * @param _usdyAmount USDY数量 * @return 手续费率(basis points) */ function getRedemptionFeeBasisPoints( address _token, uint256 _usdyAmount ) public view returns (uint256) { return _getSwapFeeBasisPoints(usdy, _token, _usdyAmount); } function _getSwapFeeBasisPoints( address _tokenIn, address _tokenOut, uint256 _usdyAmount ) private view returns (uint256) { // 稳定币交换是指两个代币都是稳定币(如 USDC <-> USDT) bool isStableSwap = stableTokens[_tokenIn] && stableTokens[_tokenOut]; uint256 baseBps = isStableSwap ? stableSwapFeeBasisPoints : swapFeeBasisPoints; uint256 taxBps = isStableSwap ? stableTaxBasisPoints : taxBasisPoints; if (!hasDynamicFees) { return baseBps; } uint256 feesBasisPoints0 = getFeeBasisPoints(_tokenIn, _usdyAmount, baseBps, taxBps, true); uint256 feesBasisPoints1 = getFeeBasisPoints(_tokenOut, _usdyAmount, baseBps, taxBps, false); return feesBasisPoints0 > feesBasisPoints1 ? feesBasisPoints0 : feesBasisPoints1; } function getFeeBasisPoints( address _token, uint256 _usdyDelta, uint256 _feeBasisPoints, uint256 _taxBasisPoints, bool _increment ) public view returns (uint256) { if (!hasDynamicFees) { return _feeBasisPoints; } uint256 initialAmount = usdyAmounts[_token]; uint256 nextAmount = initialAmount + _usdyDelta; if (!_increment) { nextAmount = _usdyDelta > initialAmount ? 0 : initialAmount - _usdyDelta; } uint256 targetAmount = getTargetUsdyAmount(_token); if (targetAmount == 0) { return _feeBasisPoints; } uint256 initialDiff = initialAmount > targetAmount ? initialAmount - targetAmount : targetAmount - initialAmount; uint256 nextDiff = nextAmount > targetAmount ? nextAmount - targetAmount : targetAmount - nextAmount; if (nextDiff < initialDiff) { uint256 rebateBps = _taxBasisPoints * initialDiff / targetAmount; return rebateBps > _feeBasisPoints ? 0 : _feeBasisPoints - rebateBps; } uint256 sumDiff = initialDiff + nextDiff; if (sumDiff / 2 > targetAmount) { sumDiff = targetAmount * 2; } uint256 taxBps = _taxBasisPoints * sumDiff / (targetAmount * 2); return _feeBasisPoints + taxBps; } function _transferIn(address _token) private returns (uint256) { uint256 prevBalance = tokenBalances[_token]; uint256 nextBalance = IERC20(_token).balanceOf(address(this)); tokenBalances[_token] = nextBalance; return nextBalance - prevBalance; } function _updateTokenBalance(address _token) private { tokenBalances[_token] = IERC20(_token).balanceOf(address(this)); } function _increasePoolAmount(address _token, uint256 _amount) private { poolAmounts[_token] += _amount; _validatePoolAmount(_token); } function _decreasePoolAmount(address _token, uint256 _amount) private { if (poolAmounts[_token] < _amount) revert InsufficientPool(); poolAmounts[_token] -= _amount; } function _validatePoolAmount(address _token) private view { if (poolAmounts[_token] > tokenBalances[_token]) revert InvalidPoolAmount(); } function _validateSwapSlippage( uint256 _amountIn, uint256 _amountOut, uint256 _priceIn, uint256 _priceOut ) private view { uint256 expectedOut = _amountIn * _priceIn / _priceOut; if (expectedOut > _amountOut) { uint256 slippage = (expectedOut - _amountOut) * BASIS_POINTS_DIVISOR / expectedOut; if (slippage > maxSwapSlippageBps) revert SlippageTooHigh(); } } function _getPrice(address _token, bool _maximise) private view returns (uint256) { return IYTPriceFeed(priceFeed).getPrice(_token, _maximise); } function _adjustForDecimals( uint256 _amount, address _tokenFrom, address _tokenTo ) private view returns (uint256) { uint256 decimalsFrom = _tokenFrom == usdy ? USDY_DECIMALS : tokenDecimals[_tokenFrom]; uint256 decimalsTo = _tokenTo == usdy ? USDY_DECIMALS : tokenDecimals[_tokenTo]; if (decimalsFrom == decimalsTo) { return _amount; } if (decimalsFrom > decimalsTo) { return _amount / (10 ** (decimalsFrom - decimalsTo)); } return _amount * (10 ** (decimalsTo - decimalsFrom)); } uint256[50] private __gap; }