// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/CouponBatch.sol"; import "../src/interfaces/ICouponBatch.sol"; contract CouponBatchTest is Test { CouponBatch batch; address admin = address(1); address factory = address(2); address burner = address(3); address issuer = address(4); address user1 = address(5); address user2 = address(6); bytes32 storeA = keccak256("STORE_A"); bytes32 storeB = keccak256("STORE_B"); function setUp() public { vm.startPrank(admin); batch = new CouponBatch(); batch.initialize("https://genex.io/api/coupon-batch/{id}.json", admin); batch.grantRole(keccak256("FACTORY_ROLE"), factory); batch.grantRole(keccak256("BURNER_ROLE"), burner); vm.stopPrank(); } // ========================================== // 铸造测试 // ========================================== function test_MintCreatesNewType() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); uint256 typeId = batch.mint(issuer, 100e6, 1000, config); assertEq(typeId, 1); assertEq(batch.balanceOf(issuer, typeId), 1000); assertEq(batch.totalSupply(typeId), 1000); assertEq(batch.getMintedQuantity(typeId), 1000); } function test_MintMultipleTypes() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.startPrank(factory); uint256 typeId1 = batch.mint(issuer, 100e6, 500, config); uint256 typeId2 = batch.mint(issuer, 50e6, 2000, config); vm.stopPrank(); assertEq(typeId1, 1); assertEq(typeId2, 2); assertEq(batch.balanceOf(issuer, typeId1), 500); assertEq(batch.balanceOf(issuer, typeId2), 2000); } function test_MintStoresConfig() public { bytes32[] memory stores = new bytes32[](2); stores[0] = storeA; stores[1] = storeB; ICouponBatch.BatchCouponConfig memory config = ICouponBatch.BatchCouponConfig({ transferable: false, maxPrice: 50e6, expiryDate: block.timestamp + 90 days, minPurchase: 10, stackable: true, allowedStores: stores, issuer: issuer, faceValue: 50e6 }); vm.prank(factory); uint256 typeId = batch.mint(issuer, 50e6, 100, config); ICouponBatch.BatchCouponConfig memory stored = batch.getConfig(typeId); assertEq(stored.transferable, false); assertEq(stored.maxPrice, 50e6); assertEq(stored.minPurchase, 10); assertEq(stored.stackable, true); assertEq(stored.allowedStores.length, 2); assertEq(stored.issuer, issuer); assertEq(stored.faceValue, 50e6); } function test_MintZeroAddressReverts() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); vm.expectRevert("CouponBatch: zero address"); batch.mint(address(0), 100e6, 1000, config); } function test_MintZeroFaceValueReverts() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); vm.expectRevert("CouponBatch: zero face value"); batch.mint(issuer, 0, 1000, config); } function test_MintZeroQuantityReverts() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); vm.expectRevert("CouponBatch: zero quantity"); batch.mint(issuer, 100e6, 0, config); } function test_MintOnlyFactoryRole() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(user1); vm.expectRevert(); batch.mint(issuer, 100e6, 1000, config); } function test_GetFaceValue() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); uint256 typeId = batch.mint(issuer, 250e6, 100, config); assertEq(batch.getFaceValue(typeId), 250e6); } function test_GetConfigNonexistentReverts() public { vm.expectRevert("CouponBatch: nonexistent type"); batch.getConfig(999); } // ========================================== // 销毁测试 // ========================================== function test_BurnByOwner() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); uint256 typeId = batch.mint(issuer, 100e6, 1000, config); vm.prank(issuer); batch.burn(issuer, typeId, 300); assertEq(batch.balanceOf(issuer, typeId), 700); assertEq(batch.totalSupply(typeId), 700); } function test_BurnByBurnerRole() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); uint256 typeId = batch.mint(issuer, 100e6, 1000, config); vm.prank(burner); batch.burn(issuer, typeId, 500); assertEq(batch.balanceOf(issuer, typeId), 500); } function test_BurnByApproved() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); uint256 typeId = batch.mint(issuer, 100e6, 1000, config); vm.prank(issuer); batch.setApprovalForAll(user1, true); vm.prank(user1); batch.burn(issuer, typeId, 200); assertEq(batch.balanceOf(issuer, typeId), 800); } function test_BurnUnauthorizedReverts() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); uint256 typeId = batch.mint(issuer, 100e6, 1000, config); vm.prank(user1); vm.expectRevert("CouponBatch: not authorized to burn"); batch.burn(issuer, typeId, 100); } // ========================================== // 转让限制测试 // ========================================== function test_TransferableTokenCanTransfer() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); config.transferable = true; vm.prank(factory); uint256 typeId = batch.mint(issuer, 100e6, 100, config); vm.prank(issuer); batch.safeTransferFrom(issuer, user1, typeId, 50, ""); assertEq(batch.balanceOf(issuer, typeId), 50); assertEq(batch.balanceOf(user1, typeId), 50); } function test_NonTransferableTokenReverts() public { ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); config.transferable = false; vm.prank(factory); uint256 typeId = batch.mint(issuer, 100e6, 100, config); vm.prank(issuer); vm.expectRevert("CouponBatch: non-transferable"); batch.safeTransferFrom(issuer, user1, typeId, 10, ""); } // ========================================== // Fuzz 测试 // ========================================== function testFuzz_MintQuantity(uint256 quantity) public { quantity = bound(quantity, 1, 100_000); ICouponBatch.BatchCouponConfig memory config = _defaultConfig(); vm.prank(factory); uint256 typeId = batch.mint(issuer, 100e6, quantity, config); assertEq(batch.balanceOf(issuer, typeId), quantity); assertEq(batch.totalSupply(typeId), quantity); assertEq(batch.getMintedQuantity(typeId), quantity); } // ========================================== // Helpers // ========================================== function _defaultConfig() 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 }); } }