2025-12-18 13:07:35 +08:00
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
pragma solidity ^0.8.0;
|
|
|
|
|
|
|
|
|
|
|
|
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
|
|
|
|
import "../../interfaces/IYTVault.sol";
|
|
|
|
|
|
import "../../interfaces/IYTLPToken.sol";
|
|
|
|
|
|
import "../../interfaces/IUSDY.sol";
|
|
|
|
|
|
|
|
|
|
|
|
contract YTPoolManager is Initializable, UUPSUpgradeable, ReentrancyGuardUpgradeable {
|
|
|
|
|
|
using SafeERC20 for IERC20;
|
|
|
|
|
|
|
2025-12-23 14:05:41 +08:00
|
|
|
|
/// @custom:oz-upgrades-unsafe-allow constructor
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
_disableInitializers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:07:35 +08:00
|
|
|
|
error Forbidden();
|
|
|
|
|
|
error 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);
|
2025-12-23 14:05:41 +08:00
|
|
|
|
event GovChanged(address indexed oldGov, address indexed newGov);
|
|
|
|
|
|
event AumAdjustmentChanged(uint256 addition, uint256 deduction);
|
2025-12-18 13:07:35 +08:00
|
|
|
|
|
|
|
|
|
|
modifier onlyGov() {
|
|
|
|
|
|
if (msg.sender != gov) revert Forbidden();
|
|
|
|
|
|
_;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modifier onlyHandler() {
|
|
|
|
|
|
if (!isHandler[msg.sender] && msg.sender != gov) revert Forbidden();
|
|
|
|
|
|
_;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _authorizeUpgrade(address newImplementation) internal override onlyGov {}
|
|
|
|
|
|
|
|
|
|
|
|
function setGov(address _gov) external onlyGov {
|
|
|
|
|
|
if (_gov == address(0)) revert InvalidAddress();
|
2025-12-23 14:05:41 +08:00
|
|
|
|
address oldGov = gov;
|
2025-12-18 13:07:35 +08:00
|
|
|
|
gov = _gov;
|
2025-12-23 14:05:41 +08:00
|
|
|
|
emit GovChanged(oldGov, _gov);
|
2025-12-18 13:07:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function 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;
|
2025-12-23 14:05:41 +08:00
|
|
|
|
emit AumAdjustmentChanged(_addition, _deduction);
|
2025-12-18 13:07:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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(_maximise);
|
|
|
|
|
|
|
2025-12-24 16:41:26 +08:00
|
|
|
|
aum += aumAddition; // aumAddition是协议额外增加的AUM,用来“预留风险缓冲 / 扣除潜在负债”
|
2025-12-18 13:07:35 +08:00
|
|
|
|
if (aum > aumDeduction) {
|
|
|
|
|
|
aum -= aumDeduction;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
aum = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return aum;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uint256[50] private __gap;
|
2025-12-25 13:29:35 +08:00
|
|
|
|
}
|