Files
assetxContracts/contracts/ytLp/core/YTVault.sol
2025-12-25 13:29:35 +08:00

555 lines
20 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}