308 lines
10 KiB
Solidity
308 lines
10 KiB
Solidity
// 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);
|
|
}
|
|
}
|