Files
assetxContracts/contracts/ytLp/core/YTVault.sol

553 lines
19 KiB
Solidity
Raw Permalink Normal View History

2025-12-18 13:07:35 +08:00
// 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;
2025-12-23 14:05:41 +08:00
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
2025-12-18 13:07:35 +08:00
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;
2025-12-25 13:29:35 +08:00
mapping(address => bool) public isSwapper;
2025-12-18 13:07:35 +08:00
bool public isSwapEnabled;
bool public emergencyMode;
address[] public allWhitelistedTokens;
mapping(address => bool) public whitelistedTokens;
2025-12-25 13:29:35 +08:00
mapping(address => bool) public stableTokens;
2025-12-18 13:07:35 +08:00
mapping(address => uint256) public tokenDecimals;
mapping(address => uint256) public tokenWeights;
uint256 public totalTokenWeights;
mapping(address => uint256) public poolAmounts;
2025-12-25 13:29:35 +08:00
mapping(address => uint256) public tokenBalances;
2025-12-18 13:07:35 +08:00
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;
2025-12-25 13:29:35 +08:00
uint256 public maxSwapSlippageBps;
2025-12-18 13:07:35 +08:00
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);
2025-12-23 14:05:41 +08:00
event GovChanged(address indexed oldGov, address indexed newGov);
event PoolManagerChanged(address indexed oldManager, address indexed newManager);
2025-12-18 13:07:35 +08:00
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;
2025-12-25 13:29:35 +08:00
maxSwapSlippageBps = 1000;
2025-12-18 13:07:35 +08:00
stableTokens[_usdy] = true;
}
function _authorizeUpgrade(address newImplementation) internal override onlyGov {}
function setGov(address _gov) external onlyGov {
if (_gov == address(0)) revert InvalidAddress();
2025-12-23 14:05:41 +08:00
address oldGov = gov;
2025-12-18 13:07:35 +08:00
gov = _gov;
2025-12-23 14:05:41 +08:00
emit GovChanged(oldGov, _gov);
2025-12-18 13:07:35 +08:00
}
function setPoolManager(address _manager) external onlyGov {
if (_manager == address(0)) revert InvalidAddress();
2025-12-23 14:05:41 +08:00
address oldManager = ytPoolManager;
2025-12-18 13:07:35 +08:00
ytPoolManager = _manager;
2025-12-23 14:05:41 +08:00
emit PoolManagerChanged(oldManager, _manager);
2025-12-18 13:07:35 +08:00
}
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 {
2025-12-25 13:29:35 +08:00
if (_slippageBps > 2000) revert SlippageTooHigh();
2025-12-18 13:07:35 +08:00
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);
_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) {
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;
}
2025-12-23 14:05:41 +08:00
uint256 sumDiff = initialDiff + nextDiff;
if (sumDiff / 2 > targetAmount) {
sumDiff = targetAmount * 2;
2025-12-18 13:07:35 +08:00
}
2025-12-23 14:05:41 +08:00
uint256 taxBps = _taxBasisPoints * sumDiff / (targetAmount * 2);
2025-12-18 13:07:35 +08:00
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;
2025-12-25 13:29:35 +08:00
}