commit
This commit is contained in:
266
contracts/ytLp/core/YTPoolManager.sol
Normal file
266
contracts/ytLp/core/YTPoolManager.sol
Normal file
@@ -0,0 +1,266 @@
|
||||
// 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/IYTVault.sol";
|
||||
import "../../interfaces/IYTLPToken.sol";
|
||||
import "../../interfaces/IUSDY.sol";
|
||||
|
||||
/**
|
||||
* @title YTPoolManager
|
||||
* @notice 管理ytLP的铸造和赎回,计算池子AUM
|
||||
* @dev UUPS可升级合约
|
||||
*/
|
||||
contract YTPoolManager is Initializable, UUPSUpgradeable, ReentrancyGuardUpgradeable {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
error Forbidden();
|
||||
error InvalidAddress();
|
||||
error InvalidDuration();
|
||||
error PrivateMode();
|
||||
error InvalidAmount();
|
||||
error InsufficientOutput();
|
||||
error CooldownNotPassed();
|
||||
|
||||
uint256 public constant PRICE_PRECISION = 10 ** 30;
|
||||
uint256 public constant YTLP_PRECISION = 10 ** 18;
|
||||
uint256 public constant BASIS_POINTS_DIVISOR = 10000;
|
||||
uint256 public constant MAX_COOLDOWN_DURATION = 48 hours;
|
||||
|
||||
address public gov;
|
||||
address public ytVault;
|
||||
address public usdy;
|
||||
address public ytLP;
|
||||
|
||||
uint256 public cooldownDuration;
|
||||
mapping(address => uint256) public lastAddedAt;
|
||||
|
||||
mapping(address => bool) public isHandler;
|
||||
|
||||
uint256 public aumAddition;
|
||||
uint256 public aumDeduction;
|
||||
|
||||
event AddLiquidity(
|
||||
address indexed account,
|
||||
address indexed token,
|
||||
uint256 amount,
|
||||
uint256 aumInUsdy,
|
||||
uint256 ytLPSupply,
|
||||
uint256 usdyAmount,
|
||||
uint256 mintAmount
|
||||
);
|
||||
event RemoveLiquidity(
|
||||
address indexed account,
|
||||
address indexed token,
|
||||
uint256 ytLPAmount,
|
||||
uint256 aumInUsdy,
|
||||
uint256 ytLPSupply,
|
||||
uint256 usdyAmount,
|
||||
uint256 amountOut
|
||||
);
|
||||
event CooldownDurationSet(uint256 duration);
|
||||
event HandlerSet(address indexed handler, bool isActive);
|
||||
|
||||
modifier onlyGov() {
|
||||
if (msg.sender != gov) revert Forbidden();
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyHandler() {
|
||||
if (!isHandler[msg.sender] && msg.sender != gov) revert Forbidden();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 初始化合约
|
||||
* @param _ytVault YTVault合约地址
|
||||
* @param _usdy USDY代币地址
|
||||
* @param _ytLP ytLP代币地址
|
||||
* @param _cooldownDuration 冷却时间(秒)
|
||||
*/
|
||||
function initialize(
|
||||
address _ytVault,
|
||||
address _usdy,
|
||||
address _ytLP,
|
||||
uint256 _cooldownDuration
|
||||
) external initializer {
|
||||
if (_ytVault == address(0) || _usdy == address(0) || _ytLP == address(0)) revert InvalidAddress();
|
||||
if (_cooldownDuration > MAX_COOLDOWN_DURATION) revert InvalidDuration();
|
||||
|
||||
__ReentrancyGuard_init();
|
||||
__UUPSUpgradeable_init();
|
||||
|
||||
gov = msg.sender;
|
||||
ytVault = _ytVault;
|
||||
usdy = _usdy;
|
||||
ytLP = _ytLP;
|
||||
cooldownDuration = _cooldownDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 授权升级(仅gov可调用)
|
||||
* @param newImplementation 新实现合约地址
|
||||
*/
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyGov {}
|
||||
|
||||
function setGov(address _gov) external onlyGov {
|
||||
if (_gov == address(0)) revert InvalidAddress();
|
||||
gov = _gov;
|
||||
}
|
||||
|
||||
function setHandler(address _handler, bool _isActive) external onlyGov {
|
||||
isHandler[_handler] = _isActive;
|
||||
emit HandlerSet(_handler, _isActive);
|
||||
}
|
||||
|
||||
function setCooldownDuration(uint256 _duration) external onlyGov {
|
||||
if (_duration > MAX_COOLDOWN_DURATION) revert InvalidDuration();
|
||||
cooldownDuration = _duration;
|
||||
emit CooldownDurationSet(_duration);
|
||||
}
|
||||
|
||||
function setAumAdjustment(uint256 _addition, uint256 _deduction) external onlyGov {
|
||||
aumAddition = _addition;
|
||||
aumDeduction = _deduction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 为指定账户添加流动性(Handler调用)
|
||||
*/
|
||||
function addLiquidityForAccount(
|
||||
address _fundingAccount,
|
||||
address _account,
|
||||
address _token,
|
||||
uint256 _amount,
|
||||
uint256 _minUsdy,
|
||||
uint256 _minYtLP
|
||||
) external onlyHandler nonReentrant returns (uint256) {
|
||||
return _addLiquidity(_fundingAccount, _account, _token, _amount, _minUsdy, _minYtLP);
|
||||
}
|
||||
|
||||
function _addLiquidity(
|
||||
address _fundingAccount,
|
||||
address _account,
|
||||
address _token,
|
||||
uint256 _amount,
|
||||
uint256 _minUsdy,
|
||||
uint256 _minYtLP
|
||||
) private returns (uint256) {
|
||||
if (_amount == 0) revert InvalidAmount();
|
||||
|
||||
uint256 aumInUsdy = getAumInUsdy(true);
|
||||
uint256 ytLPSupply = IERC20(ytLP).totalSupply();
|
||||
|
||||
IERC20(_token).safeTransferFrom(_fundingAccount, ytVault, _amount);
|
||||
uint256 usdyAmount = IYTVault(ytVault).buyUSDY(_token, address(this));
|
||||
if (usdyAmount < _minUsdy) revert InsufficientOutput();
|
||||
|
||||
uint256 mintAmount;
|
||||
if (ytLPSupply == 0) {
|
||||
mintAmount = usdyAmount;
|
||||
} else {
|
||||
mintAmount = usdyAmount * ytLPSupply / aumInUsdy;
|
||||
}
|
||||
|
||||
if (mintAmount < _minYtLP) revert InsufficientOutput();
|
||||
|
||||
IYTLPToken(ytLP).mint(_account, mintAmount);
|
||||
lastAddedAt[_account] = block.timestamp;
|
||||
|
||||
emit AddLiquidity(_account, _token, _amount, aumInUsdy, ytLPSupply, usdyAmount, mintAmount);
|
||||
|
||||
return mintAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 为指定账户移除流动性(Handler调用)
|
||||
*/
|
||||
function removeLiquidityForAccount(
|
||||
address _account,
|
||||
address _tokenOut,
|
||||
uint256 _ytLPAmount,
|
||||
uint256 _minOut,
|
||||
address _receiver
|
||||
) external onlyHandler nonReentrant returns (uint256) {
|
||||
return _removeLiquidity(_account, _tokenOut, _ytLPAmount, _minOut, _receiver);
|
||||
}
|
||||
|
||||
function _removeLiquidity(
|
||||
address _account,
|
||||
address _tokenOut,
|
||||
uint256 _ytLPAmount,
|
||||
uint256 _minOut,
|
||||
address _receiver
|
||||
) private returns (uint256) {
|
||||
if (_ytLPAmount == 0) revert InvalidAmount();
|
||||
|
||||
if (lastAddedAt[_account] + cooldownDuration > block.timestamp) revert CooldownNotPassed();
|
||||
|
||||
uint256 aumInUsdy = getAumInUsdy(false);
|
||||
uint256 ytLPSupply = IERC20(ytLP).totalSupply();
|
||||
|
||||
uint256 usdyAmount = _ytLPAmount * aumInUsdy / ytLPSupply;
|
||||
|
||||
// 先销毁ytLP
|
||||
IYTLPToken(ytLP).burn(_account, _ytLPAmount);
|
||||
|
||||
// 检查余额,只铸造差额部分
|
||||
uint256 usdyBalance = IERC20(usdy).balanceOf(address(this));
|
||||
if (usdyAmount > usdyBalance) {
|
||||
IUSDY(usdy).mint(address(this), usdyAmount - usdyBalance);
|
||||
}
|
||||
|
||||
// 转账USDY到Vault并换回代币
|
||||
IERC20(usdy).safeTransfer(ytVault, usdyAmount);
|
||||
uint256 amountOut = IYTVault(ytVault).sellUSDY(_tokenOut, _receiver);
|
||||
|
||||
if (amountOut < _minOut) revert InsufficientOutput();
|
||||
|
||||
emit RemoveLiquidity(_account, _tokenOut, _ytLPAmount, aumInUsdy, ytLPSupply, usdyAmount, amountOut);
|
||||
|
||||
return amountOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取ytLP价格
|
||||
* @param _maximise 是否取最大值
|
||||
* @return ytLP价格(18位精度)
|
||||
*/
|
||||
function getPrice(bool _maximise) external view returns (uint256) {
|
||||
uint256 aum = getAumInUsdy(_maximise);
|
||||
uint256 supply = IERC20(ytLP).totalSupply();
|
||||
|
||||
if (supply == 0) return YTLP_PRECISION;
|
||||
|
||||
return aum * YTLP_PRECISION / supply;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取池子总价值(AUM)
|
||||
* @param _maximise true=使用最大价格(添加流动性时), false=使用最小价格(移除流动性时)
|
||||
* @return USDY计价的总价值
|
||||
*/
|
||||
function getAumInUsdy(bool _maximise) public view returns (uint256) {
|
||||
uint256 aum = IYTVault(ytVault).getPoolValue(_maximise);
|
||||
|
||||
aum += aumAddition;
|
||||
if (aum > aumDeduction) {
|
||||
aum -= aumDeduction;
|
||||
} else {
|
||||
aum = 0;
|
||||
}
|
||||
|
||||
return aum;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev 预留存储空间,用于未来升级时添加新的状态变量
|
||||
* 50个slot = 50 * 32 bytes = 1600 bytes
|
||||
*/
|
||||
uint256[50] private __gap;
|
||||
}
|
||||
|
||||
314
contracts/ytLp/core/YTPriceFeed.sol
Normal file
314
contracts/ytLp/core/YTPriceFeed.sol
Normal file
@@ -0,0 +1,314 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "../../interfaces/IYTToken.sol";
|
||||
|
||||
/**
|
||||
* @title YTPriceFeed
|
||||
* @notice 价格读取器,直接从YT合约读取价格变量(带保护机制和价差)
|
||||
* @dev UUPS可升级合约
|
||||
*/
|
||||
contract YTPriceFeed is Initializable, UUPSUpgradeable {
|
||||
|
||||
error Forbidden();
|
||||
error MaxChangeTooHigh();
|
||||
error PriceChangeTooLarge();
|
||||
error SpreadTooHigh();
|
||||
error InvalidAddress();
|
||||
|
||||
address public gov;
|
||||
|
||||
uint256 public constant PRICE_PRECISION = 10 ** 30;
|
||||
uint256 public constant BASIS_POINTS_DIVISOR = 10000;
|
||||
uint256 public constant MAX_SPREAD_BASIS_POINTS = 200; // 最大2%价差
|
||||
|
||||
// WUSD固定价格
|
||||
address public wusdAddress;
|
||||
|
||||
// WUSD价格来源
|
||||
address public wusdPriceSource;
|
||||
|
||||
// 价格保护参数
|
||||
uint256 public maxPriceChangeBps; // 5% 最大价格变动
|
||||
|
||||
// 价差配置(每个代币可以有不同的价差)
|
||||
mapping(address => uint256) public spreadBasisPoints;
|
||||
|
||||
// 价格历史记录
|
||||
mapping(address => uint256) public lastPrice;
|
||||
|
||||
// 价格更新权限
|
||||
mapping(address => bool) public isKeeper;
|
||||
|
||||
event PriceUpdate(address indexed token, uint256 oldPrice, uint256 newPrice, uint256 timestamp);
|
||||
event SpreadUpdate(address indexed token, uint256 spreadBps);
|
||||
event KeeperSet(address indexed keeper, bool isActive);
|
||||
|
||||
modifier onlyGov() {
|
||||
if (msg.sender != gov) revert Forbidden();
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyKeeper() {
|
||||
if (!isKeeper[msg.sender] && msg.sender != gov) revert Forbidden();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 初始化合约
|
||||
*/
|
||||
function initialize(address _wusdAddress) external initializer {
|
||||
__UUPSUpgradeable_init();
|
||||
if (_wusdAddress == address(0)) revert InvalidAddress();
|
||||
wusdAddress = _wusdAddress;
|
||||
gov = msg.sender;
|
||||
maxPriceChangeBps = 500; // 5% 最大价格变动
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 授权升级(仅gov可调用)
|
||||
* @param newImplementation 新实现合约地址
|
||||
*/
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyGov {}
|
||||
|
||||
/**
|
||||
* @notice 设置WUSD价格来源(YTAssetVault地址)
|
||||
* @param _wusdPriceSource YTAssetVault合约地址
|
||||
*/
|
||||
function setWusdPriceSource(address _wusdPriceSource) external onlyGov {
|
||||
wusdPriceSource = _wusdPriceSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 设置keeper权限
|
||||
* @param _keeper keeper地址
|
||||
* @param _isActive 是否激活
|
||||
*/
|
||||
function setKeeper(address _keeper, bool _isActive) external onlyGov {
|
||||
isKeeper[_keeper] = _isActive;
|
||||
emit KeeperSet(_keeper, _isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 设置最大价格变动百分比
|
||||
* @param _maxPriceChangeBps 最大变动(基点)
|
||||
*/
|
||||
function setMaxPriceChangeBps(uint256 _maxPriceChangeBps) external onlyGov {
|
||||
if (_maxPriceChangeBps > 2000) revert MaxChangeTooHigh(); // 最大20%
|
||||
maxPriceChangeBps = _maxPriceChangeBps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 设置代币价差
|
||||
* @param _token 代币地址
|
||||
* @param _spreadBasisPoints 价差(基点)例如:10 = 0.1%, 100 = 1%
|
||||
*/
|
||||
function setSpreadBasisPoints(address _token, uint256 _spreadBasisPoints) external onlyGov {
|
||||
if (_spreadBasisPoints > MAX_SPREAD_BASIS_POINTS) revert SpreadTooHigh();
|
||||
spreadBasisPoints[_token] = _spreadBasisPoints;
|
||||
emit SpreadUpdate(_token, _spreadBasisPoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 批量设置代币价差
|
||||
* @param _tokens 代币地址数组
|
||||
* @param _spreadBasisPoints 价差数组
|
||||
*/
|
||||
function setSpreadBasisPointsForMultiple(
|
||||
address[] calldata _tokens,
|
||||
uint256[] calldata _spreadBasisPoints
|
||||
) external onlyGov {
|
||||
require(_tokens.length == _spreadBasisPoints.length, "length mismatch");
|
||||
for (uint256 i = 0; i < _tokens.length; i++) {
|
||||
if (_spreadBasisPoints[i] > MAX_SPREAD_BASIS_POINTS) revert SpreadTooHigh();
|
||||
spreadBasisPoints[_tokens[i]] = _spreadBasisPoints[i];
|
||||
emit SpreadUpdate(_tokens[i], _spreadBasisPoints[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 强制更新价格(紧急情况)
|
||||
* @param _token 代币地址
|
||||
* @param _price 新价格
|
||||
*/
|
||||
function forceUpdatePrice(address _token, uint256 _price) external onlyGov {
|
||||
uint256 oldPrice = lastPrice[_token];
|
||||
lastPrice[_token] = _price;
|
||||
emit PriceUpdate(_token, oldPrice, _price, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取YT代币价格(带波动保护和价差)
|
||||
* @param _token 代币地址
|
||||
* @param _maximise true=最大价格(上浮价差,对协议有利), false=最小价格(下压价差,对协议有利)
|
||||
* @return 价格(30位精度)
|
||||
*
|
||||
* 使用场景:
|
||||
* - 添加流动性时AUM计算:_maximise=true(高估AUM,用户获得较少LP)
|
||||
* - 移除流动性时AUM计算:_maximise=false(低估AUM,用户获得较少代币)
|
||||
* - buyUSDY时(用户卖代币):_maximise=false(低估用户代币价值)
|
||||
* - sellUSDY时(用户买代币):_maximise=true(高估需支付的代币价值)
|
||||
* - swap时tokenIn:_maximise=false(低估输入)
|
||||
* - swap时tokenOut:_maximise=true(高估输出)
|
||||
*/
|
||||
function getPrice(address _token, bool _maximise) external view returns (uint256) {
|
||||
if (_token == wusdAddress) {
|
||||
return _getWUSDPrice();
|
||||
}
|
||||
|
||||
uint256 basePrice = _getRawPrice(_token);
|
||||
|
||||
// 价格波动检查
|
||||
_validatePriceChange(_token, basePrice);
|
||||
|
||||
// 应用价差
|
||||
return _applySpread(_token, basePrice, _maximise);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 更新价格并返回(由keeper调用)
|
||||
* @param _token 代币地址
|
||||
* @return 新价格
|
||||
*/
|
||||
function updatePrice(address _token) external onlyKeeper returns (uint256) {
|
||||
if (_token == wusdAddress) {
|
||||
return _getWUSDPrice();
|
||||
}
|
||||
|
||||
uint256 oldPrice = lastPrice[_token];
|
||||
uint256 newPrice = _getRawPrice(_token);
|
||||
|
||||
// 价格波动检查
|
||||
_validatePriceChange(_token, newPrice);
|
||||
|
||||
lastPrice[_token] = newPrice;
|
||||
|
||||
emit PriceUpdate(_token, oldPrice, newPrice, block.timestamp);
|
||||
|
||||
return newPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 直接读取YT代币的ytPrice变量
|
||||
*/
|
||||
function _getRawPrice(address _token) private view returns (uint256) {
|
||||
return IYTToken(_token).ytPrice();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 从配置的YTAssetVault读取wusdPrice
|
||||
* @dev 如果未设置wusdPriceSource,返回固定价格1.0
|
||||
*/
|
||||
function _getWUSDPrice() private view returns (uint256) {
|
||||
if (wusdPriceSource == address(0)) {
|
||||
return PRICE_PRECISION; // 默认1.0
|
||||
}
|
||||
return IYTToken(wusdPriceSource).wusdPrice();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 应用价差
|
||||
* @param _token 代币地址
|
||||
* @param _basePrice 基础价格
|
||||
* @param _maximise true=上浮价格,false=下压价格
|
||||
* @return 应用价差后的价格
|
||||
*/
|
||||
function _applySpread(
|
||||
address _token,
|
||||
uint256 _basePrice,
|
||||
bool _maximise
|
||||
) private view returns (uint256) {
|
||||
uint256 spread = spreadBasisPoints[_token];
|
||||
|
||||
// 如果没有设置价差,直接返回基础价格
|
||||
if (spread == 0) {
|
||||
return _basePrice;
|
||||
}
|
||||
|
||||
if (_maximise) {
|
||||
// 上浮价格:basePrice * (1 + spread%)
|
||||
return _basePrice * (BASIS_POINTS_DIVISOR + spread) / BASIS_POINTS_DIVISOR;
|
||||
} else {
|
||||
// 下压价格:basePrice * (1 - spread%)
|
||||
return _basePrice * (BASIS_POINTS_DIVISOR - spread) / BASIS_POINTS_DIVISOR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 验证价格变动是否在允许范围内
|
||||
*/
|
||||
function _validatePriceChange(address _token, uint256 _newPrice) private view {
|
||||
uint256 oldPrice = lastPrice[_token];
|
||||
|
||||
// 首次设置价格,跳过检查
|
||||
if (oldPrice == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算价格变动百分比
|
||||
uint256 priceDiff = _newPrice > oldPrice ? _newPrice - oldPrice : oldPrice - _newPrice;
|
||||
uint256 maxDiff = oldPrice * maxPriceChangeBps / BASIS_POINTS_DIVISOR;
|
||||
|
||||
if (priceDiff > maxDiff) revert PriceChangeTooLarge();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取价格详细信息
|
||||
*/
|
||||
function getPriceInfo(address _token) external view returns (
|
||||
uint256 currentPrice,
|
||||
uint256 cachedPrice,
|
||||
uint256 maxPrice,
|
||||
uint256 minPrice,
|
||||
uint256 spread
|
||||
) {
|
||||
if (_token == wusdAddress) {
|
||||
uint256 wusdPrice = _getWUSDPrice();
|
||||
currentPrice = wusdPrice;
|
||||
cachedPrice = wusdPrice;
|
||||
maxPrice = wusdPrice;
|
||||
minPrice = wusdPrice;
|
||||
spread = 0;
|
||||
} else {
|
||||
currentPrice = _getRawPrice(_token);
|
||||
cachedPrice = lastPrice[_token];
|
||||
spread = spreadBasisPoints[_token];
|
||||
maxPrice = _applySpread(_token, currentPrice, true);
|
||||
minPrice = _applySpread(_token, currentPrice, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取最大价格(上浮价差)
|
||||
*/
|
||||
function getMaxPrice(address _token) external view returns (uint256) {
|
||||
if (_token == wusdAddress) {
|
||||
// WUSD通常不需要价差,直接返回原价格
|
||||
return _getWUSDPrice();
|
||||
}
|
||||
uint256 basePrice = _getRawPrice(_token);
|
||||
_validatePriceChange(_token, basePrice);
|
||||
return _applySpread(_token, basePrice, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取最小价格(下压价差)
|
||||
*/
|
||||
function getMinPrice(address _token) external view returns (uint256) {
|
||||
if (_token == wusdAddress) {
|
||||
// WUSD通常不需要价差,直接返回原价格
|
||||
return _getWUSDPrice();
|
||||
}
|
||||
uint256 basePrice = _getRawPrice(_token);
|
||||
_validatePriceChange(_token, basePrice);
|
||||
return _applySpread(_token, basePrice, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev 预留存储空间,用于未来升级时添加新的状态变量
|
||||
* 50个slot = 50 * 32 bytes = 1600 bytes
|
||||
*/
|
||||
uint256[50] private __gap;
|
||||
}
|
||||
216
contracts/ytLp/core/YTRewardRouter.sol
Normal file
216
contracts/ytLp/core/YTRewardRouter.sol
Normal file
@@ -0,0 +1,216 @@
|
||||
// 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-upgradeable/utils/PausableUpgradeable.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../../interfaces/IYTPoolManager.sol";
|
||||
import "../../interfaces/IYTVault.sol";
|
||||
|
||||
/**
|
||||
* @title YTRewardRouter
|
||||
* @notice 用户交互入口
|
||||
* @dev UUPS可升级合约
|
||||
*/
|
||||
contract YTRewardRouter is Initializable, UUPSUpgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
error Forbidden();
|
||||
error AlreadyInitialized();
|
||||
error InvalidAddress();
|
||||
error InvalidAmount();
|
||||
error InsufficientOutput();
|
||||
|
||||
address public gov;
|
||||
address public usdy;
|
||||
address public ytLP;
|
||||
address public ytPoolManager;
|
||||
address public ytVault;
|
||||
|
||||
event Swap(
|
||||
address indexed account,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountIn,
|
||||
uint256 amountOut
|
||||
);
|
||||
|
||||
modifier onlyGov() {
|
||||
if (msg.sender != gov) revert Forbidden();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 初始化合约
|
||||
* @param _usdy USDY代币地址
|
||||
* @param _ytLP ytLP代币地址
|
||||
* @param _ytPoolManager YTPoolManager地址
|
||||
* @param _ytVault YTVault地址
|
||||
*/
|
||||
function initialize(
|
||||
address _usdy,
|
||||
address _ytLP,
|
||||
address _ytPoolManager,
|
||||
address _ytVault
|
||||
) external initializer {
|
||||
if (_usdy == address(0)) revert InvalidAddress();
|
||||
if (_ytLP == address(0)) revert InvalidAddress();
|
||||
if (_ytPoolManager == address(0)) revert InvalidAddress();
|
||||
if (_ytVault == address(0)) revert InvalidAddress();
|
||||
|
||||
__ReentrancyGuard_init();
|
||||
__UUPSUpgradeable_init();
|
||||
__Pausable_init();
|
||||
|
||||
gov = msg.sender;
|
||||
|
||||
|
||||
usdy = _usdy;
|
||||
ytLP = _ytLP;
|
||||
ytPoolManager = _ytPoolManager;
|
||||
ytVault = _ytVault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 授权升级(仅gov可调用)
|
||||
* @param newImplementation 新实现合约地址
|
||||
*/
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyGov {}
|
||||
|
||||
/**
|
||||
* @notice 暂停合约(仅gov可调用)
|
||||
* @dev 暂停后,所有资金流动操作将被禁止
|
||||
*/
|
||||
function pause() external onlyGov {
|
||||
_pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 恢复合约(仅gov可调用)
|
||||
*/
|
||||
function unpause() external onlyGov {
|
||||
_unpause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 添加流动性
|
||||
* @param _token YT代币或WUSD地址
|
||||
* @param _amount 代币数量
|
||||
* @param _minUsdy 最小USDY数量
|
||||
* @param _minYtLP 最小ytLP数量
|
||||
* @return ytLPAmount 获得的ytLP数量
|
||||
*/
|
||||
function addLiquidity(
|
||||
address _token,
|
||||
uint256 _amount,
|
||||
uint256 _minUsdy,
|
||||
uint256 _minYtLP
|
||||
) external nonReentrant whenNotPaused returns (uint256) {
|
||||
if (_amount == 0) revert InvalidAmount();
|
||||
|
||||
address account = msg.sender;
|
||||
|
||||
IERC20(_token).safeTransferFrom(account, address(this), _amount);
|
||||
IERC20(_token).approve(ytPoolManager, _amount);
|
||||
|
||||
uint256 ytLPAmount = IYTPoolManager(ytPoolManager).addLiquidityForAccount(
|
||||
address(this),
|
||||
account,
|
||||
_token,
|
||||
_amount,
|
||||
_minUsdy,
|
||||
_minYtLP
|
||||
);
|
||||
|
||||
return ytLPAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 移除流动性
|
||||
* @param _tokenOut 输出代币地址
|
||||
* @param _ytLPAmount ytLP数量
|
||||
* @param _minOut 最小输出数量
|
||||
* @param _receiver 接收地址
|
||||
* @return amountOut 获得的代币数量
|
||||
*/
|
||||
function removeLiquidity(
|
||||
address _tokenOut,
|
||||
uint256 _ytLPAmount,
|
||||
uint256 _minOut,
|
||||
address _receiver
|
||||
) external nonReentrant whenNotPaused returns (uint256) {
|
||||
if (_ytLPAmount == 0) revert InvalidAmount();
|
||||
|
||||
address account = msg.sender;
|
||||
|
||||
uint256 amountOut = IYTPoolManager(ytPoolManager).removeLiquidityForAccount(
|
||||
account,
|
||||
_tokenOut,
|
||||
_ytLPAmount,
|
||||
_minOut,
|
||||
_receiver
|
||||
);
|
||||
|
||||
return amountOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice YT代币互换
|
||||
* @param _tokenIn 输入代币地址
|
||||
* @param _tokenOut 输出代币地址
|
||||
* @param _amountIn 输入数量
|
||||
* @param _minOut 最小输出数量
|
||||
* @param _receiver 接收地址
|
||||
* @return amountOut 获得的代币数量
|
||||
*/
|
||||
function swapYT(
|
||||
address _tokenIn,
|
||||
address _tokenOut,
|
||||
uint256 _amountIn,
|
||||
uint256 _minOut,
|
||||
address _receiver
|
||||
) external nonReentrant whenNotPaused returns (uint256) {
|
||||
if (_amountIn == 0) revert InvalidAmount();
|
||||
|
||||
address account = msg.sender;
|
||||
|
||||
IERC20(_tokenIn).safeTransferFrom(account, ytVault, _amountIn);
|
||||
|
||||
uint256 amountOut = IYTVault(ytVault).swap(_tokenIn, _tokenOut, _receiver);
|
||||
|
||||
if (amountOut < _minOut) revert InsufficientOutput();
|
||||
|
||||
emit Swap(account, _tokenIn, _tokenOut, _amountIn, amountOut);
|
||||
|
||||
return amountOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取ytLP价格
|
||||
* @return ytLP价格(18位精度)
|
||||
*/
|
||||
function getYtLPPrice() external view returns (uint256) {
|
||||
return IYTPoolManager(ytPoolManager).getPrice(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取账户价值
|
||||
* @param _account 账户地址
|
||||
* @return 账户持有的ytLP价值(USDY计价)
|
||||
*/
|
||||
function getAccountValue(address _account) external view returns (uint256) {
|
||||
uint256 ytLPBalance = IERC20(ytLP).balanceOf(_account);
|
||||
uint256 ytLPPrice = IYTPoolManager(ytPoolManager).getPrice(true);
|
||||
return ytLPBalance * ytLPPrice / (10 ** 18);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev 预留存储空间,用于未来升级时添加新的状态变量
|
||||
* 50个slot = 50 * 32 bytes = 1600 bytes
|
||||
*/
|
||||
uint256[50] private __gap;
|
||||
}
|
||||
|
||||
626
contracts/ytLp/core/YTVault.sol
Normal file
626
contracts/ytLp/core/YTVault.sol
Normal file
@@ -0,0 +1,626 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* @title YTVault
|
||||
* @notice 核心资金池,处理YT代币的存储、交换和动态手续费
|
||||
* @dev UUPS可升级合约
|
||||
*/
|
||||
contract YTVault is Initializable, UUPSUpgradeable, ReentrancyGuardUpgradeable {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
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; // 授权的swap调用者
|
||||
|
||||
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; // 跟踪实际代币余额
|
||||
|
||||
// USDY债务追踪(用于动态手续费)
|
||||
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; // 10% 最大滑点
|
||||
|
||||
// 单笔交易限额
|
||||
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);
|
||||
|
||||
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();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 初始化合约
|
||||
* @param _usdy USDY代币地址
|
||||
* @param _priceFeed 价格预言机地址
|
||||
*/
|
||||
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; // 10% 最大滑点
|
||||
|
||||
// 将 USDY 标记为稳定币,这样 USDY ↔ 稳定币的互换可以享受低费率
|
||||
stableTokens[_usdy] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 授权升级(仅gov可调用)
|
||||
* @param newImplementation 新实现合约地址
|
||||
*/
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyGov {}
|
||||
|
||||
function setGov(address _gov) external onlyGov {
|
||||
if (_gov == address(0)) revert InvalidAddress();
|
||||
gov = _gov;
|
||||
}
|
||||
|
||||
function setPoolManager(address _manager) external onlyGov {
|
||||
if (_manager == address(0)) revert InvalidAddress();
|
||||
ytPoolManager = _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(); // 最大20%
|
||||
maxSwapSlippageBps = _slippageBps;
|
||||
}
|
||||
|
||||
function setMaxSwapAmount(address _token, uint256 _amount) external onlyGov {
|
||||
maxSwapAmount[_token] = _amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 用YT代币购买USDY(添加流动性时调用)
|
||||
* @param _token YT代币地址
|
||||
* @param _receiver USDY接收地址
|
||||
* @return usdyAmountAfterFees 实际获得的USDY数量
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 用USDY卖出换取YT代币(移除流动性时调用)
|
||||
* @param _token YT代币地址
|
||||
* @param _receiver YT代币接收地址
|
||||
* @return amountOutAfterFees 实际获得的YT代币数量
|
||||
*/
|
||||
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();
|
||||
|
||||
// 计算实际转出的代币对应的USDY价值(用于减少usdyAmount记账)
|
||||
uint256 usdyAmountOut = amountOut * price / PRICE_PRECISION;
|
||||
usdyAmountOut = _adjustForDecimals(usdyAmountOut, _token, usdy);
|
||||
|
||||
// 手续费留在池子:只减少实际转出的部分
|
||||
_decreasePoolAmount(_token, amountOut);
|
||||
_decreaseUsdyAmount(_token, usdyAmountOut);
|
||||
|
||||
// 销毁USDY
|
||||
IUSDY(usdy).burn(address(this), usdyAmount);
|
||||
|
||||
// 转出代币
|
||||
IERC20(_token).safeTransfer(_receiver, amountOut);
|
||||
_updateTokenBalance(_token);
|
||||
|
||||
emit RemoveLiquidity(_receiver, _token, usdyAmount, amountOut);
|
||||
|
||||
return amountOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice YT代币互换
|
||||
* @param _tokenIn 输入代币地址
|
||||
* @param _tokenOut 输出代币地址
|
||||
* @param _receiver 接收地址
|
||||
* @return amountOutAfterFees 实际获得的输出代币数量
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取代币价格(带价差)
|
||||
* @param _token 代币地址
|
||||
* @param _maximise true=最大价格, false=最小价格
|
||||
* @return 价格(30位精度)
|
||||
*/
|
||||
function getPrice(address _token, bool _maximise) external view returns (uint256) {
|
||||
return _getPrice(_token, _maximise);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取最大价格
|
||||
*/
|
||||
function getMaxPrice(address _token) external view returns (uint256) {
|
||||
return _getPrice(_token, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取最小价格
|
||||
*/
|
||||
function getMinPrice(address _token) external view returns (uint256) {
|
||||
return _getPrice(_token, false);
|
||||
}
|
||||
|
||||
function getAllPoolTokens() external view returns (address[] memory) {
|
||||
return allWhitelistedTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取池子总价值
|
||||
* @param _maximise true=使用最大价格(对协议有利), false=使用最小价格(对用户有利)
|
||||
* @return 池子总价值(USDY计价)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice 获取swap手续费率(公开方法,供前端调用)
|
||||
* @param _tokenIn 输入代币
|
||||
* @param _tokenOut 输出代币
|
||||
* @param _usdyAmount USDY数量
|
||||
* @return 手续费率(basis points)
|
||||
*/
|
||||
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) {
|
||||
// 稳定币交换是指两个代币都是稳定币(如 WUSD <-> USDC)
|
||||
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 averageDiff = (initialDiff + nextDiff) / 2;
|
||||
if (averageDiff > targetAmount) {
|
||||
averageDiff = targetAmount;
|
||||
}
|
||||
uint256 taxBps = _taxBasisPoints * averageDiff / targetAmount;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev 预留存储空间,用于未来升级时添加新的状态变量
|
||||
* 50个slot = 50 * 32 bytes = 1600 bytes
|
||||
*/
|
||||
uint256[50] private __gap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user