Files
assetxContracts/contracts/ytLp/core/YTPoolManager.sol
2026-01-12 14:33:16 +08:00

296 lines
9.7 KiB
Solidity
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../../interfaces/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;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
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);
event GovChanged(address indexed oldGov, address indexed newGov);
event AumAdjustmentChanged(uint256 addition, uint256 deduction);
event CooldownInherited(address indexed from, address indexed to, uint256 cooldownTime);
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();
address oldGov = gov;
gov = _gov;
emit GovChanged(oldGov, _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;
emit AumAdjustmentChanged(_addition, _deduction);
}
/**
* @notice LP 代币转账时的回调函数
* @param _from 发送方地址
* @param _to 接收方地址
* @dev 当 LP 代币转账时,接收方继承发送方的冷却时间,防止绕过冷却期
*/
function onLPTransfer(address _from, address _to) external {
// 只允许 ytLP 代币合约调用
if (msg.sender != ytLP) revert Forbidden();
// 如果发送方有冷却时间记录,且接收方的冷却时间更早(或没有记录)
// 则将发送方的冷却时间继承给接收方
if (lastAddedAt[_from] > 0 && lastAddedAt[_to] < lastAddedAt[_from]) {
lastAddedAt[_to] = lastAddedAt[_from];
emit CooldownInherited(_from, _to, lastAddedAt[_from]);
}
}
/**
* @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).transferFrom(_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; // aumAddition是协议额外增加的AUM用来“预留风险缓冲 / 扣除潜在负债”
if (aum > aumDeduction) {
aum -= aumDeduction;
} else {
aum = 0;
}
return aum;
}
/**
* @dev 预留存储空间,用于未来升级时添加新的状态变量
* 50个slot = 50 * 32 bytes = 1600 bytes
*/
uint256[50] private __gap;
}