Files
assetxContracts/test/YTLp.t.sol

1704 lines
60 KiB
Solidity
Raw Permalink Normal View History

2025-12-18 13:07:35 +08:00
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
2025-12-23 14:05:41 +08:00
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
2025-12-24 16:41:26 +08:00
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
2025-12-18 13:07:35 +08:00
import "../contracts/ytLp/tokens/USDY.sol";
import "../contracts/ytLp/tokens/YTLPToken.sol";
import "../contracts/ytLp/core/YTPriceFeed.sol";
import "../contracts/ytLp/core/YTVault.sol";
import "../contracts/ytLp/core/YTPoolManager.sol";
import "../contracts/ytLp/core/YTRewardRouter.sol";
2025-12-23 14:05:41 +08:00
import "../contracts/ytVault/YTAssetVault.sol";
import "../contracts/ytVault/YTAssetFactory.sol";
2025-12-24 16:41:26 +08:00
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
// Mock USDC token for testing (18 decimals like on BSC)
contract MockUSDC is ERC20 {
constructor() ERC20("USD Coin", "USDC") {
_mint(msg.sender, 100000000 * 1e18);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function decimals() public pure override returns (uint8) {
return 18; // BSC USDC uses 18 decimals
}
}
// Mock Chainlink Price Feed
contract MockChainlinkPriceFeed is AggregatorV3Interface {
int256 private _price;
uint8 private _decimals;
constructor(int256 initialPrice) {
_price = initialPrice;
_decimals = 8; // Chainlink standard
}
function decimals() external view override returns (uint8) {
return _decimals;
}
function description() external pure override returns (string memory) {
return "Mock USDC/USD Price Feed";
}
function version() external pure override returns (uint256) {
return 1;
}
function getRoundData(uint80)
external
view
override
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
return (0, _price, block.timestamp, block.timestamp, 0);
}
function latestRoundData()
external
view
override
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
return (0, _price, block.timestamp, block.timestamp, 0);
}
// Helper function to update price in tests
function updatePrice(int256 newPrice) external {
_price = newPrice;
}
}
2025-12-18 13:07:35 +08:00
contract YTLpTest is Test {
address deployer;
address user1;
address user2;
address user3;
USDY usdy;
YTLPToken ytlp;
YTPriceFeed priceFeed;
YTVault vault;
YTPoolManager poolManager;
YTRewardRouter router;
2025-12-24 16:41:26 +08:00
MockUSDC usdc;
MockChainlinkPriceFeed usdcPriceFeed;
2025-12-18 13:07:35 +08:00
YTAssetFactory factory;
YTAssetVault ytTokenA;
YTAssetVault ytTokenB;
YTAssetVault ytTokenC;
uint256 constant PRICE_PRECISION = 1e30;
2025-12-24 16:41:26 +08:00
uint256 constant CHAINLINK_PRECISION = 1e8;
uint256 constant INITIAL_USDC_PRICE = 1e8; // $1.00 in Chainlink format
2025-12-18 13:07:35 +08:00
uint256 constant BASIS_POINTS = 10000;
function setUp() public {
deployer = address(this);
user1 = address(0x1);
user2 = address(0x2);
user3 = address(0x3);
vm.deal(user1, 100 ether);
vm.deal(user2, 100 ether);
vm.deal(user3, 100 ether);
2025-12-24 16:41:26 +08:00
// 部署 Mock USDC (18 decimals)
usdc = new MockUSDC();
// 部署 Mock Chainlink Price Feed
usdcPriceFeed = new MockChainlinkPriceFeed(int256(INITIAL_USDC_PRICE));
2025-12-23 14:05:41 +08:00
2025-12-24 16:41:26 +08:00
// 使用代理模式部署 USDY
2025-12-23 14:05:41 +08:00
USDY usdyImpl = new USDY();
bytes memory usdyInitData = abi.encodeWithSelector(USDY.initialize.selector);
ERC1967Proxy usdyProxy = new ERC1967Proxy(address(usdyImpl), usdyInitData);
usdy = USDY(address(usdyProxy));
2025-12-24 16:41:26 +08:00
// 使用代理模式部署 YTLPToken
2025-12-23 14:05:41 +08:00
YTLPToken ytlpImpl = new YTLPToken();
bytes memory ytlpInitData = abi.encodeWithSelector(YTLPToken.initialize.selector);
ERC1967Proxy ytlpProxy = new ERC1967Proxy(address(ytlpImpl), ytlpInitData);
ytlp = YTLPToken(address(ytlpProxy));
2025-12-24 16:41:26 +08:00
// 使用代理模式部署 YTPriceFeed
2025-12-23 14:05:41 +08:00
YTPriceFeed priceFeedImpl = new YTPriceFeed();
bytes memory priceFeedInitData = abi.encodeWithSelector(
YTPriceFeed.initialize.selector,
2025-12-24 16:41:26 +08:00
address(usdc),
address(usdcPriceFeed)
2025-12-23 14:05:41 +08:00
);
ERC1967Proxy priceFeedProxy = new ERC1967Proxy(address(priceFeedImpl), priceFeedInitData);
priceFeed = YTPriceFeed(address(priceFeedProxy));
2025-12-18 13:07:35 +08:00
2025-12-24 16:41:26 +08:00
// 使用代理模式部署 YTVault
2025-12-23 14:05:41 +08:00
YTVault vaultImpl = new YTVault();
bytes memory vaultInitData = abi.encodeWithSelector(
YTVault.initialize.selector,
address(usdy),
address(priceFeed)
);
ERC1967Proxy vaultProxy = new ERC1967Proxy(address(vaultImpl), vaultInitData);
vault = YTVault(address(vaultProxy));
2025-12-18 13:07:35 +08:00
2025-12-24 16:41:26 +08:00
// 使用代理模式部署 YTPoolManager
2025-12-23 14:05:41 +08:00
YTPoolManager poolManagerImpl = new YTPoolManager();
bytes memory poolManagerInitData = abi.encodeWithSelector(
YTPoolManager.initialize.selector,
2025-12-18 13:07:35 +08:00
address(vault),
address(usdy),
address(ytlp),
15 * 60
);
2025-12-23 14:05:41 +08:00
ERC1967Proxy poolManagerProxy = new ERC1967Proxy(address(poolManagerImpl), poolManagerInitData);
poolManager = YTPoolManager(address(poolManagerProxy));
2025-12-18 13:07:35 +08:00
2025-12-24 16:41:26 +08:00
// 使用代理模式部署 YTRewardRouter
2025-12-23 14:05:41 +08:00
YTRewardRouter routerImpl = new YTRewardRouter();
bytes memory routerInitData = abi.encodeWithSelector(
YTRewardRouter.initialize.selector,
2025-12-18 13:07:35 +08:00
address(usdy),
address(ytlp),
address(poolManager),
address(vault)
);
2025-12-23 14:05:41 +08:00
ERC1967Proxy routerProxy = new ERC1967Proxy(address(routerImpl), routerInitData);
router = YTRewardRouter(address(routerProxy));
2025-12-18 13:07:35 +08:00
2025-12-24 16:41:26 +08:00
// 部署YTAssetVault实现合约不初始化
2025-12-23 14:05:41 +08:00
YTAssetVault ytVaultImpl = new YTAssetVault();
2025-12-18 13:07:35 +08:00
2025-12-24 16:41:26 +08:00
// 使用代理模式部署 YTAssetFactory
2025-12-23 14:05:41 +08:00
YTAssetFactory factoryImpl = new YTAssetFactory();
bytes memory factoryInitData = abi.encodeWithSelector(
YTAssetFactory.initialize.selector,
address(ytVaultImpl),
2025-12-18 13:07:35 +08:00
1000000 ether // defaultHardCap
);
2025-12-23 14:05:41 +08:00
ERC1967Proxy factoryProxy = new ERC1967Proxy(address(factoryImpl), factoryInitData);
factory = YTAssetFactory(address(factoryProxy));
2025-12-18 13:07:35 +08:00
// 通过factory创建YTAssetVault代币
address ytTokenAAddr = factory.createVault(
"YT Token A",
"YT-A",
deployer, // manager
1000000 ether, // hardCap
2025-12-24 16:41:26 +08:00
address(usdc),
2025-12-18 13:07:35 +08:00
block.timestamp + 365 days, // redemptionTime
2025-12-24 16:41:26 +08:00
PRICE_PRECISION, // initialYtPrice
address(usdcPriceFeed) // usdcPriceFeed
2025-12-18 13:07:35 +08:00
);
ytTokenA = YTAssetVault(ytTokenAAddr);
address ytTokenBAddr = factory.createVault(
"YT Token B",
"YT-B",
deployer,
1000000 ether,
2025-12-24 16:41:26 +08:00
address(usdc),
2025-12-18 13:07:35 +08:00
block.timestamp + 365 days,
PRICE_PRECISION,
2025-12-24 16:41:26 +08:00
address(usdcPriceFeed)
2025-12-18 13:07:35 +08:00
);
ytTokenB = YTAssetVault(ytTokenBAddr);
address ytTokenCAddr = factory.createVault(
"YT Token C",
"YT-C",
deployer,
1000000 ether,
2025-12-24 16:41:26 +08:00
address(usdc),
2025-12-18 13:07:35 +08:00
block.timestamp + 365 days,
PRICE_PRECISION,
2025-12-24 16:41:26 +08:00
address(usdcPriceFeed)
2025-12-18 13:07:35 +08:00
);
ytTokenC = YTAssetVault(ytTokenCAddr);
// 配置权限
usdy.addVault(address(vault));
usdy.addVault(address(poolManager));
ytlp.setMinter(address(poolManager), true);
vault.setPoolManager(address(poolManager));
vault.setSwapper(address(router), true);
poolManager.setHandler(address(router), true);
// 配置参数
vault.setSwapFees(30, 4, 50, 20); // 0.3%, 0.04%, 动态税率
vault.setDynamicFees(false); // 先关闭动态费率,便于精确测试
vault.setMaxSwapSlippageBps(1000);
priceFeed.setMaxPriceChangeBps(500);
// 配置白名单
vault.setWhitelistedToken(address(ytTokenA), 18, 4000, 45000000 ether, false);
vault.setWhitelistedToken(address(ytTokenB), 18, 3000, 35000000 ether, false);
vault.setWhitelistedToken(address(ytTokenC), 18, 2000, 25000000 ether, false);
// 初始化价格 $1.00
priceFeed.forceUpdatePrice(address(ytTokenA), 1e30);
priceFeed.forceUpdatePrice(address(ytTokenB), 1e30);
priceFeed.forceUpdatePrice(address(ytTokenC), 1e30);
// 不设置价差(便于精确计算)
2025-12-24 16:41:26 +08:00
// 为测试用户铸造YT代币需要先给合约USDC再depositYT
2025-12-18 13:07:35 +08:00
uint256 initialAmount = 10000 ether;
2025-12-24 16:41:26 +08:00
// 给deployer一些USDC用于购买YT
usdc.mint(deployer, 30000 ether);
2025-12-18 13:07:35 +08:00
// Deployer购买YT代币
2025-12-24 16:41:26 +08:00
usdc.approve(address(ytTokenA), initialAmount);
2025-12-18 13:07:35 +08:00
ytTokenA.depositYT(initialAmount);
2025-12-24 16:41:26 +08:00
usdc.approve(address(ytTokenB), initialAmount);
2025-12-18 13:07:35 +08:00
ytTokenB.depositYT(initialAmount);
2025-12-24 16:41:26 +08:00
usdc.approve(address(ytTokenC), initialAmount);
2025-12-18 13:07:35 +08:00
ytTokenC.depositYT(initialAmount);
// 转账给用户
ytTokenA.transfer(user1, 5000 ether);
ytTokenB.transfer(user1, 5000 ether);
ytTokenC.transfer(user1, 5000 ether);
ytTokenA.transfer(user2, 3000 ether);
ytTokenB.transfer(user2, 3000 ether);
2025-12-24 16:41:26 +08:00
// 给用户一些USDC用于后续可能的操作
usdc.mint(user1, 10000 ether);
usdc.mint(user2, 10000 ether);
usdc.mint(user3, 10000 ether);
2025-12-18 13:07:35 +08:00
}
// ==================== 1. 部署和初始化测试 ====================
function test_01_DeployContracts() public view {
assertEq(usdy.name(), "YT USD");
assertEq(usdy.symbol(), "USDY");
assertEq(usdy.decimals(), 18);
assertEq(ytlp.name(), "YT Liquidity Provider");
assertEq(ytlp.symbol(), "ytLP");
assertEq(ytlp.decimals(), 18);
assertEq(vault.ytPoolManager(), address(poolManager));
assertEq(poolManager.ytVault(), address(vault));
}
function test_02_ConfigurePermissions() public view {
assertTrue(usdy.vaults(address(vault)));
assertTrue(usdy.vaults(address(poolManager)));
assertTrue(ytlp.isMinter(address(poolManager)));
assertTrue(poolManager.isHandler(address(router)));
assertTrue(vault.isSwapper(address(router)));
}
function test_03_ConfigureWhitelist() public view {
assertTrue(vault.whitelistedTokens(address(ytTokenA)));
assertTrue(vault.whitelistedTokens(address(ytTokenB)));
assertTrue(vault.whitelistedTokens(address(ytTokenC)));
assertEq(vault.tokenWeights(address(ytTokenA)), 4000);
assertEq(vault.tokenWeights(address(ytTokenB)), 3000);
assertEq(vault.tokenWeights(address(ytTokenC)), 2000);
assertEq(vault.totalTokenWeights(), 9000);
assertFalse(vault.stableTokens(address(ytTokenA)));
assertTrue(vault.stableTokens(address(usdy))); // USDY被标记为稳定币
}
function test_04_ConfigureFees() public view {
assertEq(vault.swapFeeBasisPoints(), 30); // 0.3%
assertEq(vault.stableSwapFeeBasisPoints(), 4); // 0.04%
assertEq(vault.taxBasisPoints(), 50);
assertEq(vault.stableTaxBasisPoints(), 20);
assertFalse(vault.hasDynamicFees()); // 已关闭便于测试
}
function test_05_YTAssetVaultBasics() public view {
assertEq(ytTokenA.name(), "YT Token A");
assertEq(ytTokenA.symbol(), "YT-A");
assertEq(ytTokenA.ytPrice(), PRICE_PRECISION);
}
// ==================== 2. 添加流动性测试(精确计算)====================
function test_06_FirstAddLiquidity() public {
uint256 depositAmount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), depositAmount);
uint256 ytLPBefore = ytlp.balanceOf(user1);
assertEq(ytLPBefore, 0);
// 添加流动性
uint256 ytLPReceived = router.addLiquidity(
address(ytTokenA),
depositAmount,
0,
0
);
vm.stopPrank();
// 精确计算预期值无价差0.3%手续费)
// 存入: 1000 YT-A @ $1.00
// 手续费: 1000 × 0.3% = 3 个代币
// 扣费后: 997 个代币
// USDY价值: 997 × $1.00 = 997 USDY
// 首次铸造: ytLP = USDY = 997 ether
uint256 expectedYtLP = 997 ether;
assertEq(ytLPReceived, expectedYtLP, "ytLP amount incorrect");
assertEq(ytlp.balanceOf(user1), expectedYtLP, "user1 balance incorrect");
assertEq(ytlp.totalSupply(), expectedYtLP, "total supply incorrect");
// 验证池子状态
assertEq(vault.poolAmounts(address(ytTokenA)), depositAmount, "pool amount incorrect");
assertEq(vault.usdyAmounts(address(ytTokenA)), expectedYtLP, "usdy amount incorrect");
// 验证ytLP价格
uint256 ytLPPrice = poolManager.getPrice(true);
// AUM = 1000 (池子有1000个代币) × $1.00 = $1000
// Supply = 997 ytLP
// Price = AUM * 1e18 / Supply (带18位精度)
assertTrue(ytLPPrice > 1 ether, "ytLP price should be > $1");
}
function test_07_SecondAddLiquidity() public {
// 用户1先添加
uint256 firstAmount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), firstAmount);
router.addLiquidity(address(ytTokenA), firstAmount, 0, 0);
vm.stopPrank();
uint256 user1YtLP = ytlp.balanceOf(user1); // 997 ether
// 用户2添加
uint256 secondAmount = 2000 ether;
vm.startPrank(user2);
ytTokenB.approve(address(router), secondAmount);
uint256 ytLPReceived = router.addLiquidity(
address(ytTokenB),
secondAmount,
0,
0
);
vm.stopPrank();
// 精确计算
uint256 expectedYtLP = 1988.018 ether;
assertEq(ytLPReceived, expectedYtLP, "second add ytLP amount incorrect");
assertEq(ytlp.balanceOf(user2), expectedYtLP, "user2 balance incorrect");
assertEq(ytlp.totalSupply(), user1YtLP + expectedYtLP, "total supply incorrect");
}
function test_08_AddLiquiditySlippageProtection() public {
uint256 amount = 1000 ether;
uint256 tooHighMinYtLP = 1500 ether; // 设置过高的最小值
vm.startPrank(user1);
ytTokenA.approve(address(router), amount);
vm.expectRevert(abi.encodeWithSignature("InsufficientOutput()"));
router.addLiquidity(
address(ytTokenA),
amount,
0,
tooHighMinYtLP
);
vm.stopPrank();
}
// ==================== 3. 移除流动性测试 ====================
function test_09_RemoveLiquidity() public {
// 先添加流动性
uint256 addAmount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), addAmount);
router.addLiquidity(address(ytTokenA), addAmount, 0, 0);
uint256 ytLPBalance = ytlp.balanceOf(user1); // 997 ether
// 等待冷却期
vm.warp(block.timestamp + 15 * 60 + 1);
uint256 tokenBalanceBefore = ytTokenA.balanceOf(user1);
// 移除流动性
uint256 amountOut = router.removeLiquidity(
address(ytTokenA),
ytLPBalance,
0,
user1
);
vm.stopPrank();
2025-12-24 16:41:26 +08:00
uint256 expectedOut = 997 ether; // 这里能够取出997是因为池子中只有一个用户user1的997个ytLp代币价值1000USDY卖出后获得997个YT-A代币
2025-12-18 13:07:35 +08:00
assertEq(amountOut, expectedOut, "remove liquidity amount incorrect");
assertEq(ytTokenA.balanceOf(user1), tokenBalanceBefore + expectedOut, "user1 final balance incorrect");
assertEq(ytlp.balanceOf(user1), 0, "ytLP should be burned");
assertEq(ytlp.totalSupply(), 0, "ytLP supply should be 0");
}
function test_10_RemoveLiquidityCooldownProtection() public {
uint256 amount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), amount);
router.addLiquidity(address(ytTokenA), amount, 0, 0);
uint256 ytLPBalance = ytlp.balanceOf(user1);
// 不等待冷却期,直接移除
vm.expectRevert(abi.encodeWithSignature("CooldownNotPassed()"));
router.removeLiquidity(address(ytTokenA), ytLPBalance, 0, user1);
vm.stopPrank();
}
// ==================== 4. Swap测试 ====================
function test_11_SwapYTTokens() public {
// 先为池子添加流动性
uint256 liquidityAmount = 2000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), liquidityAmount);
router.addLiquidity(address(ytTokenA), liquidityAmount, 0, 0);
ytTokenB.approve(address(router), liquidityAmount);
router.addLiquidity(address(ytTokenB), liquidityAmount, 0, 0);
vm.stopPrank();
// Swap测试
uint256 swapAmount = 100 ether;
vm.startPrank(user2);
ytTokenA.approve(address(router), swapAmount);
uint256 balanceBBefore = ytTokenB.balanceOf(user2);
uint256 amountOut = router.swapYT(
address(ytTokenA),
address(ytTokenB),
swapAmount,
0,
user2
);
vm.stopPrank();
uint256 expectedOut = 99.7 ether;
assertEq(amountOut, expectedOut, "swap amount incorrect");
assertEq(ytTokenB.balanceOf(user2), balanceBBefore + expectedOut, "user2 balance incorrect");
}
function test_12_SwapSameTokenReverts() public {
uint256 amount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), amount);
router.addLiquidity(address(ytTokenA), amount, 0, 0);
ytTokenA.approve(address(router), 100 ether);
vm.expectRevert(abi.encodeWithSignature("SameToken()"));
router.swapYT(address(ytTokenA), address(ytTokenA), 100 ether, 0, user1);
vm.stopPrank();
}
// ==================== 5. 价格测试 ====================
function test_13_PriceWithoutSpread() public view {
// 未设置价差时
uint256 price = priceFeed.getPrice(address(ytTokenA), true);
assertEq(price, 1e30, "price should be $1.00");
uint256 maxPrice = priceFeed.getMaxPrice(address(ytTokenA));
uint256 minPrice = priceFeed.getMinPrice(address(ytTokenA));
assertEq(maxPrice, 1e30, "maxPrice should equal base price");
assertEq(minPrice, 1e30, "minPrice should equal base price");
}
function test_14_PriceWithSpread() public {
// 设置0.2%价差 (20 bps)
priceFeed.setSpreadBasisPoints(address(ytTokenA), 20);
uint256 basePrice = 1e30; // $1.00
uint256 maxPrice = priceFeed.getMaxPrice(address(ytTokenA));
uint256 minPrice = priceFeed.getMinPrice(address(ytTokenA));
// MaxPrice = $1.00 × 1.002 = $1.002
uint256 expectedMax = basePrice * 10020 / 10000;
assertEq(maxPrice, expectedMax, "maxPrice with spread incorrect");
// MinPrice = $1.00 × 0.998 = $0.998
uint256 expectedMin = basePrice * 9980 / 10000;
assertEq(minPrice, expectedMin, "minPrice with spread incorrect");
// 清除价差设置
priceFeed.setSpreadBasisPoints(address(ytTokenA), 0);
}
2025-12-24 16:41:26 +08:00
function test_15_USDCPriceFromChainlink() public view {
// USDC价格应该从Chainlink读取
uint256 usdcPrice = priceFeed.getPrice(address(usdc), true);
2025-12-18 13:07:35 +08:00
2025-12-24 16:41:26 +08:00
// 应该等于 $1.00 (转换为 1e30 精度)
// Chainlink 返回 1e8需要转换为 1e30: 1e8 * 1e22 = 1e30
assertEq(usdcPrice, PRICE_PRECISION, "USDC price should be 1.0");
2025-12-18 13:07:35 +08:00
}
// ==================== 6. YTAssetVault特定功能测试 ====================
function test_16_UpdateYTPrices() public {
uint256 newYtPrice = 1.05e30; // $1.05
2025-12-24 16:41:26 +08:00
// 通过factory更新价格USDC价格从Chainlink获取只更新ytPrice
factory.updateVaultPrices(address(ytTokenA), newYtPrice);
2025-12-18 13:07:35 +08:00
assertEq(ytTokenA.ytPrice(), newYtPrice, "ytPrice update failed");
// 重置价格
2025-12-24 16:41:26 +08:00
factory.updateVaultPrices(address(ytTokenA), PRICE_PRECISION);
2025-12-18 13:07:35 +08:00
}
2025-12-24 16:41:26 +08:00
function test_17_BuyYTWithUSDC() public {
uint256 usdcAmount = 1000 ether;
2025-12-18 13:07:35 +08:00
vm.startPrank(user1);
2025-12-24 16:41:26 +08:00
usdc.approve(address(ytTokenA), usdcAmount);
2025-12-18 13:07:35 +08:00
uint256 ytBefore = ytTokenA.balanceOf(user1);
2025-12-24 16:41:26 +08:00
uint256 ytReceived = ytTokenA.depositYT(usdcAmount);
2025-12-18 13:07:35 +08:00
uint256 ytAfter = ytTokenA.balanceOf(user1);
vm.stopPrank();
// 价格都是1.0应该1:1兑换
2025-12-24 16:41:26 +08:00
assertEq(ytReceived, usdcAmount, "YT amount should equal USDC amount");
assertEq(ytAfter - ytBefore, usdcAmount, "YT balance incorrect");
2025-12-18 13:07:35 +08:00
}
function test_18_HardCapProtection() public {
// 获取当前totalSupply
uint256 currentSupply = ytTokenA.totalSupply(); // 10000 ether
// 通过factory设置hardCap为当前供应量 + 500允许少量增加
uint256 newHardCap = currentSupply + 500 ether;
factory.setHardCap(address(ytTokenA), newHardCap);
vm.startPrank(user1);
2025-12-24 16:41:26 +08:00
usdc.approve(address(ytTokenA), 1000 ether);
2025-12-18 13:07:35 +08:00
// 尝试存入1000 ether会超过hardCap应该revert
vm.expectRevert(abi.encodeWithSignature("HardCapExceeded()"));
ytTokenA.depositYT(1000 ether);
vm.stopPrank();
// 恢复hardCap
factory.setHardCap(address(ytTokenA), 1000000 ether);
}
// ==================== 7. 权限测试 ====================
function test_19_OnlyFactoryCanUpdatePrices() public {
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("Forbidden()"));
2025-12-24 16:41:26 +08:00
ytTokenA.updatePrices(1e30);
2025-12-18 13:07:35 +08:00
vm.stopPrank();
}
function test_20_OnlyGovCanSetWhitelist() public {
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("Forbidden()"));
vault.setWhitelistedToken(address(0x123), 18, 1000, 1000000 ether, false);
vm.stopPrank();
}
// ==================== 8. 完整流程测试 ====================
function test_21_CompleteFlow() public {
console.log("=== Complete Flow Test ===");
// 步骤1: User1添加YT-A流动性
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
uint256 ytLP1 = router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
console.log("User1 added 1000 YT-A, received ytLP:", ytLP1);
assertEq(ytLP1, 997 ether);
vm.stopPrank();
// 步骤2: User1添加YT-B流动性
vm.startPrank(user1);
ytTokenB.approve(address(router), 1000 ether);
uint256 ytLP1b = router.addLiquidity(address(ytTokenB), 1000 ether, 0, 0);
console.log("User1 added 1000 YT-B, received ytLP:", ytLP1b);
assertEq(ytLP1b, 994.009 ether);
vm.stopPrank();
uint256 totalYtLP = ytlp.balanceOf(user1);
console.log("User1 total ytLP:", totalYtLP);
// 步骤3: User2执行Swap
vm.startPrank(user2);
ytTokenA.approve(address(router), 100 ether);
uint256 swapOut = router.swapYT(address(ytTokenA), address(ytTokenB), 100 ether, 0, user2);
console.log("User2 swapped 100 YT-A, received YT-B:", swapOut);
assertEq(swapOut, 99.7 ether);
vm.stopPrank();
// 步骤4: 等待冷却期后User1移除流动性
vm.warp(block.timestamp + 16 * 60);
vm.startPrank(user1);
uint256 removeAmount = totalYtLP / 2; // 移除一半
uint256 tokenOut = router.removeLiquidity(address(ytTokenA), removeAmount, 0, user1);
console.log("User1 removed half ytLP, received YT-A:", tokenOut);
vm.stopPrank();
}
// ==================== 9. 手续费测试 ====================
function test_22_SwapFeesAccumulation() public {
// 添加初始流动性
uint256 liquidityAmount = 2000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), liquidityAmount);
router.addLiquidity(address(ytTokenA), liquidityAmount, 0, 0);
ytTokenB.approve(address(router), liquidityAmount);
router.addLiquidity(address(ytTokenB), liquidityAmount, 0, 0);
uint256 ytLPBefore = ytlp.balanceOf(user1);
uint256 priceBefore = poolManager.getPrice(true);
vm.stopPrank();
// 执行swap累积手续费使用user2
uint256 swapAmount = 500 ether;
vm.startPrank(user2);
// Swap 1: YT-A → YT-B
ytTokenA.approve(address(router), swapAmount);
router.swapYT(address(ytTokenA), address(ytTokenB), swapAmount, 0, user2);
// Swap 2: YT-B → YT-A
ytTokenB.approve(address(router), swapAmount);
router.swapYT(address(ytTokenB), address(ytTokenA), swapAmount, 0, user2);
vm.stopPrank();
uint256 priceAfter = poolManager.getPrice(true);
// ytLP价格应该增长手续费留在池中
assertTrue(priceAfter > priceBefore, "ytLP price should increase");
// user1的ytLP数量不变
assertEq(ytlp.balanceOf(user1), ytLPBefore, "ytLP balance should not change");
}
2025-12-23 14:05:41 +08:00
function test_23_GetSwapFeeBasisPoints() public view {
2025-12-18 13:07:35 +08:00
uint256 usdyAmount = 1000 ether;
// YT代币之间互换
uint256 feeBps = vault.getSwapFeeBasisPoints(
address(ytTokenA),
address(ytTokenB),
usdyAmount
);
assertEq(feeBps, 30, "YT swap fee should be 30 bps");
// 赎回费率
uint256 redemptionFeeBps = vault.getRedemptionFeeBasisPoints(
address(ytTokenA),
usdyAmount
);
assertEq(redemptionFeeBps, 30, "redemption fee should be 30 bps");
}
// ==================== 10. 白名单管理测试 ====================
function test_24_AddWhitelistToken() public {
// 通过factory创建新的YTAssetVault
address ytTokenDAddr = factory.createVault(
"YT Token D",
"YT-D",
deployer,
1000000 ether,
2025-12-24 16:41:26 +08:00
address(usdc),
2025-12-18 13:07:35 +08:00
block.timestamp + 365 days,
PRICE_PRECISION,
2025-12-24 16:41:26 +08:00
address(usdcPriceFeed)
2025-12-18 13:07:35 +08:00
);
YTAssetVault ytTokenD = YTAssetVault(ytTokenDAddr);
// 铸造一些YT-D
2025-12-24 16:41:26 +08:00
usdc.mint(deployer, 1000 ether);
usdc.approve(address(ytTokenD), 1000 ether);
2025-12-18 13:07:35 +08:00
ytTokenD.depositYT(1000 ether);
// 添加到白名单
vault.setWhitelistedToken(address(ytTokenD), 18, 1000, 10000000 ether, false);
// 验证
assertTrue(vault.whitelistedTokens(address(ytTokenD)), "should be whitelisted");
assertEq(vault.tokenWeights(address(ytTokenD)), 1000, "weight incorrect");
assertEq(vault.totalTokenWeights(), 10000, "total weight incorrect");
// 初始化价格
priceFeed.forceUpdatePrice(address(ytTokenD), 1e30);
// 验证可以添加流动性
vm.startPrank(deployer);
ytTokenD.approve(address(router), 100 ether);
uint256 ytLPReceived = router.addLiquidity(address(ytTokenD), 100 ether, 0, 0);
vm.stopPrank();
assertEq(ytLPReceived, 99.7 ether, "first liquidity for new token incorrect");
}
function test_25_RemoveWhitelistToken() public {
// 确保池子是空的
assertEq(vault.poolAmounts(address(ytTokenC)), 0, "pool should be empty");
uint256 weightBefore = vault.totalTokenWeights();
// 移除白名单
vault.clearWhitelistedToken(address(ytTokenC));
// 验证
assertFalse(vault.whitelistedTokens(address(ytTokenC)), "should not be whitelisted");
assertEq(vault.tokenWeights(address(ytTokenC)), 0, "weight should be 0");
assertEq(vault.totalTokenWeights(), weightBefore - 2000, "total weight incorrect");
// 验证无法添加流动性
vm.startPrank(user1);
ytTokenC.approve(address(router), 100 ether);
vm.expectRevert(abi.encodeWithSignature("TokenNotWhitelisted()"));
router.addLiquidity(address(ytTokenC), 100 ether, 0, 0);
vm.stopPrank();
}
function test_26_UpdateTokenWeight() public {
uint256 oldWeight = vault.tokenWeights(address(ytTokenA));
assertEq(oldWeight, 4000);
// 更新权重
vault.setWhitelistedToken(address(ytTokenA), 18, 5000, 45000000 ether, false);
// 验证
assertEq(vault.tokenWeights(address(ytTokenA)), 5000, "updated weight incorrect");
assertEq(vault.totalTokenWeights(), 10000, "total weight after update incorrect");
}
// ==================== 11. 查询函数测试 ====================
function test_27_GetPoolValue() public {
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
ytTokenB.approve(address(router), 2000 ether);
router.addLiquidity(address(ytTokenB), 2000 ether, 0, 0);
vm.stopPrank();
// 获取池子总价值
uint256 poolValue = vault.getPoolValue(true);
// 池中有: 1000 YT-A + 2000 YT-B = $3000
uint256 expectedValue = 3000 ether;
assertEq(poolValue, expectedValue, "pool value incorrect");
}
function test_28_GetTargetUsdyAmount() public {
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
uint256 totalUsdy = usdy.totalSupply();
uint256 targetUsdy = vault.getTargetUsdyAmount(address(ytTokenA));
// YT-A权重 4000, 总权重 9000
uint256 expectedTarget = totalUsdy * 4000 / 9000;
assertEq(targetUsdy, expectedTarget, "target usdy amount incorrect");
}
function test_29_GetAccountValue() public {
uint256 amount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), amount);
router.addLiquidity(address(ytTokenA), amount, 0, 0);
vm.stopPrank();
uint256 accountValue = router.getAccountValue(user1);
// 账户价值应该接近1000 USDY
assertTrue(accountValue >= 995 ether && accountValue <= 1005 ether, "account value should be around 1000");
}
// ==================== 12. 动态手续费测试 ====================
2025-12-23 14:05:41 +08:00
function test_30_DynamicFeesDisabled() public view {
2025-12-18 13:07:35 +08:00
assertFalse(vault.hasDynamicFees());
uint256 feeBps = vault.getFeeBasisPoints(
address(ytTokenA),
1000 ether,
30,
50,
true
);
assertEq(feeBps, 30, "should return base fee when dynamic disabled");
}
function test_31_DynamicFeesEnabled() public {
vault.setDynamicFees(true);
vm.startPrank(user1);
// 大量添加YT-A
ytTokenA.approve(address(router), 3000 ether);
router.addLiquidity(address(ytTokenA), 3000 ether, 0, 0);
// 少量添加YT-B
ytTokenB.approve(address(router), 500 ether);
router.addLiquidity(address(ytTokenB), 500 ether, 0, 0);
vm.stopPrank();
uint256 usdyAmount = 100 ether;
// YT-A → YT-B (恶化平衡,费率更高)
uint256 feeHigher = vault.getSwapFeeBasisPoints(
address(ytTokenA),
address(ytTokenB),
usdyAmount
);
// YT-B → YT-A (改善平衡,费率更低)
uint256 feeLower = vault.getSwapFeeBasisPoints(
address(ytTokenB),
address(ytTokenA),
usdyAmount
);
assertTrue(feeHigher > 30, "fee should be higher when worsening balance");
assertTrue(feeLower < 30, "fee should be lower when improving balance");
vault.setDynamicFees(false);
}
// ==================== 13. 价格预言机测试 ====================
function test_32_SetSpreadBasisPoints() public {
uint256 spreadBps = 20;
priceFeed.setSpreadBasisPoints(address(ytTokenA), spreadBps);
assertEq(priceFeed.spreadBasisPoints(address(ytTokenA)), spreadBps);
}
function test_33_SpreadBasisPointsTooHigh() public {
uint256 tooHighSpread = 300; // 3% > 最大2%
vm.expectRevert(abi.encodeWithSignature("SpreadTooHigh()"));
priceFeed.setSpreadBasisPoints(address(ytTokenA), tooHighSpread);
}
function test_34_BatchSetSpread() public {
address[] memory tokens = new address[](3);
tokens[0] = address(ytTokenA);
tokens[1] = address(ytTokenB);
tokens[2] = address(ytTokenC);
uint256[] memory spreads = new uint256[](3);
spreads[0] = 10;
spreads[1] = 20;
spreads[2] = 30;
priceFeed.setSpreadBasisPointsForMultiple(tokens, spreads);
assertEq(priceFeed.spreadBasisPoints(address(ytTokenA)), 10);
assertEq(priceFeed.spreadBasisPoints(address(ytTokenB)), 20);
assertEq(priceFeed.spreadBasisPoints(address(ytTokenC)), 30);
// 清除
spreads[0] = 0;
spreads[1] = 0;
spreads[2] = 0;
priceFeed.setSpreadBasisPointsForMultiple(tokens, spreads);
}
function test_35_PriceProtectionMaxChange() public {
priceFeed.forceUpdatePrice(address(ytTokenA), 1e30);
uint256 tooHighPrice = 1.06e30; // +6%
priceFeed.forceUpdatePrice(address(ytTokenA), tooHighPrice);
assertEq(priceFeed.maxPriceChangeBps(), 500, "max change should be 5%");
}
// ==================== 14. AUM计算测试 ====================
function test_36_GetAumWithMaximise() public {
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
uint256 aumMax = poolManager.getAumInUsdy(true);
uint256 aumMin = poolManager.getAumInUsdy(false);
// 无价差时,两者应该相等
assertEq(aumMax, aumMin, "aum should be equal without spread");
assertEq(aumMax, 1000 ether, "aum should be $1000");
}
function test_37_GetAumWithSpread() public {
priceFeed.setSpreadBasisPoints(address(ytTokenA), 20); // 0.2%
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
uint256 aumMax = poolManager.getAumInUsdy(true);
uint256 aumMin = poolManager.getAumInUsdy(false);
assertEq(aumMax, 1002 ether, "aum max with spread incorrect");
assertEq(aumMin, 998 ether, "aum min with spread incorrect");
priceFeed.setSpreadBasisPoints(address(ytTokenA), 0);
}
// ==================== 15. 多用户场景测试 ====================
function test_38_MultipleUsersAddLiquidity() public {
// User1 添加
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
uint256 ytLP1 = router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
// User2 添加
vm.startPrank(user2);
ytTokenA.approve(address(router), 2000 ether);
uint256 ytLP2 = router.addLiquidity(address(ytTokenA), 2000 ether, 0, 0);
vm.stopPrank();
assertEq(ytLP1, 997 ether, "user1 ytLP incorrect");
assertEq(ytLP2, 1988.018 ether, "user2 ytLP incorrect");
// 份额比例
uint256 total = ytlp.totalSupply();
uint256 user1Share = ytLP1 * 10000 / total;
uint256 user2Share = ytLP2 * 10000 / total;
assertApproxEqAbs(user1Share, 3340, 1, "user1 share incorrect");
assertApproxEqAbs(user2Share, 6660, 1, "user2 share incorrect");
}
function test_39_RemoveLiquidityPartial() public {
uint256 addAmount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), addAmount);
router.addLiquidity(address(ytTokenA), addAmount, 0, 0);
uint256 ytLPBalance = ytlp.balanceOf(user1);
uint256 removeAmount = ytLPBalance / 2;
vm.warp(block.timestamp + 15 * 60 + 1);
uint256 amountOut = router.removeLiquidity(
address(ytTokenA),
removeAmount,
0,
user1
);
vm.stopPrank();
uint256 expectedOut = 498.5 ether;
assertEq(amountOut, expectedOut, "partial remove amount incorrect");
assertEq(ytlp.balanceOf(user1), removeAmount, "remaining ytLP incorrect");
}
// ==================== 16. 安全功能测试 ====================
function test_40_EmergencyMode() public {
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
vault.setEmergencyMode(true);
vm.startPrank(user2);
ytTokenA.approve(address(router), 100 ether);
vm.expectRevert(abi.encodeWithSignature("EmergencyMode()"));
router.addLiquidity(address(ytTokenA), 100 ether, 0, 0);
vm.expectRevert(abi.encodeWithSignature("EmergencyMode()"));
router.swapYT(address(ytTokenA), address(ytTokenB), 100 ether, 0, user2);
vm.stopPrank();
vault.setEmergencyMode(false);
}
function test_41_SwapDisabled() public {
vault.setSwapEnabled(false);
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
vm.expectRevert(abi.encodeWithSignature("SwapDisabled()"));
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
vault.setSwapEnabled(true);
}
function test_42_MaxSwapAmount() public {
vm.startPrank(user1);
ytTokenA.approve(address(router), 2000 ether);
router.addLiquidity(address(ytTokenA), 2000 ether, 0, 0);
ytTokenB.approve(address(router), 2000 ether);
router.addLiquidity(address(ytTokenB), 2000 ether, 0, 0);
vm.stopPrank();
vault.setMaxSwapAmount(address(ytTokenA), 50 ether);
vm.startPrank(user2);
ytTokenA.approve(address(router), 100 ether);
vm.expectRevert(abi.encodeWithSignature("AmountExceedsLimit()"));
router.swapYT(address(ytTokenA), address(ytTokenB), 100 ether, 0, user2);
vm.stopPrank();
vault.setMaxSwapAmount(address(ytTokenA), 0);
}
// ==================== 17. 边界条件测试 ====================
function test_43_AddZeroAmountReverts() public {
vm.startPrank(user1);
ytTokenA.approve(address(router), 0);
vm.expectRevert(abi.encodeWithSignature("InvalidAmount()"));
router.addLiquidity(address(ytTokenA), 0, 0, 0);
vm.stopPrank();
}
function test_44_RemoveZeroAmountReverts() public {
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("InvalidAmount()"));
router.removeLiquidity(address(ytTokenA), 0, 0, user1);
vm.stopPrank();
}
function test_45_SwapZeroAmountReverts() public {
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("InvalidAmount()"));
router.swapYT(address(ytTokenA), address(ytTokenB), 0, 0, user1);
vm.stopPrank();
}
function test_46_SwapUnwhitelistedTokenReverts() public {
// 通过factory创建新的YTAssetVault
address ytTokenDAddr = factory.createVault(
"YT Token D",
"YT-D",
deployer,
1000000 ether,
2025-12-24 16:41:26 +08:00
address(usdc),
2025-12-18 13:07:35 +08:00
block.timestamp + 365 days,
PRICE_PRECISION,
2025-12-24 16:41:26 +08:00
address(usdcPriceFeed)
2025-12-18 13:07:35 +08:00
);
YTAssetVault ytTokenD = YTAssetVault(ytTokenDAddr);
2025-12-24 16:41:26 +08:00
usdc.mint(user1, 500 ether);
2025-12-18 13:07:35 +08:00
vm.startPrank(user1);
2025-12-24 16:41:26 +08:00
usdc.approve(address(ytTokenD), 500 ether);
2025-12-18 13:07:35 +08:00
ytTokenD.depositYT(500 ether);
// 添加YT-A流动性
ytTokenA.approve(address(router), 1000 ether);
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
// 尝试swap未白名单的代币
ytTokenD.approve(address(router), 100 ether);
vm.expectRevert(abi.encodeWithSignature("TokenNotWhitelisted()"));
router.swapYT(address(ytTokenD), address(ytTokenA), 100 ether, 0, user1);
vm.stopPrank();
}
// ==================== 18. 费用精确计算测试 ====================
function test_47_ExactFeeCalculation() public {
uint256 amount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), amount);
uint256 poolAmountBefore = vault.poolAmounts(address(ytTokenA));
uint256 usdyBefore = vault.usdyAmounts(address(ytTokenA));
router.addLiquidity(address(ytTokenA), amount, 0, 0);
vm.stopPrank();
uint256 poolAmountAfter = vault.poolAmounts(address(ytTokenA));
uint256 usdyAfter = vault.usdyAmounts(address(ytTokenA));
// 池子应该收到全部代币
assertEq(poolAmountAfter - poolAmountBefore, amount, "pool should receive full amount");
// USDY债务只记录扣费后的
assertEq(usdyAfter - usdyBefore, 997 ether, "usdy debt incorrect");
}
function test_48_RedemptionFeeCalculation() public {
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
uint256 ytLPBalance = ytlp.balanceOf(user1);
vm.warp(block.timestamp + 16 * 60);
uint256 poolAmountBefore = vault.poolAmounts(address(ytTokenA));
router.removeLiquidity(address(ytTokenA), ytLPBalance, 0, user1);
vm.stopPrank();
uint256 poolAmountAfter = vault.poolAmounts(address(ytTokenA));
2025-12-24 16:41:26 +08:00
uint256 amountRemoved = poolAmountBefore - poolAmountAfter;
assertEq(amountRemoved, 997 ether, "amount removed from pool incorrect");
2025-12-18 13:07:35 +08:00
2025-12-24 16:41:26 +08:00
// 池子剩余:添加时的手续费 3 ether
2025-12-18 13:07:35 +08:00
assertEq(poolAmountAfter, 3 ether, "remaining pool incorrect");
}
// ==================== 19. ytLP价格增长测试 ====================
function test_49_YtLPPriceGrowthFromFees() public {
vm.startPrank(user1);
ytTokenA.approve(address(router), 2000 ether);
router.addLiquidity(address(ytTokenA), 2000 ether, 0, 0);
ytTokenB.approve(address(router), 2000 ether);
router.addLiquidity(address(ytTokenB), 2000 ether, 0, 0);
vm.stopPrank();
uint256 priceBefore = poolManager.getPrice(true);
uint256 supplyBefore = ytlp.totalSupply();
console.log("Price before swaps:", priceBefore);
console.log("Supply:", supplyBefore);
// 执行多次swap累积手续费
vm.startPrank(user1);
for (uint i = 0; i < 10; i++) {
ytTokenA.approve(address(router), 100 ether);
router.swapYT(address(ytTokenA), address(ytTokenB), 100 ether, 0, user2);
ytTokenB.approve(address(router), 100 ether);
router.swapYT(address(ytTokenB), address(ytTokenA), 100 ether, 0, user2);
}
vm.stopPrank();
uint256 priceAfter = poolManager.getPrice(true);
uint256 supplyAfter = ytlp.totalSupply();
console.log("Price after swaps:", priceAfter);
assertEq(supplyAfter, supplyBefore, "supply should not change");
assertTrue(priceAfter > priceBefore, "price should increase");
uint256 priceIncrease = (priceAfter - priceBefore) * 10000 / priceBefore;
console.log("Price increase (bps):", priceIncrease);
assertTrue(priceIncrease >= 10 && priceIncrease <= 30, "price increase should be 10-30 bps");
}
// ==================== 20. 价格查询测试 ====================
function test_50_GetPriceFromVault() public view {
uint256 price = vault.getPrice(address(ytTokenA), true);
assertEq(price, 1e30, "vault price incorrect");
uint256 maxPrice = vault.getMaxPrice(address(ytTokenA));
uint256 minPrice = vault.getMinPrice(address(ytTokenA));
assertEq(maxPrice, 1e30);
assertEq(minPrice, 1e30);
}
function test_51_GetPriceInfo() public view {
(
uint256 currentPrice,
2025-12-23 14:05:41 +08:00
,
2025-12-18 13:07:35 +08:00
uint256 maxPrice,
uint256 minPrice,
uint256 spread
) = priceFeed.getPriceInfo(address(ytTokenA));
assertEq(currentPrice, 1e30, "current price incorrect");
assertEq(maxPrice, 1e30, "max price incorrect");
assertEq(minPrice, 1e30, "min price incorrect");
assertEq(spread, 0, "spread should be 0");
}
function test_52_YtLPPriceCalculation() public {
uint256 amount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), amount);
router.addLiquidity(address(ytTokenA), amount, 0, 0);
vm.stopPrank();
uint256 ytLPPrice = poolManager.getPrice(true);
assertTrue(ytLPPrice > 1 ether, "ytLP price should be > $1");
assertTrue(ytLPPrice < 1.01 ether, "ytLP price should be < $1.01");
}
function test_53_AddLiquidityWithSpread() public {
priceFeed.setSpreadBasisPoints(address(ytTokenA), 20); // 0.2%
uint256 amount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), amount);
uint256 ytLPReceived = router.addLiquidity(address(ytTokenA), amount, 0, 0);
vm.stopPrank();
uint256 expectedYtLP = 995.006 ether;
assertEq(ytLPReceived, expectedYtLP, "ytLP with spread incorrect");
priceFeed.setSpreadBasisPoints(address(ytTokenA), 0);
}
function test_54_RemoveLiquiditySlippageProtection() public {
uint256 amount = 1000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), amount);
router.addLiquidity(address(ytTokenA), amount, 0, 0);
uint256 ytLPBalance = ytlp.balanceOf(user1);
vm.warp(block.timestamp + 15 * 60 + 1);
uint256 tooHighMinOut = 2000 ether;
vm.expectRevert(abi.encodeWithSignature("InsufficientOutput()"));
router.removeLiquidity(address(ytTokenA), ytLPBalance, tooHighMinOut, user1);
vm.stopPrank();
}
function test_55_SwapSlippageProtection() public {
uint256 liquidityAmount = 2000 ether;
vm.startPrank(user1);
ytTokenA.approve(address(router), liquidityAmount);
router.addLiquidity(address(ytTokenA), liquidityAmount, 0, 0);
ytTokenB.approve(address(router), liquidityAmount);
router.addLiquidity(address(ytTokenB), liquidityAmount, 0, 0);
vm.stopPrank();
uint256 swapAmount = 100 ether;
uint256 tooHighMinOut = 150 ether;
vm.startPrank(user2);
ytTokenA.approve(address(router), swapAmount);
vm.expectRevert(abi.encodeWithSignature("InsufficientOutput()"));
router.swapYT(address(ytTokenA), address(ytTokenB), swapAmount, tooHighMinOut, user2);
vm.stopPrank();
}
function test_56_OnlyHandlerCanAddLiquidity() public {
vm.startPrank(user1);
ytTokenA.approve(address(poolManager), 1000 ether);
vm.expectRevert(abi.encodeWithSignature("Forbidden()"));
poolManager.addLiquidityForAccount(
user1,
user1,
address(ytTokenA),
1000 ether,
0,
0
);
vm.stopPrank();
}
function test_57_OnlyPoolManagerCanBuyUSDY() public {
vm.startPrank(user1);
ytTokenA.approve(address(vault), 1000 ether);
vm.expectRevert(abi.encodeWithSignature("OnlyPoolManager()"));
vault.buyUSDY(address(ytTokenA), user1);
vm.stopPrank();
}
function test_58_OnlyGovCanSetFees() public {
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("Forbidden()"));
vault.setSwapFees(40, 5, 60, 25);
vm.stopPrank();
}
function test_59_OnlyKeeperCanUpdatePrice() public {
// 非keeper和非gov不能调用updatePrice
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("Forbidden()"));
priceFeed.updatePrice(address(ytTokenA));
vm.stopPrank();
}
function test_60_SetKeeperPermission() public {
// 设置user1为keeper
priceFeed.setKeeper(user1, true);
assertTrue(priceFeed.isKeeper(user1), "user1 should be keeper");
// keeper可以调用updatePrice
vm.startPrank(user1);
uint256 price = priceFeed.updatePrice(address(ytTokenA));
vm.stopPrank();
assertEq(price, PRICE_PRECISION, "price should be updated");
// 移除keeper权限
priceFeed.setKeeper(user1, false);
assertFalse(priceFeed.isKeeper(user1), "user1 should not be keeper");
// 移除后不能调用
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("Forbidden()"));
priceFeed.updatePrice(address(ytTokenA));
vm.stopPrank();
}
function test_61_GovCanAlwaysUpdatePrice() public {
// gov可以直接调用updatePrice
uint256 price = priceFeed.updatePrice(address(ytTokenA));
assertEq(price, PRICE_PRECISION, "gov can update price");
}
// ==================== 21. YTRewardRouter 暂停功能测试 ====================
function test_62_RouterPauseByGov() public {
// Gov可以暂停
router.pause();
assertTrue(router.paused(), "router should be paused");
// Gov可以恢复
router.unpause();
assertFalse(router.paused(), "router should be unpaused");
}
function test_63_OnlyGovCanPauseRouter() public {
// User不能暂停
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("Forbidden()"));
router.pause();
vm.stopPrank();
// User不能恢复
router.pause(); // 由deployer暂停
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("Forbidden()"));
router.unpause();
vm.stopPrank();
router.unpause(); // 恢复
}
function test_64_CannotAddLiquidityWhenRouterPaused() public {
// 暂停router
router.pause();
// 尝试添加流动性应该失败
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
vm.expectRevert(abi.encodeWithSignature("EnforcedPause()"));
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
// 恢复后应该可以添加
router.unpause();
vm.startPrank(user1);
uint256 ytLPReceived = router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
assertEq(ytLPReceived, 997 ether, "add liquidity should work after unpause");
}
function test_65_CannotRemoveLiquidityWhenRouterPaused() public {
// 先添加流动性
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
uint256 ytLPBalance = ytlp.balanceOf(user1);
// 等待冷却期
vm.warp(block.timestamp + 15 * 60 + 1);
vm.stopPrank();
// 暂停router
router.pause();
// 尝试移除流动性应该失败
vm.startPrank(user1);
vm.expectRevert(abi.encodeWithSignature("EnforcedPause()"));
router.removeLiquidity(address(ytTokenA), ytLPBalance, 0, user1);
vm.stopPrank();
// 恢复后应该可以移除
router.unpause();
vm.startPrank(user1);
uint256 amountOut = router.removeLiquidity(address(ytTokenA), ytLPBalance, 0, user1);
vm.stopPrank();
assertEq(amountOut, 997 ether, "remove liquidity should work after unpause");
}
function test_66_CannotSwapWhenRouterPaused() public {
// 先添加流动性
vm.startPrank(user1);
ytTokenA.approve(address(router), 2000 ether);
router.addLiquidity(address(ytTokenA), 2000 ether, 0, 0);
ytTokenB.approve(address(router), 2000 ether);
router.addLiquidity(address(ytTokenB), 2000 ether, 0, 0);
vm.stopPrank();
// 暂停router
router.pause();
// 尝试swap应该失败
vm.startPrank(user2);
ytTokenA.approve(address(router), 100 ether);
vm.expectRevert(abi.encodeWithSignature("EnforcedPause()"));
router.swapYT(address(ytTokenA), address(ytTokenB), 100 ether, 0, user2);
vm.stopPrank();
// 恢复后应该可以swap
router.unpause();
vm.startPrank(user2);
uint256 amountOut = router.swapYT(address(ytTokenA), address(ytTokenB), 100 ether, 0, user2);
vm.stopPrank();
assertEq(amountOut, 99.7 ether, "swap should work after unpause");
}
function test_67_QueryFunctionsWorkWhenRouterPaused() public {
// 先添加流动性
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
// 暂停router
router.pause();
// 查询函数应该仍然可用
uint256 ytLPPrice = router.getYtLPPrice();
assertTrue(ytLPPrice > 0, "getYtLPPrice should work when paused");
uint256 accountValue = router.getAccountValue(user1);
assertTrue(accountValue > 0, "getAccountValue should work when paused");
// 验证返回的值是合理的
assertTrue(ytLPPrice > 1 ether, "ytLP price should be > $1");
assertTrue(accountValue >= 995 ether && accountValue <= 1005 ether, "account value should be around 1000");
}
function test_68_PauseRouterDoesNotAffectVaultDirectly() public {
// 暂停router不影响直接通过vault操作
router.pause();
vm.startPrank(user1);
ytTokenA.approve(address(vault), 1000 ether);
// 直接通过poolManager添加流动性仍然失败因为user1不是handler
vm.expectRevert(abi.encodeWithSignature("Forbidden()"));
poolManager.addLiquidityForAccount(user1, user1, address(ytTokenA), 1000 ether, 0, 0);
vm.stopPrank();
router.unpause();
}
function test_69_CompleteFlowWithPauseResume() public {
console.log("=== Complete Flow With Pause/Resume Test ===");
// 步骤1: 添加流动性
vm.startPrank(user1);
ytTokenA.approve(address(router), 1000 ether);
uint256 ytLP1 = router.addLiquidity(address(ytTokenA), 1000 ether, 0, 0);
console.log("Added liquidity, received ytLP:", ytLP1);
vm.stopPrank();
// 步骤2: 暂停router
router.pause();
console.log("Router paused");
// 步骤3: 验证所有操作都被阻止
vm.startPrank(user1);
ytTokenB.approve(address(router), 1000 ether);
vm.expectRevert(abi.encodeWithSignature("EnforcedPause()"));
router.addLiquidity(address(ytTokenB), 1000 ether, 0, 0);
console.log("Add liquidity blocked during pause");
vm.stopPrank();
// 步骤4: 恢复router
router.unpause();
console.log("Router unpaused");
// 步骤5: 继续正常操作
vm.startPrank(user1);
uint256 ytLP2 = router.addLiquidity(address(ytTokenB), 1000 ether, 0, 0);
console.log("Added liquidity after unpause, received ytLP:", ytLP2);
vm.stopPrank();
// 验证总余额
uint256 totalYtLP = ytlp.balanceOf(user1);
console.log("Total ytLP:", totalYtLP);
assertEq(totalYtLP, ytLP1 + ytLP2, "total ytLP should be sum of both additions");
}
function test_70_EmergencyScenarioPauseEverything() public {
console.log("=== Emergency Scenario: Pause Everything ===");
// 先建立一些状态
vm.startPrank(user1);
ytTokenA.approve(address(router), 2000 ether);
router.addLiquidity(address(ytTokenA), 2000 ether, 0, 0);
ytTokenB.approve(address(router), 2000 ether);
router.addLiquidity(address(ytTokenB), 2000 ether, 0, 0);
vm.stopPrank();
console.log("Initial liquidity added");
// 模拟紧急情况暂停router
router.pause();
console.log("Router paused for emergency");
// 同时暂停vault (通过设置紧急模式)
vault.setEmergencyMode(true);
console.log("Vault emergency mode activated");
// 验证所有操作都被阻止
vm.startPrank(user2);
ytTokenA.approve(address(router), 100 ether);
// Router暂停阻止操作
vm.expectRevert(abi.encodeWithSignature("EnforcedPause()"));
router.addLiquidity(address(ytTokenA), 100 ether, 0, 0);
vm.expectRevert(abi.encodeWithSignature("EnforcedPause()"));
router.swapYT(address(ytTokenA), address(ytTokenB), 100 ether, 0, user2);
vm.stopPrank();
console.log("All operations blocked during emergency");
// 恢复系统
router.unpause();
vault.setEmergencyMode(false);
console.log("System recovered from emergency");
// 验证系统恢复正常
vm.startPrank(user2);
uint256 swapOut = router.swapYT(address(ytTokenA), address(ytTokenB), 100 ether, 0, user2);
vm.stopPrank();
assertEq(swapOut, 99.7 ether, "swap should work after recovery");
console.log("System operational after recovery");
}
receive() external payable {}
2025-12-24 16:41:26 +08:00
}