// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/CouponBackedSecurity.sol"; import "../src/Coupon.sol"; import "../src/Compliance.sol"; import "../src/interfaces/ICoupon.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockUSDC2 is ERC20 { constructor() ERC20("USD Coin", "USDC") {} function decimals() public pure override returns (uint8) { return 6; } function mint(address to, uint256 amount) external { _mint(to, amount); } } contract CouponBackedSecurityTest is Test { CouponBackedSecurity cbs; Coupon coupon; Compliance compliance; MockUSDC2 usdc; address admin = address(1); address cbsIssuer = address(2); address investor1 = address(3); address investor2 = address(4); address settler = address(5); address ratingAgency = address(6); function setUp() public { vm.startPrank(admin); usdc = new MockUSDC2(); compliance = new Compliance(); compliance.initialize(admin); coupon = new Coupon(); coupon.initialize("Genex Coupon", "GXC", admin); coupon.grantRole(keccak256("FACTORY_ROLE"), admin); cbs = new CouponBackedSecurity(); cbs.initialize(address(coupon), address(compliance), address(usdc), admin); cbs.grantRole(keccak256("ISSUER_ROLE"), cbsIssuer); cbs.grantRole(keccak256("SETTLER_ROLE"), settler); cbs.grantRole(keccak256("RATING_AGENCY_ROLE"), ratingAgency); // KYC compliance.setKycLevel(cbsIssuer, 3); compliance.setKycLevel(investor1, 2); compliance.setKycLevel(investor2, 2); // Fund usdc.mint(investor1, 1_000_000e6); usdc.mint(investor2, 1_000_000e6); usdc.mint(settler, 1_000_000e6); vm.stopPrank(); // Approvals vm.prank(investor1); usdc.approve(address(cbs), type(uint256).max); vm.prank(investor2); usdc.approve(address(cbs), type(uint256).max); vm.prank(settler); usdc.approve(address(cbs), type(uint256).max); vm.prank(cbsIssuer); coupon.setApprovalForAll(address(cbs), true); } function test_CreatePool() public { uint256[] memory tokenIds = _mintSecurityCoupons(3, 1000e6); vm.prank(cbsIssuer); uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days); ( uint256 totalFaceValue, uint256 totalShares, uint256 soldShares, , , address issuer, bool active ) = cbs.getPool(poolId); assertEq(totalFaceValue, 3000e6); assertEq(totalShares, 100); assertEq(soldShares, 0); assertEq(issuer, cbsIssuer); assertTrue(active); } function test_PurchaseShares() public { uint256[] memory tokenIds = _mintSecurityCoupons(2, 500e6); vm.prank(cbsIssuer); uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days); vm.prank(investor1); cbs.purchaseShares(poolId, 30); assertEq(cbs.shares(poolId, investor1), 30); (, , uint256 soldShares, , , , ) = cbs.getPool(poolId); assertEq(soldShares, 30); } function test_PurchaseSharesInsufficientReverts() public { uint256[] memory tokenIds = _mintSecurityCoupons(1, 100e6); vm.prank(cbsIssuer); uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); vm.prank(investor1); vm.expectRevert("CBS: insufficient shares"); cbs.purchaseShares(poolId, 11); } function test_PurchaseSharesRequiresKycL2() public { uint256[] memory tokenIds = _mintSecurityCoupons(1, 100e6); vm.prank(cbsIssuer); uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); address lowKyc = address(99); vm.prank(admin); compliance.setKycLevel(lowKyc, 1); vm.prank(lowKyc); vm.expectRevert("Compliance: insufficient KYC level"); cbs.purchaseShares(poolId, 1); } function test_OnlySecurityCouponsAllowed() public { // Mint utility coupon bytes32[] memory stores = new bytes32[](0); ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Utility, transferable: true, maxResaleCount: 3, maxPrice: 100e6, expiryDate: block.timestamp + 180 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: cbsIssuer, faceValue: 100e6 }); vm.prank(admin); uint256 tokenId = coupon.mint(cbsIssuer, 100e6, config); uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = tokenId; vm.prank(cbsIssuer); vm.expectRevert("CBS: only Security coupons"); cbs.createPool(tokenIds, 10, block.timestamp + 365 days); } function test_SetCreditRating() public { uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6); vm.prank(cbsIssuer); uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); vm.prank(ratingAgency); cbs.setCreditRating(poolId, "AAA"); (, , , , string memory rating, , ) = cbs.getPool(poolId); assertEq(rating, "AAA"); } function test_DepositAndClaimYield() public { uint256[] memory tokenIds = _mintSecurityCoupons(2, 500e6); vm.prank(cbsIssuer); uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days); // 两个投资者各买 50 份 vm.prank(investor1); cbs.purchaseShares(poolId, 50); vm.prank(investor2); cbs.purchaseShares(poolId, 50); // Settler 存入收益 1000 USDC vm.prank(settler); cbs.depositYield(poolId, 1000e6); // 各自领取 500 USDC vm.prank(investor1); cbs.claimYield(poolId); assertEq(usdc.balanceOf(investor1), 1_000_000e6 - 500e6 + 500e6); // 购买花了500e6, 领回500e6 vm.prank(investor2); cbs.claimYield(poolId); } function test_ClaimYieldNothingToClaimReverts() public { uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6); vm.prank(cbsIssuer); uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); vm.prank(investor1); cbs.purchaseShares(poolId, 5); // 没有收益存入 vm.prank(investor1); vm.expectRevert("CBS: nothing to claim"); cbs.claimYield(poolId); } function test_DeactivatePool() public { uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6); vm.prank(cbsIssuer); uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); vm.prank(admin); cbs.deactivatePool(poolId); (, , , , , , bool active) = cbs.getPool(poolId); assertFalse(active); } function test_GetClaimableYield() public { uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6); vm.prank(cbsIssuer); uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days); vm.prank(investor1); cbs.purchaseShares(poolId, 25); vm.prank(settler); cbs.depositYield(poolId, 400e6); uint256 claimable = cbs.getClaimableYield(poolId, investor1); assertEq(claimable, 100e6); // 25% of 400 } // --- Helpers --- function _mintSecurityCoupons(uint256 count, uint256 faceValue) internal returns (uint256[] memory) { uint256[] memory tokenIds = new uint256[](count); bytes32[] memory stores = new bytes32[](0); ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Security, transferable: true, maxResaleCount: 10, maxPrice: 0, expiryDate: block.timestamp + 730 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: cbsIssuer, faceValue: faceValue }); for (uint256 i = 0; i < count; i++) { vm.prank(admin); tokenIds[i] = coupon.mint(cbsIssuer, faceValue, config); } return tokenIds; } }