// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/Redemption1155.sol"; import "../src/CouponBatch.sol"; import "../src/Compliance.sol"; import "../src/interfaces/ICouponBatch.sol"; contract Redemption1155Test is Test { Redemption1155 redemption; CouponBatch batch; Compliance compliance; address admin = address(1); address factory = address(2); address consumer = address(4); address issuer = address(5); bytes32 storeA = keccak256("STORE_A"); bytes32 storeB = keccak256("STORE_B"); function setUp() public { vm.startPrank(admin); // 部署 Compliance compliance = new Compliance(); compliance.initialize(admin); // 部署 CouponBatch batch = new CouponBatch(); batch.initialize("https://genex.io/api/coupon-batch/{id}.json", admin); // 部署 Redemption1155 redemption = new Redemption1155(); redemption.initialize(address(batch), address(compliance), admin); // 角色授权 batch.grantRole(keccak256("FACTORY_ROLE"), factory); batch.grantRole(keccak256("BURNER_ROLE"), address(redemption)); compliance.setKycLevel(consumer, 1); vm.stopPrank(); } function test_RedeemSuccess() public { uint256 typeId = _mintBatch(consumer, 100e6, 10, false); vm.prank(consumer); redemption.redeem(typeId, 3, storeA); // 余额减少 assertEq(batch.balanceOf(consumer, typeId), 7); // 记录已创建 Redemption1155.RedemptionRecord memory record = redemption.getRedemption(0); assertEq(record.typeId, typeId); assertEq(record.amount, 3); assertEq(record.consumer, consumer); assertEq(record.issuer, issuer); assertEq(record.storeId, storeA); } function test_RedeemAllUnits() public { uint256 typeId = _mintBatch(consumer, 50e6, 5, false); vm.prank(consumer); redemption.redeem(typeId, 5, storeA); assertEq(batch.balanceOf(consumer, typeId), 0); assertEq(redemption.totalRedemptions(), 1); } function test_RedeemMultipleTimes() public { uint256 typeId = _mintBatch(consumer, 100e6, 100, false); vm.startPrank(consumer); redemption.redeem(typeId, 30, storeA); redemption.redeem(typeId, 50, storeA); vm.stopPrank(); assertEq(batch.balanceOf(consumer, typeId), 20); assertEq(redemption.totalRedemptions(), 2); Redemption1155.RedemptionRecord memory r0 = redemption.getRedemption(0); assertEq(r0.amount, 30); Redemption1155.RedemptionRecord memory r1 = redemption.getRedemption(1); assertEq(r1.amount, 50); } function test_RedeemZeroAmountReverts() public { uint256 typeId = _mintBatch(consumer, 100e6, 10, false); vm.prank(consumer); vm.expectRevert("Redemption1155: zero amount"); redemption.redeem(typeId, 0, storeA); } function test_RedeemInsufficientBalanceReverts() public { uint256 typeId = _mintBatch(consumer, 100e6, 5, false); vm.prank(consumer); vm.expectRevert("Redemption1155: insufficient balance"); redemption.redeem(typeId, 10, storeA); } function test_RedeemBlacklistedReverts() public { uint256 typeId = _mintBatch(consumer, 100e6, 10, false); vm.prank(admin); compliance.addToBlacklist(consumer, "OFAC"); vm.prank(consumer); vm.expectRevert("Redemption1155: blacklisted"); redemption.redeem(typeId, 1, storeA); } function test_RedeemExpiredReverts() public { uint256 typeId = _mintBatch(consumer, 100e6, 10, false); // 快进到过期 vm.warp(block.timestamp + 200 days); vm.prank(consumer); vm.expectRevert("Redemption1155: coupon expired"); redemption.redeem(typeId, 1, storeA); } function test_RedeemStoreRestricted() public { uint256 typeId = _mintBatch(consumer, 100e6, 10, true); // 不在允许门店列表中 bytes32 storeC = keccak256("STORE_C"); vm.prank(consumer); vm.expectRevert("Redemption1155: store not allowed"); redemption.redeem(typeId, 1, storeC); // 使用允许的门店 vm.prank(consumer); redemption.redeem(typeId, 1, storeA); assertEq(batch.balanceOf(consumer, typeId), 9); } function test_RedeemNoBalanceReverts() public { uint256 typeId = _mintBatch(issuer, 100e6, 10, false); // consumer 没有该 typeId 的余额 vm.prank(consumer); vm.expectRevert("Redemption1155: insufficient balance"); redemption.redeem(typeId, 1, storeA); } // ========================================== // Helpers // ========================================== function _mintBatch( address to, uint256 faceValue, uint256 quantity, bool withStores ) internal returns (uint256) { bytes32[] memory stores; if (withStores) { stores = new bytes32[](2); stores[0] = storeA; stores[1] = storeB; } else { stores = new bytes32[](0); } ICouponBatch.BatchCouponConfig memory config = ICouponBatch.BatchCouponConfig({ transferable: true, maxPrice: faceValue, expiryDate: block.timestamp + 180 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: issuer, faceValue: faceValue }); vm.prank(factory); return batch.mint(to, faceValue, quantity, config); } }