// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/CouponFactory.sol"; import "../src/Coupon.sol"; import "../src/Compliance.sol"; import "../src/interfaces/ICoupon.sol"; import "../src/CouponBatch.sol"; import "../src/interfaces/ICouponBatch.sol"; contract CouponFactoryTest is Test { CouponFactory factory; Coupon coupon; Compliance compliance; CouponBatch couponBatch; address admin = address(1); address issuer = address(2); address minter = address(3); function setUp() public { vm.startPrank(admin); // 部署 Compliance compliance = new Compliance(); compliance.initialize(admin); // 部署 Coupon coupon = new Coupon(); coupon.initialize("Genex Coupon", "GXC", admin); // 部署 CouponFactory factory = new CouponFactory(); factory.initialize(address(coupon), address(compliance), admin); // 部署 CouponBatch couponBatch = new CouponBatch(); couponBatch.initialize("https://genex.io/api/coupon-batch/{id}.json", admin); // 授权 coupon.grantRole(keccak256("FACTORY_ROLE"), address(factory)); factory.grantRole(keccak256("MINTER_ROLE"), minter); couponBatch.grantRole(keccak256("FACTORY_ROLE"), address(factory)); // 关联 CouponBatch factory.setCouponBatchContract(address(couponBatch)); // 设置发行方 KYC compliance.setKycLevel(issuer, 3); vm.stopPrank(); } function test_MintBatchUtility() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 5, config); assertEq(tokenIds.length, 5); for (uint256 i = 0; i < 5; i++) { assertEq(coupon.ownerOf(tokenIds[i]), issuer); assertEq(coupon.getFaceValue(tokenIds[i]), 100e6); } } function test_MintBatchSecurity() public { vm.prank(minter); ICoupon.CouponConfig memory config = _securityConfig(); uint256[] memory tokenIds = factory.mintBatch(issuer, 1000e6, 3, config); assertEq(tokenIds.length, 3); } function test_UtilityMaxPriceExceedsFaceValue() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); config.maxPrice = 200e6; // 超过面值 100e6 vm.expectRevert("Utility: maxPrice <= faceValue"); factory.mintBatch(issuer, 100e6, 1, config); } function test_UtilityMaxExpiry12Months() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); config.expiryDate = block.timestamp + 400 days; vm.expectRevert("Utility: max 12 months"); factory.mintBatch(issuer, 100e6, 1, config); } function test_SecurityRequiresExpiry() public { vm.prank(minter); ICoupon.CouponConfig memory config = _securityConfig(); config.expiryDate = 0; vm.expectRevert("Security: expiry required"); factory.mintBatch(issuer, 1000e6, 1, config); } function test_SecurityRequiresKycL3() public { address lowKycIssuer = address(10); vm.prank(admin); compliance.setKycLevel(lowKycIssuer, 1); vm.prank(minter); ICoupon.CouponConfig memory config = _securityConfig(); vm.expectRevert("Compliance: insufficient KYC level"); factory.mintBatch(lowKycIssuer, 1000e6, 1, config); } function test_OnlyMinterCanMint() public { vm.prank(issuer); ICoupon.CouponConfig memory config = _utilityConfig(); vm.expectRevert(); factory.mintBatch(issuer, 100e6, 1, config); } function test_ZeroQuantityReverts() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); vm.expectRevert("CouponFactory: invalid quantity"); factory.mintBatch(issuer, 100e6, 0, config); } function test_ExcessiveQuantityReverts() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); vm.expectRevert("CouponFactory: invalid quantity"); factory.mintBatch(issuer, 100e6, 10001, config); } function test_BatchInfoRecorded() public { vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); factory.mintBatch(issuer, 100e6, 3, config); CouponFactory.BatchInfo memory batch = factory.getBatchInfo(0); assertEq(batch.issuer, issuer); assertEq(batch.faceValue, 100e6); assertEq(batch.quantity, 3); assertEq(batch.tokenIds.length, 3); } function testFuzz_MintBatchQuantity(uint256 quantity) public { quantity = bound(quantity, 1, 100); vm.prank(minter); ICoupon.CouponConfig memory config = _utilityConfig(); uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, quantity, config); assertEq(tokenIds.length, quantity); } // --- Helpers --- function _utilityConfig() internal view returns (ICoupon.CouponConfig memory) { bytes32[] memory stores = new bytes32[](0); return ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Utility, transferable: true, maxResaleCount: 3, maxPrice: 100e6, expiryDate: block.timestamp + 180 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: issuer, faceValue: 100e6 }); } function _securityConfig() internal view returns (ICoupon.CouponConfig memory) { bytes32[] memory stores = new bytes32[](0); return ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Security, transferable: true, maxResaleCount: 10, maxPrice: 0, expiryDate: block.timestamp + 365 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: issuer, faceValue: 1000e6 }); } function _batchConfig() internal view returns (ICouponBatch.BatchCouponConfig memory) { bytes32[] memory stores = new bytes32[](0); return ICouponBatch.BatchCouponConfig({ transferable: true, maxPrice: 100e6, expiryDate: block.timestamp + 180 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: issuer, faceValue: 100e6 }); } // ========================================== // ERC-1155 (mintBatch1155) 测试 // ========================================== function test_MintBatch1155Success() public { vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); uint256 typeId = factory.mintBatch1155(issuer, 100e6, 10000, config); assertEq(typeId, 1); assertEq(couponBatch.balanceOf(issuer, typeId), 10000); } function test_MintBatch1155LargeQuantity() public { vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); uint256 typeId = factory.mintBatch1155(issuer, 100e6, 100000, config); assertEq(couponBatch.balanceOf(issuer, typeId), 100000); } function test_MintBatch1155RecordsBatchInfo() public { vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); factory.mintBatch1155(issuer, 100e6, 5000, config); CouponFactory.BatchInfo memory info = factory.getBatchInfo(0); assertEq(info.issuer, issuer); assertEq(info.faceValue, 100e6); assertEq(info.quantity, 5000); assertEq(info.tokenIds.length, 0); // ERC-1155 uses typeId, not tokenIds } function test_MintBatch1155ZeroIssuerReverts() public { vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); vm.expectRevert("CouponFactory: zero address issuer"); factory.mintBatch1155(address(0), 100e6, 1000, config); } function test_MintBatch1155ZeroFaceValueReverts() public { vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); vm.expectRevert("CouponFactory: zero face value"); factory.mintBatch1155(issuer, 0, 1000, config); } function test_MintBatch1155ExcessiveQuantityReverts() public { vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); vm.expectRevert("CouponFactory: invalid quantity"); factory.mintBatch1155(issuer, 100e6, 100001, config); } function test_MintBatch1155MaxPriceExceedsFaceValueReverts() public { vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); config.maxPrice = 200e6; // 超过面值 vm.expectRevert("Utility: maxPrice <= faceValue"); factory.mintBatch1155(issuer, 100e6, 1000, config); } function test_MintBatch1155ExpiryTooFarReverts() public { vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); config.expiryDate = block.timestamp + 400 days; vm.expectRevert("Utility: max 12 months"); factory.mintBatch1155(issuer, 100e6, 1000, config); } function test_MintBatch1155ExpiredReverts() public { vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); config.expiryDate = block.timestamp - 1; vm.expectRevert("Utility: expiry must be future"); factory.mintBatch1155(issuer, 100e6, 1000, config); } function test_MintBatch1155BatchContractNotSetReverts() public { // 部署新工厂,不设置 CouponBatch vm.startPrank(admin); CouponFactory factory2 = new CouponFactory(); factory2.initialize(address(coupon), address(compliance), admin); factory2.grantRole(keccak256("MINTER_ROLE"), minter); vm.stopPrank(); vm.prank(minter); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); vm.expectRevert("CouponFactory: batch contract not set"); factory2.mintBatch1155(issuer, 100e6, 1000, config); } function test_MintBatch1155OnlyMinterRole() public { vm.prank(issuer); ICouponBatch.BatchCouponConfig memory config = _batchConfig(); vm.expectRevert(); factory.mintBatch1155(issuer, 100e6, 1000, config); } }