1955 lines
61 KiB
Markdown
1955 lines
61 KiB
Markdown
|
|
# YT流动性池合约设计方案
|
|||
|
|
## 基于GMX GLP核心概念的YT代币流动性池系统
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 项目概述
|
|||
|
|
|
|||
|
|
### 1.1 设计目标
|
|||
|
|
|
|||
|
|
基于GMX GLP原理,设计一个支持多种YT(Yield Token)代币的流动性池系统。
|
|||
|
|
|
|||
|
|
**核心特点**:
|
|||
|
|
- **一篮子YT代币**: YT-A, YT-B, YT-C等(不同的收益代币)
|
|||
|
|
- **WUSD代币支持**: 支持WUSD稳定币(地址: `0x7Cd017ca5ddb86861FA983a34b5F495C6F898c41`)
|
|||
|
|
- **统一底层**: 所有YT都基于USDC
|
|||
|
|
- **LP代币(ytLP)**: 代表用户在池子中的份额
|
|||
|
|
- **Swap功能**: YT-A ↔ YT-B ↔ YT-C ↔ WUSD
|
|||
|
|
- **计价单位**: USDY(完全模拟GMX的USDG)
|
|||
|
|
- **动态手续费**: 根据池子占比自动调整手续费
|
|||
|
|
- **自动复利**: 手续费直接留在池子中,ytLP价值自动增长
|
|||
|
|
|
|||
|
|
### 1.2 架构对比
|
|||
|
|
|
|||
|
|
| 特性 | GMX GLP | YT流动性池 |
|
|||
|
|
|------|---------|-----------|
|
|||
|
|
| **资产类型** | 多种加密资产 | 多种YT代币 + WUSD |
|
|||
|
|
| **代币示例** | ETH, WBTC, USDC, USDT | YT-A, YT-B, YT-C, WUSD |
|
|||
|
|
| **底层资产** | 各不相同 | YT代币(YT本身基于USDC) |
|
|||
|
|
| **主要用途** | 为永续合约提供流动性 | YT代币互换 |
|
|||
|
|
| **计价单位** | USDG | USDY |
|
|||
|
|
| **动态手续费** | ✅ | ✅ |
|
|||
|
|
| **收益来源** | 手续费 + 交易者损失 | 仅Swap手续费 |
|
|||
|
|
| **LP代币** | GLP | ytLP |
|
|||
|
|
|
|||
|
|
### 1.3 三层架构说明
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
层次1: YT流动性池
|
|||
|
|
├─ 底层资产:YT-A, YT-B, YT-C, WUSD(池子实际持有的)
|
|||
|
|
├─ 计价单位:USDY
|
|||
|
|
└─ LP代币:ytLP
|
|||
|
|
|
|||
|
|
层次2: YT代币本身
|
|||
|
|
├─ YT-A:ERC4626代币,本金是USDC
|
|||
|
|
├─ YT-B:ERC4626代币,本金是USDC
|
|||
|
|
└─ YT-C:ERC4626代币,本金是USDC
|
|||
|
|
|
|||
|
|
层次3: 底层资产
|
|||
|
|
└─ USDC:所有YT的本金资产
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 系统架构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────┐
|
|||
|
|
│ 用户交互层 │
|
|||
|
|
│ YTRewardRouter.sol │
|
|||
|
|
│ (addLiquidity / removeLiquidity) │
|
|||
|
|
└──────────────┬──────────────────────────┘
|
|||
|
|
│
|
|||
|
|
┌──────────┴─────────────┐
|
|||
|
|
▼ ▼
|
|||
|
|
┌───────────────┐ ┌──────────────────┐
|
|||
|
|
│ YTPoolManager │◄────│ YTVault │
|
|||
|
|
│ (流动性管理) │ │ (资金池+动态费率)│
|
|||
|
|
└───────┬───────┘ └──────┬───────────┘
|
|||
|
|
│ │
|
|||
|
|
│ ├──► YT-A (40%)
|
|||
|
|
│ ├──► YT-B (30%)
|
|||
|
|
│ ├──► YT-C (20%)
|
|||
|
|
│ └──► WUSD (10%)
|
|||
|
|
▼
|
|||
|
|
┌───────────────┐
|
|||
|
|
│ ytLP Token │
|
|||
|
|
│ (ERC20) │
|
|||
|
|
│ 持有即增值 │
|
|||
|
|
└───────────────┘
|
|||
|
|
|
|||
|
|
辅助合约:
|
|||
|
|
├─ USDY.sol (统一计价代币)
|
|||
|
|
└─ YTPriceFeed.sol (价格读取器)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 核心合约设计
|
|||
|
|
|
|||
|
|
### 3.1 USDY Token (USDY.sol)
|
|||
|
|
|
|||
|
|
**功能**: 统一计价代币,完全模拟GMX的USDG
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
pragma solidity ^0.8.0;
|
|||
|
|
|
|||
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|||
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|||
|
|
|
|||
|
|
contract USDY is ERC20, Ownable {
|
|||
|
|
mapping(address => bool) public vaults;
|
|||
|
|
|
|||
|
|
event VaultAdded(address indexed vault);
|
|||
|
|
event VaultRemoved(address indexed vault);
|
|||
|
|
|
|||
|
|
modifier onlyVault() {
|
|||
|
|
require(vaults[msg.sender], "USDY: forbidden");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
constructor(address _vault) ERC20("YT USD", "USDY") {
|
|||
|
|
vaults[_vault] = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addVault(address _vault) external onlyOwner {
|
|||
|
|
vaults[_vault] = true;
|
|||
|
|
emit VaultAdded(_vault);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function removeVault(address _vault) external onlyOwner {
|
|||
|
|
vaults[_vault] = false;
|
|||
|
|
emit VaultRemoved(_vault);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mint(address _account, uint256 _amount) external onlyVault {
|
|||
|
|
_mint(_account, _amount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function burn(address _account, uint256 _amount) external onlyVault {
|
|||
|
|
_burn(_account, _amount);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**关键特性**:
|
|||
|
|
- 授权铸造:只有授权合约可以mint/burn
|
|||
|
|
- ERC20标准:可在合约间转移
|
|||
|
|
- 价值传递:作为Vault和PoolManager之间的中介
|
|||
|
|
- 计价单位:1 USDY ≈ $1
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.2 ytLP Token (YTLPToken.sol)
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
pragma solidity ^0.8.0;
|
|||
|
|
|
|||
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|||
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|||
|
|
|
|||
|
|
contract YTLPToken is ERC20, Ownable {
|
|||
|
|
mapping(address => bool) public isMinter;
|
|||
|
|
|
|||
|
|
constructor() ERC20("YT Liquidity Provider", "ytLP") {}
|
|||
|
|
|
|||
|
|
modifier onlyMinter() {
|
|||
|
|
require(isMinter[msg.sender], "YTLPToken: not minter");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setMinter(address _minter, bool _isActive) external onlyOwner {
|
|||
|
|
isMinter[_minter] = _isActive;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mint(address _to, uint256 _amount) external onlyMinter {
|
|||
|
|
_mint(_to, _amount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function burn(address _from, uint256 _amount) external onlyMinter {
|
|||
|
|
_burn(_from, _amount);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.3 YT Vault (YTVault.sol)
|
|||
|
|
|
|||
|
|
**功能**: 核心资金池,处理YT代币的存储、交换和手续费
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
pragma solidity ^0.8.0;
|
|||
|
|
|
|||
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|||
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|||
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|||
|
|
|
|||
|
|
contract YTVault is ReentrancyGuard {
|
|||
|
|
using SafeERC20 for IERC20;
|
|||
|
|
|
|||
|
|
// ========== 常量 ==========
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
bool public isSwapEnabled = true;
|
|||
|
|
bool public emergencyMode = false;
|
|||
|
|
|
|||
|
|
// 代币白名单
|
|||
|
|
address[] public allWhitelistedTokens;
|
|||
|
|
mapping(address => bool) public whitelistedTokens;
|
|||
|
|
mapping(address => uint256) public tokenDecimals;
|
|||
|
|
mapping(address => uint256) public tokenWeights;
|
|||
|
|
uint256 public totalTokenWeights;
|
|||
|
|
|
|||
|
|
// 池子资产
|
|||
|
|
mapping(address => uint256) public poolAmounts;
|
|||
|
|
mapping(address => uint256) public bufferAmounts;
|
|||
|
|
mapping(address => uint256) public reservedAmounts;
|
|||
|
|
mapping(address => uint256) public tokenBalances; // 跟踪实际代币余额
|
|||
|
|
|
|||
|
|
// USDY债务追踪(用于动态手续费)
|
|||
|
|
mapping(address => uint256) public usdyAmounts;
|
|||
|
|
mapping(address => uint256) public maxUsdyAmounts;
|
|||
|
|
|
|||
|
|
// 手续费配置
|
|||
|
|
uint256 public swapFeeBasisPoints = 30;
|
|||
|
|
uint256 public stableSwapFeeBasisPoints = 4;
|
|||
|
|
uint256 public taxBasisPoints = 50;
|
|||
|
|
uint256 public stableTaxBasisPoints = 20;
|
|||
|
|
bool public hasDynamicFees = true;
|
|||
|
|
|
|||
|
|
// 全局滑点保护
|
|||
|
|
uint256 public maxSwapSlippageBps = 1000; // 10% 最大滑点
|
|||
|
|
|
|||
|
|
// 单笔交易限额
|
|||
|
|
mapping(address => uint256) public maxSwapAmount;
|
|||
|
|
|
|||
|
|
// 断路器参数
|
|||
|
|
uint256 public dailySwapLimit;
|
|||
|
|
uint256 public currentDaySwapVolume;
|
|||
|
|
uint256 public lastSwapDay;
|
|||
|
|
|
|||
|
|
// ========== 事件 ==========
|
|||
|
|
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 wusdAmount);
|
|||
|
|
event RemoveLiquidity(address indexed account, address indexed token, uint256 wusdAmount, uint256 amountOut);
|
|||
|
|
|
|||
|
|
// ========== 修饰符 ==========
|
|||
|
|
modifier onlyGov() {
|
|||
|
|
require(msg.sender == gov, "YTVault: forbidden");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
modifier onlyPoolManager() {
|
|||
|
|
require(msg.sender == ytPoolManager, "YTVault: only pool manager");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
modifier notInEmergency() {
|
|||
|
|
require(!emergencyMode, "YTVault: emergency mode");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 构造函数 ==========
|
|||
|
|
constructor(address _usdy, address _priceFeed) {
|
|||
|
|
gov = msg.sender;
|
|||
|
|
usdy = _usdy;
|
|||
|
|
priceFeed = _priceFeed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 管理函数 ==========
|
|||
|
|
|
|||
|
|
function setPoolManager(address _manager) external onlyGov {
|
|||
|
|
ytPoolManager = _manager;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setWhitelistedToken(
|
|||
|
|
address _token,
|
|||
|
|
uint256 _decimals,
|
|||
|
|
uint256 _weight,
|
|||
|
|
uint256 _maxUsdyAmount
|
|||
|
|
) external onlyGov {
|
|||
|
|
if (!whitelistedTokens[_token]) {
|
|||
|
|
allWhitelistedTokens.push(_token);
|
|||
|
|
whitelistedTokens[_token] = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
totalTokenWeights = totalTokenWeights - tokenWeights[_token] + _weight;
|
|||
|
|
tokenDecimals[_token] = _decimals;
|
|||
|
|
tokenWeights[_token] = _weight;
|
|||
|
|
maxUsdyAmounts[_token] = _maxUsdyAmount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearWhitelistedToken(address _token) external onlyGov {
|
|||
|
|
require(whitelistedTokens[_token], "YTVault: token not whitelisted");
|
|||
|
|
totalTokenWeights = totalTokenWeights - tokenWeights[_token];
|
|||
|
|
delete whitelistedTokens[_token];
|
|||
|
|
delete tokenDecimals[_token];
|
|||
|
|
delete tokenWeights[_token];
|
|||
|
|
delete maxUsdyAmounts[_token];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setSwapFees(
|
|||
|
|
uint256 _swapFee,
|
|||
|
|
uint256 _stableSwapFee,
|
|||
|
|
uint256 _taxBasisPoints,
|
|||
|
|
uint256 _stableTaxBasisPoints
|
|||
|
|
) external onlyGov {
|
|||
|
|
require(_swapFee <= 100, "YTVault: invalid swap fee");
|
|||
|
|
require(_stableSwapFee <= 50, "YTVault: invalid stable swap fee");
|
|||
|
|
swapFeeBasisPoints = _swapFee;
|
|||
|
|
stableSwapFeeBasisPoints = _stableSwapFee;
|
|||
|
|
taxBasisPoints = _taxBasisPoints;
|
|||
|
|
stableTaxBasisPoints = _stableTaxBasisPoints;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setDynamicFees(bool _hasDynamicFees) external onlyGov {
|
|||
|
|
hasDynamicFees = _hasDynamicFees;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setEmergencyMode(bool _emergencyMode) external onlyGov {
|
|||
|
|
emergencyMode = _emergencyMode;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setSwapEnabled(bool _isSwapEnabled) external onlyGov {
|
|||
|
|
isSwapEnabled = _isSwapEnabled;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function withdrawToken(address _token, address _receiver, uint256 _amount) external onlyGov {
|
|||
|
|
require(emergencyMode, "YTVault: not in emergency");
|
|||
|
|
IERC20(_token).safeTransfer(_receiver, _amount);
|
|||
|
|
_updateTokenBalance(_token);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setMaxSwapSlippageBps(uint256 _slippageBps) external onlyGov {
|
|||
|
|
require(_slippageBps <= 2000, "YTVault: slippage too high"); // 最大20%
|
|||
|
|
maxSwapSlippageBps = _slippageBps;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setMaxSwapAmount(address _token, uint256 _amount) external onlyGov {
|
|||
|
|
maxSwapAmount[_token] = _amount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setDailySwapLimit(uint256 _limit) external onlyGov {
|
|||
|
|
dailySwapLimit = _limit;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 核心功能:buyUSDY ==========
|
|||
|
|
|
|||
|
|
function buyUSDY(address _token, address _receiver)
|
|||
|
|
external
|
|||
|
|
onlyPoolManager
|
|||
|
|
nonReentrant
|
|||
|
|
notInEmergency
|
|||
|
|
returns (uint256)
|
|||
|
|
{
|
|||
|
|
require(whitelistedTokens[_token], "YTVault: token not whitelisted");
|
|||
|
|
require(isSwapEnabled, "YTVault: swap disabled");
|
|||
|
|
|
|||
|
|
uint256 tokenAmount = _transferIn(_token);
|
|||
|
|
require(tokenAmount > 0, "YTVault: invalid token amount");
|
|||
|
|
|
|||
|
|
uint256 price = _getPrice(_token, false);
|
|||
|
|
uint256 usdyAmount = tokenAmount * price / PRICE_PRECISION;
|
|||
|
|
usdyAmount = _adjustForDecimals(usdyAmount, _token, usdy);
|
|||
|
|
require(usdyAmount > 0, "YTVault: invalid usdy amount");
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 核心功能:sellUSDY ==========
|
|||
|
|
|
|||
|
|
function sellUSDY(address _token, address _receiver)
|
|||
|
|
external
|
|||
|
|
onlyPoolManager
|
|||
|
|
nonReentrant
|
|||
|
|
notInEmergency
|
|||
|
|
returns (uint256)
|
|||
|
|
{
|
|||
|
|
require(whitelistedTokens[_token], "YTVault: token not whitelisted");
|
|||
|
|
require(isSwapEnabled, "YTVault: swap disabled");
|
|||
|
|
|
|||
|
|
uint256 usdyAmount = _transferIn(usdy);
|
|||
|
|
require(usdyAmount > 0, "YTVault: invalid usdy amount");
|
|||
|
|
|
|||
|
|
uint256 price = _getPrice(_token, true);
|
|||
|
|
uint256 tokenAmount = usdyAmount * PRICE_PRECISION / price;
|
|||
|
|
tokenAmount = _adjustForDecimals(tokenAmount, usdy, _token);
|
|||
|
|
|
|||
|
|
uint256 feeBasisPoints = _getSwapFeeBasisPoints(usdy, _token, tokenAmount);
|
|||
|
|
uint256 amountOutAfterFees = tokenAmount * (BASIS_POINTS_DIVISOR - feeBasisPoints) / BASIS_POINTS_DIVISOR;
|
|||
|
|
|
|||
|
|
require(amountOutAfterFees > 0, "YTVault: invalid amount out");
|
|||
|
|
require(poolAmounts[_token] >= amountOutAfterFees, "YTVault: insufficient pool");
|
|||
|
|
|
|||
|
|
_decreasePoolAmount(_token, amountOutAfterFees);
|
|||
|
|
_decreaseUsdyAmount(_token, usdyAmount);
|
|||
|
|
|
|||
|
|
IUSDY(usdy).burn(address(this), usdyAmount);
|
|||
|
|
IERC20(_token).safeTransfer(_receiver, amountOutAfterFees);
|
|||
|
|
|
|||
|
|
emit RemoveLiquidity(_receiver, _token, usdyAmount, amountOutAfterFees);
|
|||
|
|
|
|||
|
|
return amountOutAfterFees;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 核心功能:swap ==========
|
|||
|
|
|
|||
|
|
function swap(
|
|||
|
|
address _tokenIn,
|
|||
|
|
address _tokenOut,
|
|||
|
|
address _receiver
|
|||
|
|
) external onlyPoolManager nonReentrant notInEmergency returns (uint256) {
|
|||
|
|
require(isSwapEnabled, "YTVault: swap disabled");
|
|||
|
|
require(whitelistedTokens[_tokenIn], "YTVault: tokenIn not whitelisted");
|
|||
|
|
require(whitelistedTokens[_tokenOut], "YTVault: tokenOut not whitelisted");
|
|||
|
|
require(_tokenIn != _tokenOut, "YTVault: same token");
|
|||
|
|
|
|||
|
|
uint256 amountIn = _transferIn(_tokenIn);
|
|||
|
|
require(amountIn > 0, "YTVault: invalid amount in");
|
|||
|
|
|
|||
|
|
// 检查单笔交易限额
|
|||
|
|
if (maxSwapAmount[_tokenIn] > 0) {
|
|||
|
|
require(amountIn <= maxSwapAmount[_tokenIn], "YTVault: amount exceeds limit");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uint256 priceIn = _getPrice(_tokenIn, false);
|
|||
|
|
uint256 priceOut = _getPrice(_tokenOut, true);
|
|||
|
|
|
|||
|
|
uint256 usdyAmount = amountIn * priceIn / PRICE_PRECISION;
|
|||
|
|
usdyAmount = _adjustForDecimals(usdyAmount, _tokenIn, usdy);
|
|||
|
|
|
|||
|
|
// 检查每日交易量限制
|
|||
|
|
_checkDailySwapLimit(usdyAmount);
|
|||
|
|
|
|||
|
|
uint256 amountOut = usdyAmount * PRICE_PRECISION / priceOut;
|
|||
|
|
amountOut = _adjustForDecimals(amountOut, usdy, _tokenOut);
|
|||
|
|
|
|||
|
|
uint256 feeBasisPoints = _getSwapFeeBasisPoints(_tokenIn, _tokenOut, amountOut);
|
|||
|
|
uint256 amountOutAfterFees = amountOut * (BASIS_POINTS_DIVISOR - feeBasisPoints) / BASIS_POINTS_DIVISOR;
|
|||
|
|
|
|||
|
|
require(amountOutAfterFees > 0, "YTVault: invalid amount out");
|
|||
|
|
require(poolAmounts[_tokenOut] >= amountOutAfterFees, "YTVault: insufficient pool");
|
|||
|
|
|
|||
|
|
// 全局滑点保护
|
|||
|
|
_validateSwapSlippage(amountIn, amountOutAfterFees, priceIn, priceOut);
|
|||
|
|
|
|||
|
|
_increasePoolAmount(_tokenIn, amountIn);
|
|||
|
|
_decreasePoolAmount(_tokenOut, amountOutAfterFees);
|
|||
|
|
|
|||
|
|
_increaseUsdyAmount(_tokenIn, usdyAmount);
|
|||
|
|
_decreaseUsdyAmount(_tokenOut, usdyAmount);
|
|||
|
|
|
|||
|
|
IERC20(_tokenOut).safeTransfer(_receiver, amountOutAfterFees);
|
|||
|
|
|
|||
|
|
emit Swap(msg.sender, _tokenIn, _tokenOut, amountIn, amountOutAfterFees, feeBasisPoints);
|
|||
|
|
|
|||
|
|
return amountOutAfterFees;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 查询函数 ==========
|
|||
|
|
|
|||
|
|
function getPrice(address _token) external view returns (uint256) {
|
|||
|
|
return _getPrice(_token, true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getAllPoolTokens() external view returns (address[] memory) {
|
|||
|
|
return allWhitelistedTokens;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getPoolValue() 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, true);
|
|||
|
|
uint256 value = amount * price / PRICE_PRECISION;
|
|||
|
|
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) {
|
|||
|
|
require(usdyAmounts[_token] <= maxUsdyAmount, "YTVault: max USDY exceeded");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _decreaseUsdyAmount(address _token, uint256 _amount) private {
|
|||
|
|
uint256 value = usdyAmounts[_token];
|
|||
|
|
require(value >= _amount, "YTVault: insufficient USDY amount");
|
|||
|
|
usdyAmounts[_token] = value - _amount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 _getSwapFeeBasisPoints(
|
|||
|
|
address _tokenIn,
|
|||
|
|
address _tokenOut,
|
|||
|
|
uint256 _usdyAmount
|
|||
|
|
) private view returns (uint256) {
|
|||
|
|
bool isStableSwap = (_tokenIn == usdy || _tokenOut == usdy);
|
|||
|
|
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 _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 {
|
|||
|
|
require(poolAmounts[_token] >= _amount, "YTVault: insufficient pool");
|
|||
|
|
poolAmounts[_token] -= _amount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _validatePoolAmount(address _token) private view {
|
|||
|
|
require(poolAmounts[_token] <= tokenBalances[_token], "YTVault: invalid pool amount");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
require(slippage <= maxSwapSlippageBps, "YTVault: slippage too high");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _checkDailySwapLimit(uint256 _usdyAmount) private {
|
|||
|
|
if (dailySwapLimit == 0) return;
|
|||
|
|
|
|||
|
|
uint256 currentDay = block.timestamp / 1 days;
|
|||
|
|
|
|||
|
|
// 新的一天,重置计数器
|
|||
|
|
if (currentDay > lastSwapDay) {
|
|||
|
|
currentDaySwapVolume = 0;
|
|||
|
|
lastSwapDay = currentDay;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
currentDaySwapVolume += _usdyAmount;
|
|||
|
|
require(currentDaySwapVolume <= dailySwapLimit, "YTVault: daily limit exceeded");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 = tokenDecimals[_tokenFrom];
|
|||
|
|
uint256 decimalsTo = tokenDecimals[_tokenTo];
|
|||
|
|
|
|||
|
|
if (decimalsFrom == decimalsTo) {
|
|||
|
|
return _amount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (decimalsFrom > decimalsTo) {
|
|||
|
|
return _amount / (10 ** (decimalsFrom - decimalsTo));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return _amount * (10 ** (decimalsTo - decimalsFrom));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 接口定义 ==========
|
|||
|
|
|
|||
|
|
interface IUSDY {
|
|||
|
|
function mint(address _to, uint256 _amount) external;
|
|||
|
|
function burn(address _from, uint256 _amount) external;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface IYTPriceFeed {
|
|||
|
|
function getPrice(address _token, bool _maximise) external view returns (uint256);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.4 YT Pool Manager (YTPoolManager.sol)
|
|||
|
|
|
|||
|
|
**功能**: 管理ytLP的铸造和赎回,计算池子AUM
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
pragma solidity ^0.8.0;
|
|||
|
|
|
|||
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|||
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|||
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|||
|
|
|
|||
|
|
contract YTPoolManager is ReentrancyGuard {
|
|||
|
|
using SafeERC20 for IERC20;
|
|||
|
|
|
|||
|
|
// ========== 常量 ==========
|
|||
|
|
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 = 15 minutes;
|
|||
|
|
mapping(address => uint256) public lastAddedAt;
|
|||
|
|
|
|||
|
|
bool public inPrivateMode;
|
|||
|
|
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);
|
|||
|
|
|
|||
|
|
// ========== 修饰符 ==========
|
|||
|
|
modifier onlyGov() {
|
|||
|
|
require(msg.sender == gov, "YTPoolManager: forbidden");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
modifier onlyHandler() {
|
|||
|
|
require(isHandler[msg.sender] || msg.sender == gov, "YTPoolManager: forbidden");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 构造函数 ==========
|
|||
|
|
constructor(
|
|||
|
|
address _ytVault,
|
|||
|
|
address _usdy,
|
|||
|
|
address _ytLP,
|
|||
|
|
uint256 _cooldownDuration
|
|||
|
|
) {
|
|||
|
|
gov = msg.sender;
|
|||
|
|
ytVault = _ytVault;
|
|||
|
|
usdy = _usdy;
|
|||
|
|
ytLP = _ytLP;
|
|||
|
|
cooldownDuration = _cooldownDuration;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 管理函数 ==========
|
|||
|
|
|
|||
|
|
function setHandler(address _handler, bool _isActive) external onlyGov {
|
|||
|
|
isHandler[_handler] = _isActive;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setCooldownDuration(uint256 _duration) external onlyGov {
|
|||
|
|
require(_duration <= MAX_COOLDOWN_DURATION, "YTPoolManager: invalid duration");
|
|||
|
|
cooldownDuration = _duration;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setAumAdjustment(uint256 _addition, uint256 _deduction) external onlyGov {
|
|||
|
|
aumAddition = _addition;
|
|||
|
|
aumDeduction = _deduction;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setInPrivateMode(bool _inPrivateMode) external onlyGov {
|
|||
|
|
inPrivateMode = _inPrivateMode;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 核心功能:添加流动性 ==========
|
|||
|
|
|
|||
|
|
function addLiquidity(
|
|||
|
|
address _token,
|
|||
|
|
uint256 _amount,
|
|||
|
|
uint256 _minUsdy,
|
|||
|
|
uint256 _minYtLP
|
|||
|
|
) external nonReentrant returns (uint256) {
|
|||
|
|
require(!inPrivateMode, "YTPoolManager: private mode");
|
|||
|
|
return _addLiquidity(msg.sender, msg.sender, _token, _amount, _minUsdy, _minYtLP);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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) {
|
|||
|
|
require(_amount > 0, "YTPoolManager: invalid amount");
|
|||
|
|
|
|||
|
|
uint256 aumInUsdy = getAumInUsdy(true);
|
|||
|
|
uint256 ytLPSupply = IERC20(ytLP).totalSupply();
|
|||
|
|
|
|||
|
|
IERC20(_token).safeTransferFrom(_fundingAccount, ytVault, _amount);
|
|||
|
|
uint256 usdyAmount = IYTVault(ytVault).buyUSDY(_token, address(this));
|
|||
|
|
require(usdyAmount >= _minUsdy, "YTPoolManager: insufficient USDY");
|
|||
|
|
|
|||
|
|
uint256 mintAmount;
|
|||
|
|
if (ytLPSupply == 0) {
|
|||
|
|
mintAmount = usdyAmount;
|
|||
|
|
} else {
|
|||
|
|
mintAmount = usdyAmount * ytLPSupply / aumInUsdy;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
require(mintAmount >= _minYtLP, "YTPoolManager: insufficient ytLP");
|
|||
|
|
|
|||
|
|
IYTLPToken(ytLP).mint(_account, mintAmount);
|
|||
|
|
lastAddedAt[_account] = block.timestamp;
|
|||
|
|
|
|||
|
|
emit AddLiquidity(_account, _token, _amount, aumInUsdy, ytLPSupply, usdyAmount, mintAmount);
|
|||
|
|
|
|||
|
|
return mintAmount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 核心功能:移除流动性 ==========
|
|||
|
|
|
|||
|
|
function removeLiquidity(
|
|||
|
|
address _tokenOut,
|
|||
|
|
uint256 _ytLPAmount,
|
|||
|
|
uint256 _minOut,
|
|||
|
|
address _receiver
|
|||
|
|
) external nonReentrant returns (uint256) {
|
|||
|
|
require(!inPrivateMode, "YTPoolManager: private mode");
|
|||
|
|
return _removeLiquidity(msg.sender, _tokenOut, _ytLPAmount, _minOut, _receiver);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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) {
|
|||
|
|
require(_ytLPAmount > 0, "YTPoolManager: invalid amount");
|
|||
|
|
|
|||
|
|
require(
|
|||
|
|
lastAddedAt[_account] + cooldownDuration <= block.timestamp,
|
|||
|
|
"YTPoolManager: cooldown not passed"
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
uint256 aumInUsdy = getAumInUsdy(false);
|
|||
|
|
uint256 ytLPSupply = IERC20(ytLP).totalSupply();
|
|||
|
|
|
|||
|
|
uint256 usdyAmount = _ytLPAmount * aumInUsdy / ytLPSupply;
|
|||
|
|
|
|||
|
|
// 先销毁ytLP
|
|||
|
|
IYTLPToken(ytLP).burn(_account, _ytLPAmount);
|
|||
|
|
|
|||
|
|
// 检查并准备USDY
|
|||
|
|
uint256 usdyBalance = IERC20(usdy).balanceOf(address(this));
|
|||
|
|
if (usdyAmount > usdyBalance) {
|
|||
|
|
// 不直接铸造,而是从Vault获取
|
|||
|
|
uint256 needed = usdyAmount - usdyBalance;
|
|||
|
|
// Vault应该预留足够的USDY储备,这里只是容错处理
|
|||
|
|
// 实际部署时应确保Vault有足够的USDY储备或由治理手动补充
|
|||
|
|
require(usdyBalance >= usdyAmount * 95 / 100, "YTPoolManager: insufficient USDY reserve");
|
|||
|
|
usdyAmount = usdyBalance; // 使用可用余额
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
IERC20(usdy).safeTransfer(ytVault, usdyAmount);
|
|||
|
|
uint256 amountOut = IYTVault(ytVault).sellUSDY(_tokenOut, _receiver);
|
|||
|
|
|
|||
|
|
require(amountOut >= _minOut, "YTPoolManager: insufficient output");
|
|||
|
|
|
|||
|
|
emit RemoveLiquidity(_account, _tokenOut, _ytLPAmount, aumInUsdy, ytLPSupply, usdyAmount, amountOut);
|
|||
|
|
|
|||
|
|
return amountOut;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== USDY储备管理 ==========
|
|||
|
|
|
|||
|
|
function depositUsdyReserve(uint256 _amount) external onlyGov {
|
|||
|
|
IERC20(usdy).safeTransferFrom(msg.sender, address(this), _amount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function withdrawUsdyReserve(uint256 _amount, address _receiver) external onlyGov {
|
|||
|
|
IERC20(usdy).safeTransfer(_receiver, _amount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getUsdyReserve() external view returns (uint256) {
|
|||
|
|
return IERC20(usdy).balanceOf(address(this));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 查询函数 ==========
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getAumInUsdy(bool _maximise) public view returns (uint256) {
|
|||
|
|
uint256 aum = IYTVault(ytVault).getPoolValue();
|
|||
|
|
|
|||
|
|
aum += aumAddition;
|
|||
|
|
if (aum > aumDeduction) {
|
|||
|
|
aum -= aumDeduction;
|
|||
|
|
} else {
|
|||
|
|
aum = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return aum;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 接口定义 ==========
|
|||
|
|
|
|||
|
|
interface IYTVault {
|
|||
|
|
function buyUSDY(address _token, address _receiver) external returns (uint256);
|
|||
|
|
function sellUSDY(address _token, address _receiver) external returns (uint256);
|
|||
|
|
function getPoolValue() external view returns (uint256);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface IYTLPToken {
|
|||
|
|
function mint(address _to, uint256 _amount) external;
|
|||
|
|
function burn(address _from, uint256 _amount) external;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface IUSDY {
|
|||
|
|
function mint(address _to, uint256 _amount) external;
|
|||
|
|
function burn(address _from, uint256 _amount) external;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.5 YT Reward Router (YTRewardRouter.sol)
|
|||
|
|
|
|||
|
|
**功能**: 用户交互入口(简化版,自动复利模式)
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
pragma solidity ^0.8.0;
|
|||
|
|
|
|||
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|||
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|||
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|||
|
|
|
|||
|
|
contract YTRewardRouter is ReentrancyGuard {
|
|||
|
|
using SafeERC20 for IERC20;
|
|||
|
|
|
|||
|
|
// ========== 状态变量 ==========
|
|||
|
|
address public gov;
|
|||
|
|
address public usdy;
|
|||
|
|
address public ytLP;
|
|||
|
|
address public ytPoolManager;
|
|||
|
|
address public ytVault;
|
|||
|
|
|
|||
|
|
bool public isInitialized;
|
|||
|
|
|
|||
|
|
// ========== 事件 ==========
|
|||
|
|
event Swap(address indexed account, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut);
|
|||
|
|
|
|||
|
|
// ========== 修饰符 ==========
|
|||
|
|
modifier onlyGov() {
|
|||
|
|
require(msg.sender == gov, "YTRewardRouter: forbidden");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 构造函数 ==========
|
|||
|
|
constructor() {
|
|||
|
|
gov = msg.sender;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function initialize(
|
|||
|
|
address _usdy,
|
|||
|
|
address _ytLP,
|
|||
|
|
address _ytPoolManager,
|
|||
|
|
address _ytVault
|
|||
|
|
) external onlyGov {
|
|||
|
|
require(!isInitialized, "YTRewardRouter: already initialized");
|
|||
|
|
isInitialized = true;
|
|||
|
|
|
|||
|
|
usdy = _usdy;
|
|||
|
|
ytLP = _ytLP;
|
|||
|
|
ytPoolManager = _ytPoolManager;
|
|||
|
|
ytVault = _ytVault;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 核心功能:添加流动性 ==========
|
|||
|
|
|
|||
|
|
function addLiquidity(
|
|||
|
|
address _token,
|
|||
|
|
uint256 _amount,
|
|||
|
|
uint256 _minUsdy,
|
|||
|
|
uint256 _minYtLP
|
|||
|
|
) external nonReentrant returns (uint256) {
|
|||
|
|
require(_amount > 0, "YTRewardRouter: invalid amount");
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 核心功能:移除流动性 ==========
|
|||
|
|
|
|||
|
|
function removeLiquidity(
|
|||
|
|
address _tokenOut,
|
|||
|
|
uint256 _ytLPAmount,
|
|||
|
|
uint256 _minOut,
|
|||
|
|
address _receiver
|
|||
|
|
) external nonReentrant returns (uint256) {
|
|||
|
|
require(_ytLPAmount > 0, "YTRewardRouter: invalid amount");
|
|||
|
|
|
|||
|
|
address account = msg.sender;
|
|||
|
|
|
|||
|
|
uint256 amountOut = IYTPoolManager(ytPoolManager).removeLiquidityForAccount(
|
|||
|
|
account,
|
|||
|
|
_tokenOut,
|
|||
|
|
_ytLPAmount,
|
|||
|
|
_minOut,
|
|||
|
|
_receiver
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return amountOut;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 核心功能:YT代币Swap ==========
|
|||
|
|
|
|||
|
|
function swapYT(
|
|||
|
|
address _tokenIn,
|
|||
|
|
address _tokenOut,
|
|||
|
|
uint256 _amountIn,
|
|||
|
|
uint256 _minOut,
|
|||
|
|
address _receiver
|
|||
|
|
) external nonReentrant returns (uint256) {
|
|||
|
|
require(_amountIn > 0, "YTRewardRouter: invalid amount");
|
|||
|
|
|
|||
|
|
address account = msg.sender;
|
|||
|
|
|
|||
|
|
IERC20(_tokenIn).safeTransferFrom(account, ytVault, _amountIn);
|
|||
|
|
|
|||
|
|
uint256 amountOut = IYTVault(ytVault).swap(_tokenIn, _tokenOut, _receiver);
|
|||
|
|
|
|||
|
|
require(amountOut >= _minOut, "YTRewardRouter: insufficient output");
|
|||
|
|
|
|||
|
|
emit Swap(account, _tokenIn, _tokenOut, _amountIn, amountOut);
|
|||
|
|
|
|||
|
|
return amountOut;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 查询函数 ==========
|
|||
|
|
|
|||
|
|
function getYtLPPrice() external view returns (uint256) {
|
|||
|
|
return IYTPoolManager(ytPoolManager).getPrice(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 接口定义 ==========
|
|||
|
|
|
|||
|
|
interface IYTPoolManager {
|
|||
|
|
function addLiquidityForAccount(
|
|||
|
|
address _fundingAccount,
|
|||
|
|
address _account,
|
|||
|
|
address _token,
|
|||
|
|
uint256 _amount,
|
|||
|
|
uint256 _minUsdy,
|
|||
|
|
uint256 _minYtLP
|
|||
|
|
) external returns (uint256);
|
|||
|
|
|
|||
|
|
function removeLiquidityForAccount(
|
|||
|
|
address _account,
|
|||
|
|
address _tokenOut,
|
|||
|
|
uint256 _ytLPAmount,
|
|||
|
|
uint256 _minOut,
|
|||
|
|
address _receiver
|
|||
|
|
) external returns (uint256);
|
|||
|
|
|
|||
|
|
function getPrice(bool _maximise) external view returns (uint256);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface IYTVault {
|
|||
|
|
function swap(
|
|||
|
|
address _tokenIn,
|
|||
|
|
address _tokenOut,
|
|||
|
|
address _receiver
|
|||
|
|
) external returns (uint256);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.6 YT Price Feed (YTPriceFeed.sol)
|
|||
|
|
|
|||
|
|
**功能**: 直接从YT合约读取价格变量
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
pragma solidity ^0.8.0;
|
|||
|
|
|
|||
|
|
contract YTPriceFeed {
|
|||
|
|
address public gov;
|
|||
|
|
|
|||
|
|
uint256 public constant PRICE_PRECISION = 10 ** 30;
|
|||
|
|
uint256 public constant BASIS_POINTS_DIVISOR = 10000;
|
|||
|
|
|
|||
|
|
// WUSD固定价格
|
|||
|
|
address public constant WUSD = 0x7Cd017ca5ddb86861FA983a34b5F495C6F898c41;
|
|||
|
|
uint256 public constant WUSD_PRICE = 1e30;
|
|||
|
|
|
|||
|
|
// 价格保护参数
|
|||
|
|
uint256 public maxPriceChangeBps = 500; // 5% 最大价格变动
|
|||
|
|
uint256 public maxPriceAge = 1 hours; // 价格最大有效期
|
|||
|
|
|
|||
|
|
// 价格历史记录
|
|||
|
|
mapping(address => uint256) public lastPrice;
|
|||
|
|
mapping(address => uint256) public lastPriceUpdateTime;
|
|||
|
|
|
|||
|
|
// 断路器
|
|||
|
|
bool public circuitBreakerEnabled = true;
|
|||
|
|
|
|||
|
|
event PriceUpdate(address indexed token, uint256 oldPrice, uint256 newPrice, uint256 timestamp);
|
|||
|
|
event CircuitBreakerTriggered(address indexed token, uint256 oldPrice, uint256 newPrice);
|
|||
|
|
|
|||
|
|
modifier onlyGov() {
|
|||
|
|
require(msg.sender == gov, "YTPriceFeed: forbidden");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
constructor() {
|
|||
|
|
gov = msg.sender;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 管理函数 ==========
|
|||
|
|
|
|||
|
|
function setMaxPriceChangeBps(uint256 _maxPriceChangeBps) external onlyGov {
|
|||
|
|
require(_maxPriceChangeBps <= 2000, "YTPriceFeed: max change too high"); // 最大20%
|
|||
|
|
maxPriceChangeBps = _maxPriceChangeBps;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setMaxPriceAge(uint256 _maxPriceAge) external onlyGov {
|
|||
|
|
require(_maxPriceAge <= 24 hours, "YTPriceFeed: max age too long");
|
|||
|
|
maxPriceAge = _maxPriceAge;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setCircuitBreakerEnabled(bool _enabled) external onlyGov {
|
|||
|
|
circuitBreakerEnabled = _enabled;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function forceUpdatePrice(address _token, uint256 _price) external onlyGov {
|
|||
|
|
lastPrice[_token] = _price;
|
|||
|
|
lastPriceUpdateTime[_token] = block.timestamp;
|
|||
|
|
emit PriceUpdate(_token, lastPrice[_token], _price, block.timestamp);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 核心功能 ==========
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @notice 获取YT代币价格(带保护机制)
|
|||
|
|
* @dev 包含价格变动限制和时效性检查
|
|||
|
|
*/
|
|||
|
|
function getPrice(address _token, bool _maximise) external view returns (uint256) {
|
|||
|
|
if (_token == WUSD) {
|
|||
|
|
return WUSD_PRICE;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uint256 newPrice = _getRawPrice(_token);
|
|||
|
|
|
|||
|
|
// 价格时效性检查
|
|||
|
|
uint256 lastUpdate = IYTToken(_token).lastPriceUpdate();
|
|||
|
|
require(block.timestamp - lastUpdate <= maxPriceAge, "YTPriceFeed: price too old");
|
|||
|
|
|
|||
|
|
// 断路器检查
|
|||
|
|
if (circuitBreakerEnabled) {
|
|||
|
|
_validatePriceChange(_token, newPrice);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return newPrice;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @notice 获取原始价格并更新记录
|
|||
|
|
* @dev 此函数应由可信的keeper定期调用
|
|||
|
|
*/
|
|||
|
|
function updatePrice(address _token) external returns (uint256) {
|
|||
|
|
if (_token == WUSD) {
|
|||
|
|
return WUSD_PRICE;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uint256 oldPrice = lastPrice[_token];
|
|||
|
|
uint256 newPrice = _getRawPrice(_token);
|
|||
|
|
|
|||
|
|
// 价格时效性检查
|
|||
|
|
uint256 lastUpdate = IYTToken(_token).lastPriceUpdate();
|
|||
|
|
require(block.timestamp - lastUpdate <= maxPriceAge, "YTPriceFeed: price too old");
|
|||
|
|
|
|||
|
|
// 断路器检查
|
|||
|
|
if (circuitBreakerEnabled) {
|
|||
|
|
_validatePriceChange(_token, newPrice);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lastPrice[_token] = newPrice;
|
|||
|
|
lastPriceUpdateTime[_token] = block.timestamp;
|
|||
|
|
|
|||
|
|
emit PriceUpdate(_token, oldPrice, newPrice, block.timestamp);
|
|||
|
|
|
|||
|
|
return newPrice;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 内部函数 ==========
|
|||
|
|
|
|||
|
|
function _getRawPrice(address _token) private view returns (uint256) {
|
|||
|
|
return IYTToken(_token).assetPrice();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _validatePriceChange(address _token, uint256 _newPrice) private view {
|
|||
|
|
uint256 oldPrice = lastPrice[_token];
|
|||
|
|
|
|||
|
|
// 首次设置价格,跳过检查
|
|||
|
|
if (oldPrice == 0) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查时间间隔(短时间内的大幅波动才需要警惕)
|
|||
|
|
uint256 timeSinceLastUpdate = block.timestamp - lastPriceUpdateTime[_token];
|
|||
|
|
if (timeSinceLastUpdate >= maxPriceAge) {
|
|||
|
|
return; // 长时间未更新,允许较大变动
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算价格变动百分比
|
|||
|
|
uint256 priceDiff = _newPrice > oldPrice ? _newPrice - oldPrice : oldPrice - _newPrice;
|
|||
|
|
uint256 maxDiff = oldPrice * maxPriceChangeBps / BASIS_POINTS_DIVISOR;
|
|||
|
|
|
|||
|
|
if (priceDiff > maxDiff) {
|
|||
|
|
emit CircuitBreakerTriggered(_token, oldPrice, _newPrice);
|
|||
|
|
revert("YTPriceFeed: price change too large");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 查询函数 ==========
|
|||
|
|
|
|||
|
|
function getPriceInfo(address _token) external view returns (
|
|||
|
|
uint256 currentPrice,
|
|||
|
|
uint256 cachedPrice,
|
|||
|
|
uint256 lastUpdate,
|
|||
|
|
uint256 priceAge,
|
|||
|
|
bool isStale
|
|||
|
|
) {
|
|||
|
|
currentPrice = _getRawPrice(_token);
|
|||
|
|
cachedPrice = lastPrice[_token];
|
|||
|
|
lastUpdate = IYTToken(_token).lastPriceUpdate();
|
|||
|
|
priceAge = block.timestamp - lastUpdate;
|
|||
|
|
isStale = priceAge > maxPriceAge;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface IYTToken {
|
|||
|
|
function assetPrice() external view returns (uint256);
|
|||
|
|
function lastPriceUpdate() external view returns (uint256);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**YT代币需要实现**:
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
pragma solidity ^0.8.0;
|
|||
|
|
|
|||
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|||
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|||
|
|
|
|||
|
|
contract YTToken is ERC20, Ownable {
|
|||
|
|
uint256 public constant PRICE_PRECISION = 10 ** 30;
|
|||
|
|
|
|||
|
|
uint256 public totalAssets;
|
|||
|
|
uint256 public accumulatedYield;
|
|||
|
|
|
|||
|
|
// 价格变量(public自动生成getter)
|
|||
|
|
uint256 public assetPrice;
|
|||
|
|
uint256 public lastPriceUpdate;
|
|||
|
|
|
|||
|
|
// 价格更新控制
|
|||
|
|
address public priceUpdater;
|
|||
|
|
uint256 public minUpdateInterval = 5 minutes; // 最小更新间隔
|
|||
|
|
|
|||
|
|
event PriceUpdated(uint256 oldPrice, uint256 newPrice, uint256 timestamp);
|
|||
|
|
event YieldAccumulated(uint256 amount, uint256 timestamp);
|
|||
|
|
|
|||
|
|
modifier onlyPriceUpdater() {
|
|||
|
|
require(msg.sender == priceUpdater || msg.sender == owner(), "YTToken: not updater");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
|
|||
|
|
assetPrice = PRICE_PRECISION; // 初始价格为1
|
|||
|
|
lastPriceUpdate = block.timestamp;
|
|||
|
|
priceUpdater = msg.sender;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setPriceUpdater(address _updater) external onlyOwner {
|
|||
|
|
priceUpdater = _updater;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setMinUpdateInterval(uint256 _interval) external onlyOwner {
|
|||
|
|
require(_interval <= 1 hours, "YTToken: interval too long");
|
|||
|
|
minUpdateInterval = _interval;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @notice 更新代币价格
|
|||
|
|
* @dev 只能由授权的updater调用,有最小时间间隔限制
|
|||
|
|
*/
|
|||
|
|
function updatePrice() public onlyPriceUpdater {
|
|||
|
|
require(
|
|||
|
|
block.timestamp >= lastPriceUpdate + minUpdateInterval,
|
|||
|
|
"YTToken: update too frequent"
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
uint256 oldPrice = assetPrice;
|
|||
|
|
uint256 supply = totalSupply();
|
|||
|
|
|
|||
|
|
if (supply == 0) {
|
|||
|
|
assetPrice = PRICE_PRECISION;
|
|||
|
|
} else {
|
|||
|
|
uint256 totalValue = totalAssets + accumulatedYield;
|
|||
|
|
// 计算每个token对应的USDC价值(18位精度)
|
|||
|
|
uint256 usdcPerToken = totalValue * 1e18 / supply;
|
|||
|
|
// 转换为30位精度的价格
|
|||
|
|
assetPrice = usdcPerToken * PRICE_PRECISION / 1e18;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lastPriceUpdate = block.timestamp;
|
|||
|
|
|
|||
|
|
emit PriceUpdated(oldPrice, assetPrice, block.timestamp);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @notice 累积收益并更新价格
|
|||
|
|
* @dev 当从收益策略中收到新收益时调用
|
|||
|
|
*/
|
|||
|
|
function updateYield(uint256 _newYield) external onlyPriceUpdater {
|
|||
|
|
require(_newYield > 0, "YTToken: invalid yield");
|
|||
|
|
|
|||
|
|
accumulatedYield += _newYield;
|
|||
|
|
|
|||
|
|
emit YieldAccumulated(_newYield, block.timestamp);
|
|||
|
|
|
|||
|
|
// 收益更新后立即更新价格
|
|||
|
|
if (block.timestamp >= lastPriceUpdate + minUpdateInterval) {
|
|||
|
|
uint256 oldPrice = assetPrice;
|
|||
|
|
uint256 supply = totalSupply();
|
|||
|
|
|
|||
|
|
if (supply > 0) {
|
|||
|
|
uint256 totalValue = totalAssets + accumulatedYield;
|
|||
|
|
uint256 usdcPerToken = totalValue * 1e18 / supply;
|
|||
|
|
assetPrice = usdcPerToken * PRICE_PRECISION / 1e18;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lastPriceUpdate = block.timestamp;
|
|||
|
|
emit PriceUpdated(oldPrice, assetPrice, block.timestamp);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @notice 获取当前价格信息
|
|||
|
|
*/
|
|||
|
|
function getPriceInfo() external view returns (
|
|||
|
|
uint256 price,
|
|||
|
|
uint256 lastUpdate,
|
|||
|
|
uint256 timeSinceUpdate,
|
|||
|
|
uint256 totalVal
|
|||
|
|
) {
|
|||
|
|
price = assetPrice;
|
|||
|
|
lastUpdate = lastPriceUpdate;
|
|||
|
|
timeSinceUpdate = block.timestamp - lastPriceUpdate;
|
|||
|
|
totalVal = totalAssets + accumulatedYield;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 自动复利机制
|
|||
|
|
|
|||
|
|
### 4.1 核心理念
|
|||
|
|
|
|||
|
|
类似Compound的cToken,持有ytLP即自动增值,无需手动领取奖励。
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户持有ytLP → 池子收取手续费 → 手续费留在poolAmounts → AUM增长 → ytLP价格上涨
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 收益来源
|
|||
|
|
|
|||
|
|
| 收益来源 | 实现方式 |
|
|||
|
|
|---------|---------|
|
|||
|
|
| YT Swap手续费 | 留在池子,增加poolAmounts |
|
|||
|
|
| 存入手续费 | 留在池子 |
|
|||
|
|
| 取出手续费 | 留在池子 |
|
|||
|
|
|
|||
|
|
### 4.3 与GMX的差异
|
|||
|
|
|
|||
|
|
| 特性 | GMX GLP | YT Pool |
|
|||
|
|
|------|---------|---------|
|
|||
|
|
| 手续费分配 | 70% LP + 30% GMX质押者 | 100% LP |
|
|||
|
|
| 领取方式 | 手动claim | 自动复利 |
|
|||
|
|
| 质押要求 | 需要质押 | 无需质押 |
|
|||
|
|
| Gas成本 | 需要claim交易 | 零额外成本 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 动态手续费机制
|
|||
|
|
|
|||
|
|
### 5.1 三重平衡机制
|
|||
|
|
|
|||
|
|
#### 1. 目标权重(tokenWeights)
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// 配置示例
|
|||
|
|
totalTokenWeights = 10000
|
|||
|
|
|
|||
|
|
tokenWeights[YT-A] = 4000 // 40%
|
|||
|
|
tokenWeights[YT-B] = 3000 // 30%
|
|||
|
|
tokenWeights[YT-C] = 2000 // 20%
|
|||
|
|
tokenWeights[WUSD] = 1000 // 10%
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. 硬限制(maxUsdyAmounts)
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
maxUsdyAmounts[YT-A] = 45M USDY
|
|||
|
|
maxUsdyAmounts[YT-B] = 35M USDY
|
|||
|
|
maxUsdyAmounts[YT-C] = 25M USDY
|
|||
|
|
maxUsdyAmounts[WUSD] = 15M USDY
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. 动态手续费
|
|||
|
|
|
|||
|
|
- 改善平衡 → 降低手续费(可至0%)
|
|||
|
|
- 恶化平衡 → 提高手续费(可至0.8%)
|
|||
|
|
|
|||
|
|
### 5.2 配置参数
|
|||
|
|
|
|||
|
|
| 参数 | 典型值 | 说明 |
|
|||
|
|
|------|--------|------|
|
|||
|
|
| swapFeeBasisPoints | 30 | 基础swap手续费0.3% |
|
|||
|
|
| stableSwapFeeBasisPoints | 4 | 稳定币操作0.04% |
|
|||
|
|
| taxBasisPoints | 50 | 动态调整范围0.5% |
|
|||
|
|
| stableTaxBasisPoints | 20 | 稳定币动态调整0.2% |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 部署配置
|
|||
|
|
|
|||
|
|
### 6.1 部署顺序
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 部署USDY
|
|||
|
|
2. 部署YTLPToken
|
|||
|
|
3. 部署YTPriceFeed
|
|||
|
|
4. 部署YTVault
|
|||
|
|
5. 部署YTPoolManager
|
|||
|
|
6. 部署YTRewardRouter
|
|||
|
|
7. 配置权限和参数
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 部署脚本
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 1. 部署基础代币
|
|||
|
|
const usdy = await USDY.deploy(vault.address);
|
|||
|
|
const ytLP = await YTLPToken.deploy();
|
|||
|
|
|
|||
|
|
// 2. 部署核心合约
|
|||
|
|
const priceFeed = await YTPriceFeed.deploy();
|
|||
|
|
const vault = await YTVault.deploy(usdy.address, priceFeed.address);
|
|||
|
|
const poolManager = await YTPoolManager.deploy(
|
|||
|
|
vault.address,
|
|||
|
|
usdy.address,
|
|||
|
|
ytLP.address,
|
|||
|
|
15 * 60 // 15分钟冷却期
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 3. 配置权限
|
|||
|
|
await ytLP.setMinter(poolManager.address, true);
|
|||
|
|
await vault.setPoolManager(poolManager.address);
|
|||
|
|
await usdy.addVault(vault.address);
|
|||
|
|
await usdy.addVault(poolManager.address);
|
|||
|
|
|
|||
|
|
// 4. 配置手续费和安全参数
|
|||
|
|
await vault.setSwapFees(30, 4, 50, 20);
|
|||
|
|
await vault.setDynamicFees(true);
|
|||
|
|
await vault.setMaxSwapSlippageBps(1000); // 10%
|
|||
|
|
await vault.setDailySwapLimit(ethers.utils.parseEther("10000000")); // 1000万USDY/天
|
|||
|
|
|
|||
|
|
// 5. 配置价格保护
|
|||
|
|
await priceFeed.setMaxPriceChangeBps(500); // 5%
|
|||
|
|
await priceFeed.setMaxPriceAge(3600); // 1小时
|
|||
|
|
await priceFeed.setCircuitBreakerEnabled(true);
|
|||
|
|
|
|||
|
|
// 6. 添加白名单代币
|
|||
|
|
await vault.setWhitelistedToken(
|
|||
|
|
ytTokenA.address,
|
|||
|
|
18,
|
|||
|
|
4000,
|
|||
|
|
ethers.utils.parseEther("45000000")
|
|||
|
|
);
|
|||
|
|
await vault.setMaxSwapAmount(ytTokenA.address, ethers.utils.parseEther("1000000")); // 单笔100万
|
|||
|
|
|
|||
|
|
await vault.setWhitelistedToken(
|
|||
|
|
ytTokenB.address,
|
|||
|
|
18,
|
|||
|
|
3000,
|
|||
|
|
ethers.utils.parseEther("35000000")
|
|||
|
|
);
|
|||
|
|
await vault.setMaxSwapAmount(ytTokenB.address, ethers.utils.parseEther("1000000"));
|
|||
|
|
|
|||
|
|
await vault.setWhitelistedToken(
|
|||
|
|
ytTokenC.address,
|
|||
|
|
18,
|
|||
|
|
2000,
|
|||
|
|
ethers.utils.parseEther("25000000")
|
|||
|
|
);
|
|||
|
|
await vault.setMaxSwapAmount(ytTokenC.address, ethers.utils.parseEther("1000000"));
|
|||
|
|
|
|||
|
|
await vault.setWhitelistedToken(
|
|||
|
|
"0x7Cd017ca5ddb86861FA983a34b5F495C6F898c41", // WUSD
|
|||
|
|
18,
|
|||
|
|
1000,
|
|||
|
|
ethers.utils.parseEther("15000000")
|
|||
|
|
);
|
|||
|
|
await vault.setMaxSwapAmount("0x7Cd017ca5ddb86861FA983a34b5F495C6F898c41", ethers.utils.parseEther("500000"));
|
|||
|
|
|
|||
|
|
// 7. 初始化价格(首次部署)
|
|||
|
|
await priceFeed.forceUpdatePrice(ytTokenA.address, ethers.utils.parseUnits("1", 30));
|
|||
|
|
await priceFeed.forceUpdatePrice(ytTokenB.address, ethers.utils.parseUnits("1", 30));
|
|||
|
|
await priceFeed.forceUpdatePrice(ytTokenC.address, ethers.utils.parseUnits("1", 30));
|
|||
|
|
|
|||
|
|
// 8. 准备USDY储备金(建议为预期总供应量的10-20%)
|
|||
|
|
const reserveAmount = ethers.utils.parseEther("10000000"); // 1000万USDY储备
|
|||
|
|
await usdy.mint(poolManager.address, reserveAmount);
|
|||
|
|
|
|||
|
|
// 9. 部署Router
|
|||
|
|
const router = await YTRewardRouter.deploy();
|
|||
|
|
await router.initialize(
|
|||
|
|
usdy.address,
|
|||
|
|
ytLP.address,
|
|||
|
|
poolManager.address,
|
|||
|
|
vault.address
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 关键参数配置
|
|||
|
|
|
|||
|
|
### 7.1 Vault参数
|
|||
|
|
|
|||
|
|
| 参数 | 建议值 | 说明 |
|
|||
|
|
|------|--------|------|
|
|||
|
|
| swapFeeBasisPoints | 30 | 基础swap手续费 0.3% |
|
|||
|
|
| stableSwapFeeBasisPoints | 4 | 稳定币操作 0.04% |
|
|||
|
|
| taxBasisPoints | 50 | 动态调整范围 0.5% |
|
|||
|
|
| stableTaxBasisPoints | 20 | 稳定币动态调整 0.2% |
|
|||
|
|
| maxSwapSlippageBps | 1000 | 最大滑点 10% |
|
|||
|
|
| dailySwapLimit | 10M USDY | 每日交易量限制 |
|
|||
|
|
| maxSwapAmount | 1M per token | 单笔交易限额 |
|
|||
|
|
|
|||
|
|
### 7.2 PriceFeed参数
|
|||
|
|
|
|||
|
|
| 参数 | 建议值 | 说明 |
|
|||
|
|
|------|--------|------|
|
|||
|
|
| maxPriceChangeBps | 500 | 最大价格变动 5% |
|
|||
|
|
| maxPriceAge | 1 hour | 价格最大有效期 |
|
|||
|
|
| circuitBreakerEnabled | true | 启用断路器 |
|
|||
|
|
|
|||
|
|
### 7.3 PoolManager参数
|
|||
|
|
|
|||
|
|
| 参数 | 建议值 | 说明 |
|
|||
|
|
|------|--------|------|
|
|||
|
|
| cooldownDuration | 15分钟 | 提现冷却期 |
|
|||
|
|
| usdyReserve | 10-20% of total | USDY储备金比例 |
|
|||
|
|
|
|||
|
|
### 7.4 YTToken参数
|
|||
|
|
|
|||
|
|
| 参数 | 建议值 | 说明 |
|
|||
|
|
|------|--------|------|
|
|||
|
|
| minUpdateInterval | 5-15分钟 | 最小价格更新间隔 |
|
|||
|
|
|
|||
|
|
### 7.5 代币权重配置
|
|||
|
|
|
|||
|
|
| 代币 | 权重 | 最大USDY债务 | 单笔限额 |
|
|||
|
|
|------|------|------------|---------|
|
|||
|
|
| YT-A | 4000 (40%) | 45M | 1M |
|
|||
|
|
| YT-B | 3000 (30%) | 35M | 1M |
|
|||
|
|
| YT-C | 2000 (20%) | 25M | 1M |
|
|||
|
|
| WUSD | 1000 (10%) | 15M | 500K |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 安全机制
|
|||
|
|
|
|||
|
|
### 8.1 基础安全
|
|||
|
|
|
|||
|
|
1. **冷却期**: 15分钟锁定,防止闪电贷攻击
|
|||
|
|
2. **滑点保护**: 所有操作都有最小输出参数
|
|||
|
|
3. **重入保护**: nonReentrant修饰符保护所有外部调用
|
|||
|
|
4. **权限控制**: onlyGov/onlyPoolManager严格访问控制
|
|||
|
|
5. **余额验证**: tokenBalances精确跟踪防止余额不一致
|
|||
|
|
|
|||
|
|
### 8.2 价格保护机制
|
|||
|
|
|
|||
|
|
1. **价格变动限制**: 默认5%,防止价格被快速操纵
|
|||
|
|
2. **价格时效性**: 最长1小时,防止使用过期价格
|
|||
|
|
3. **价格断路器**: 异常时自动触发保护
|
|||
|
|
4. **价格历史**: 记录所有价格变动,便于审计
|
|||
|
|
|
|||
|
|
### 8.3 交易限制
|
|||
|
|
|
|||
|
|
1. **单笔限额**: 可配置的单笔交易上限
|
|||
|
|
2. **每日限额**: 防止大规模资金流动
|
|||
|
|
3. **全局滑点**: 最大10%滑点保护
|
|||
|
|
4. **池子余额**: 确保足够流动性
|
|||
|
|
|
|||
|
|
### 8.4 紧急机制
|
|||
|
|
|
|||
|
|
**实现代码**:
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
bool public emergencyMode = false;
|
|||
|
|
bool public isSwapEnabled = true;
|
|||
|
|
|
|||
|
|
function setEmergencyMode(bool _enabled) external onlyGov {
|
|||
|
|
emergencyMode = _enabled;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setSwapEnabled(bool _enabled) external onlyGov {
|
|||
|
|
isSwapEnabled = _enabled;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
modifier notInEmergency() {
|
|||
|
|
require(!emergencyMode, "YTVault: emergency mode");
|
|||
|
|
_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 紧急状态下可提取资金
|
|||
|
|
function withdrawToken(address _token, address _receiver, uint256 _amount) external onlyGov {
|
|||
|
|
require(emergencyMode, "YTVault: not in emergency");
|
|||
|
|
IERC20(_token).safeTransfer(_receiver, _amount);
|
|||
|
|
_updateTokenBalance(_token);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**使用场景**:
|
|||
|
|
- 发现安全漏洞时立即暂停
|
|||
|
|
- 价格异常时保护用户资金
|
|||
|
|
- 系统升级维护期间
|
|||
|
|
- 遭受攻击时的应急响应
|
|||
|
|
|
|||
|
|
### 8.5 多重验证
|
|||
|
|
|
|||
|
|
每个关键操作都经过多重验证:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户发起交易
|
|||
|
|
↓
|
|||
|
|
1. 检查紧急模式 (notInEmergency)
|
|||
|
|
↓
|
|||
|
|
2. 检查权限 (onlyPoolManager)
|
|||
|
|
↓
|
|||
|
|
3. 检查重入 (nonReentrant)
|
|||
|
|
↓
|
|||
|
|
4. 检查白名单 (whitelistedTokens)
|
|||
|
|
↓
|
|||
|
|
5. 检查交易限额 (maxSwapAmount)
|
|||
|
|
↓
|
|||
|
|
6. 检查价格有效性 (maxPriceAge)
|
|||
|
|
↓
|
|||
|
|
7. 检查价格变动 (maxPriceChangeBps)
|
|||
|
|
↓
|
|||
|
|
8. 检查每日限额 (dailySwapLimit)
|
|||
|
|
↓
|
|||
|
|
9. 检查滑点 (maxSwapSlippageBps)
|
|||
|
|
↓
|
|||
|
|
10. 执行交易
|
|||
|
|
↓
|
|||
|
|
11. 验证余额一致性 (_validatePoolAmount)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. 接口定义
|
|||
|
|
|
|||
|
|
### 9.1 核心接口
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// YTVault接口
|
|||
|
|
interface IYTVault {
|
|||
|
|
function buyUSDY(address _token, address _receiver) external returns (uint256);
|
|||
|
|
function sellUSDY(address _token, address _receiver) external returns (uint256);
|
|||
|
|
function swap(address _tokenIn, address _tokenOut, address _receiver) external returns (uint256);
|
|||
|
|
function getPoolValue() external view returns (uint256);
|
|||
|
|
function getTargetUsdyAmount(address _token) external view returns (uint256);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// YTPoolManager接口
|
|||
|
|
interface IYTPoolManager {
|
|||
|
|
function addLiquidity(address _token, uint256 _amount, uint256 _minUsdy, uint256 _minYtLP) external returns (uint256);
|
|||
|
|
function removeLiquidity(address _tokenOut, uint256 _ytLPAmount, uint256 _minOut, address _receiver) external returns (uint256);
|
|||
|
|
function getAumInUsdy(bool _maximise) external view returns (uint256);
|
|||
|
|
function getPrice(bool _maximise) external view returns (uint256);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// YTRewardRouter接口
|
|||
|
|
interface IYTRewardRouter {
|
|||
|
|
function addLiquidity(address _token, uint256 _amount, uint256 _minUsdy, uint256 _minYtLP) external returns (uint256);
|
|||
|
|
function removeLiquidity(address _tokenOut, uint256 _ytLPAmount, uint256 _minOut, address _receiver) external returns (uint256);
|
|||
|
|
function swapYT(address _tokenIn, address _tokenOut, uint256 _amountIn, uint256 _minOut, address _receiver) external returns (uint256);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. 合约列表
|
|||
|
|
|
|||
|
|
| 合约名称 | 文件 | 说明 |
|
|||
|
|
|---------|------|------|
|
|||
|
|
| USDY | USDY.sol | 统一计价代币 |
|
|||
|
|
| YTLPToken | YTLPToken.sol | LP代币 |
|
|||
|
|
| YTVault | YTVault.sol | 核心资金池 |
|
|||
|
|
| YTPoolManager | YTPoolManager.sol | 流动性管理 |
|
|||
|
|
| YTRewardRouter | YTRewardRouter.sol | 用户入口 |
|
|||
|
|
| YTPriceFeed | YTPriceFeed.sol | 价格读取器 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 附录
|
|||
|
|
|
|||
|
|
### A. 依赖库
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"dependencies": {
|
|||
|
|
"@openzeppelin/contracts": "^4.9.0"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### B. 参考资源
|
|||
|
|
|
|||
|
|
- GMX源码: https://github.com/gmx-io/gmx-contracts
|
|||
|
|
- GMX文档: https://gmxio.gitbook.io/gmx/
|
|||
|
|
- OpenZeppelin: https://docs.openzeppelin.com/
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. 安全改进总结
|
|||
|
|
|
|||
|
|
### 11.1 针对GMX V1漏洞的防护
|
|||
|
|
|
|||
|
|
✅ **访问控制严格**
|
|||
|
|
- 所有关键函数(`buyUSDY`, `sellUSDY`, `swap`)都有`onlyPoolManager`修饰符
|
|||
|
|
- 防止外部直接调用核心函数
|
|||
|
|
- 不存在GMX V1的`increasePosition`直接调用漏洞
|
|||
|
|
|
|||
|
|
✅ **价格计算独立**
|
|||
|
|
- 价格来自外部YT合约,不在Vault内部维护可被操纵的状态
|
|||
|
|
- 没有"全局空头平均价格"这种易被操纵的内部变量
|
|||
|
|
- AUM计算简单透明:`poolAmounts * price`
|
|||
|
|
|
|||
|
|
### 11.2 关键安全修复
|
|||
|
|
|
|||
|
|
#### 🔴 修复1: _transferIn函数
|
|||
|
|
**问题**: 原设计中两次余额查询在同一时刻,差值永远为0
|
|||
|
|
**修复**: 添加`tokenBalances`映射跟踪上次余额,正确计算转入金额
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// 修复前(错误)
|
|||
|
|
uint256 prevBalance = IERC20(_token).balanceOf(address(this));
|
|||
|
|
uint256 nextBalance = IERC20(_token).balanceOf(address(this));
|
|||
|
|
return nextBalance - prevBalance; // 永远为0!
|
|||
|
|
|
|||
|
|
// 修复后(正确)
|
|||
|
|
uint256 prevBalance = tokenBalances[_token];
|
|||
|
|
uint256 nextBalance = IERC20(_token).balanceOf(address(this));
|
|||
|
|
tokenBalances[_token] = nextBalance;
|
|||
|
|
return nextBalance - prevBalance;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 🟠 修复2: 价格操纵保护
|
|||
|
|
**问题**: 外部YT价格可能被操纵
|
|||
|
|
**修复**:
|
|||
|
|
- 价格变动限制(默认5%)
|
|||
|
|
- 价格时效性检查(最长1小时)
|
|||
|
|
- 断路器机制
|
|||
|
|
- 价格历史记录
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// 断路器检查
|
|||
|
|
if (priceDiff > maxDiff) {
|
|||
|
|
emit CircuitBreakerTriggered(_token, oldPrice, _newPrice);
|
|||
|
|
revert("YTPriceFeed: price change too large");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 🟠 修复3: USDY铸造权限
|
|||
|
|
**问题**: YTPoolManager可以随意铸造USDY
|
|||
|
|
**修复**:
|
|||
|
|
- 移除直接铸造权限
|
|||
|
|
- 要求预先储备USDY
|
|||
|
|
- 添加储备金管理函数
|
|||
|
|
- 95%储备金要求作为安全底线
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
// 不再随意铸造
|
|||
|
|
// IUSDY(usdy).mint(address(this), needed); // 已移除
|
|||
|
|
|
|||
|
|
// 改为检查储备
|
|||
|
|
require(usdyBalance >= usdyAmount * 95 / 100, "insufficient reserve");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 🟡 修复4: 全局滑点保护
|
|||
|
|
**新增功能**:
|
|||
|
|
- 最大滑点限制(默认10%)
|
|||
|
|
- 单笔交易限额
|
|||
|
|
- 每日交易量限制
|
|||
|
|
- 实时滑点计算和验证
|
|||
|
|
|
|||
|
|
```solidity
|
|||
|
|
function _validateSwapSlippage(...) {
|
|||
|
|
uint256 slippage = (expectedOut - actualOut) * 10000 / expectedOut;
|
|||
|
|
require(slippage <= maxSwapSlippageBps, "slippage too high");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 🟡 修复5: 紧急暂停机制
|
|||
|
|
**新增功能**:
|
|||
|
|
- `emergencyMode`布尔标志
|
|||
|
|
- `notInEmergency`修饰符应用于所有核心函数
|
|||
|
|
- 紧急状态下可提取资金
|
|||
|
|
- 独立的swap开关`isSwapEnabled`
|
|||
|
|
|
|||
|
|
### 11.3 多层防护体系
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
第一层: 访问控制
|
|||
|
|
├─ onlyPoolManager修饰符
|
|||
|
|
├─ onlyGov管理员控制
|
|||
|
|
└─ notInEmergency紧急保护
|
|||
|
|
|
|||
|
|
第二层: 价格保护
|
|||
|
|
├─ 价格变动限制(5%)
|
|||
|
|
├─ 价格时效性检查(1小时)
|
|||
|
|
├─ 断路器自动触发
|
|||
|
|
└─ 价格历史记录
|
|||
|
|
|
|||
|
|
第三层: 交易限制
|
|||
|
|
├─ 单笔交易限额
|
|||
|
|
├─ 每日交易量限制
|
|||
|
|
├─ 全局滑点保护
|
|||
|
|
└─ 冷却期机制(15分钟)
|
|||
|
|
|
|||
|
|
第四层: 余额验证
|
|||
|
|
├─ tokenBalances精确跟踪
|
|||
|
|
├─ poolAmounts验证
|
|||
|
|
├─ USDY储备金检查
|
|||
|
|
└─ 池子余额一致性验证
|
|||
|
|
|
|||
|
|
第五层: 紧急响应
|
|||
|
|
├─ 紧急模式开关
|
|||
|
|
├─ Swap功能开关
|
|||
|
|
├─ 资金提取功能
|
|||
|
|
└─ 治理控制
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 11.4 安全最佳实践
|
|||
|
|
|
|||
|
|
1. **重入保护**: 所有外部调用函数都有`nonReentrant`修饰符
|
|||
|
|
2. **滑点保护**: 所有用户操作都需要`_minOut`参数
|
|||
|
|
3. **冷却期**: 15分钟防闪电贷攻击
|
|||
|
|
4. **权限分离**: Gov和PoolManager职责明确
|
|||
|
|
5. **状态同步**: tokenBalances确保余额一致性
|
|||
|
|
6. **价格验证**: 多重价格检查机制
|
|||
|
|
7. **限额控制**: 单笔和每日交易限制
|
|||
|
|
8. **紧急机制**: 多层次的暂停和恢复功能
|
|||
|
|
|
|||
|
|
### 11.5 与GMX V1的对比
|
|||
|
|
|
|||
|
|
| 安全特性 | GMX V1 | YT Pool (修复后) |
|
|||
|
|
|---------|--------|------------------|
|
|||
|
|
| 核心函数访问控制 | ❌ 可被任意调用 | ✅ 严格限制 |
|
|||
|
|
| 价格计算 | ❌ 内部可操纵 | ✅ 外部独立 |
|
|||
|
|
| 价格保护 | ❌ 无 | ✅ 多层保护 |
|
|||
|
|
| 余额跟踪 | ✅ 正确 | ✅ 正确(已修复) |
|
|||
|
|
| 滑点保护 | ⚠️ 基础 | ✅ 全局保护 |
|
|||
|
|
| 紧急机制 | ✅ 有 | ✅ 增强版 |
|
|||
|
|
| 交易限额 | ❌ 无 | ✅ 多重限制 |
|
|||
|
|
| 价格断路器 | ❌ 无 | ✅ 自动触发 |
|
|||
|
|
|
|||
|
|
### 11.6 部署前检查清单
|
|||
|
|
|
|||
|
|
- [ ] 确认所有YT代币实现了`assetPrice()`和`lastPriceUpdate()`接口
|
|||
|
|
- [ ] 设置合理的价格更新间隔(建议5-15分钟)
|
|||
|
|
- [ ] 配置价格变动限制(建议3-5%)
|
|||
|
|
- [ ] 设置单笔交易限额
|
|||
|
|
- [ ] 配置每日交易量限制
|
|||
|
|
- [ ] 准备足够的USDY储备金
|
|||
|
|
- [ ] 测试紧急暂停机制
|
|||
|
|
- [ ] 验证所有访问控制权限
|
|||
|
|
- [ ] 进行全面的安全审计
|
|||
|
|
- [ ] 部署价格监控系统
|
|||
|
|
- [ ] 配置多签钱包作为Gov
|
|||
|
|
|
|||
|
|
### 11.7 运维建议
|
|||
|
|
|
|||
|
|
1. **价格监控**: 部署自动化系统监控YT代币价格异常
|
|||
|
|
2. **储备金管理**: 保持足够的USDY储备(建议>总供应量的10%)
|
|||
|
|
3. **定期审查**: 每周审查交易量、手续费和池子平衡
|
|||
|
|
4. **断路器测试**: 定期测试紧急暂停功能
|
|||
|
|
5. **权限管理**: 使用多签钱包管理Gov权限
|
|||
|
|
6. **升级计划**: 预留升级路径和迁移方案
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**文档版本**: v4.0 (安全增强版)
|
|||
|
|
**更新日期**: 2025-12-10
|
|||
|
|
**适用项目**: YT流动性池系统
|
|||
|
|
**安全状态**: ✅ 已修复所有已知漏洞
|