555 lines
20 KiB
Solidity
555 lines
20 KiB
Solidity
// 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;
|
||
} |