290 lines
10 KiB
Solidity
290 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/CouponBatch.sol";
|
||
import "../src/Redemption.sol";
|
||
import "../src/Redemption1155.sol";
|
||
import "../src/Settlement.sol";
|
||
import "../src/Compliance.sol";
|
||
import "../src/interfaces/ICoupon.sol";
|
||
import "../src/interfaces/ICouponBatch.sol";
|
||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||
|
||
contract MockUSDC1155 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); }
|
||
}
|
||
|
||
/// @title ERC-721 + ERC-1155 混合端到端集成测试
|
||
contract Integration1155Test is Test {
|
||
CouponFactory factory;
|
||
Coupon coupon;
|
||
CouponBatch couponBatch;
|
||
Settlement settlement;
|
||
Redemption redemption;
|
||
Redemption1155 redemption1155;
|
||
Compliance compliance;
|
||
MockUSDC1155 usdc;
|
||
|
||
address admin = address(1);
|
||
address issuer = address(2);
|
||
address consumer1 = address(3);
|
||
address consumer2 = address(4);
|
||
|
||
bytes32 storeA = keccak256("STORE_A");
|
||
|
||
function setUp() public {
|
||
vm.startPrank(admin);
|
||
|
||
usdc = new MockUSDC1155();
|
||
|
||
// 部署合约
|
||
compliance = new Compliance();
|
||
compliance.initialize(admin);
|
||
|
||
coupon = new Coupon();
|
||
coupon.initialize("Genex Coupon", "GXC", admin);
|
||
|
||
couponBatch = new CouponBatch();
|
||
couponBatch.initialize("https://genex.io/api/coupon-batch/{id}.json", admin);
|
||
|
||
factory = new CouponFactory();
|
||
factory.initialize(address(coupon), address(compliance), admin);
|
||
|
||
settlement = new Settlement();
|
||
settlement.initialize(address(coupon), address(compliance), address(usdc), admin);
|
||
|
||
redemption = new Redemption();
|
||
redemption.initialize(address(coupon), address(compliance), admin);
|
||
|
||
redemption1155 = new Redemption1155();
|
||
redemption1155.initialize(address(couponBatch), address(compliance), admin);
|
||
|
||
// 角色授权
|
||
coupon.grantRole(keccak256("FACTORY_ROLE"), address(factory));
|
||
coupon.grantRole(keccak256("SETTLER_ROLE"), address(settlement));
|
||
coupon.grantRole(keccak256("SETTLER_ROLE"), address(redemption));
|
||
couponBatch.grantRole(keccak256("FACTORY_ROLE"), address(factory));
|
||
couponBatch.grantRole(keccak256("BURNER_ROLE"), address(redemption1155));
|
||
|
||
// 关联 CouponBatch
|
||
factory.setCouponBatchContract(address(couponBatch));
|
||
|
||
// KYC
|
||
compliance.setKycLevel(issuer, 3);
|
||
compliance.setKycLevel(consumer1, 1);
|
||
compliance.setKycLevel(consumer2, 1);
|
||
|
||
// 资金
|
||
usdc.mint(consumer1, 500_000e6);
|
||
usdc.mint(consumer2, 500_000e6);
|
||
|
||
vm.stopPrank();
|
||
|
||
// Approvals
|
||
vm.prank(consumer1);
|
||
usdc.approve(address(settlement), type(uint256).max);
|
||
vm.prank(consumer1);
|
||
coupon.setApprovalForAll(address(settlement), true);
|
||
vm.prank(issuer);
|
||
coupon.setApprovalForAll(address(settlement), true);
|
||
}
|
||
|
||
/// @notice ERC-1155 完整生命周期: Factory铸造 → 分发 → 兑付
|
||
function test_ERC1155FullLifecycle() public {
|
||
// Step 1: 通过 Factory 铸造 ERC-1155 券
|
||
bytes32[] memory stores = new bytes32[](0);
|
||
ICouponBatch.BatchCouponConfig memory batchConfig = ICouponBatch.BatchCouponConfig({
|
||
transferable: true,
|
||
maxPrice: 50e6,
|
||
expiryDate: block.timestamp + 180 days,
|
||
minPurchase: 0,
|
||
stackable: false,
|
||
allowedStores: stores,
|
||
issuer: issuer,
|
||
faceValue: 50e6
|
||
});
|
||
|
||
vm.prank(admin);
|
||
uint256 typeId = factory.mintBatch1155(issuer, 50e6, 10000, batchConfig);
|
||
|
||
assertEq(couponBatch.balanceOf(issuer, typeId), 10000);
|
||
assertEq(couponBatch.totalSupply(typeId), 10000);
|
||
|
||
// Step 2: 发行方分发给消费者
|
||
vm.prank(issuer);
|
||
couponBatch.safeTransferFrom(issuer, consumer1, typeId, 100, "");
|
||
|
||
assertEq(couponBatch.balanceOf(consumer1, typeId), 100);
|
||
assertEq(couponBatch.balanceOf(issuer, typeId), 9900);
|
||
|
||
// Step 3: 消费者兑付
|
||
vm.prank(consumer1);
|
||
redemption1155.redeem(typeId, 5, storeA);
|
||
|
||
assertEq(couponBatch.balanceOf(consumer1, typeId), 95);
|
||
assertEq(couponBatch.totalSupply(typeId), 9995); // 5 已销毁
|
||
|
||
Redemption1155.RedemptionRecord memory record = redemption1155.getRedemption(0);
|
||
assertEq(record.consumer, consumer1);
|
||
assertEq(record.amount, 5);
|
||
}
|
||
|
||
/// @notice 同一 Factory 同时管理 ERC-721 和 ERC-1155 券
|
||
function test_MixedERC721AndERC1155() public {
|
||
// 铸造 ERC-721 券
|
||
ICoupon.CouponConfig memory config721 = ICoupon.CouponConfig({
|
||
couponType: ICoupon.CouponType.Utility,
|
||
transferable: true,
|
||
maxResaleCount: 3,
|
||
maxPrice: 100e6,
|
||
expiryDate: block.timestamp + 180 days,
|
||
minPurchase: 0,
|
||
stackable: false,
|
||
allowedStores: new bytes32[](0),
|
||
issuer: issuer,
|
||
faceValue: 100e6
|
||
});
|
||
|
||
vm.prank(admin);
|
||
uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 3, config721);
|
||
|
||
// 铸造 ERC-1155 券
|
||
ICouponBatch.BatchCouponConfig memory config1155 = ICouponBatch.BatchCouponConfig({
|
||
transferable: true,
|
||
maxPrice: 50e6,
|
||
expiryDate: block.timestamp + 180 days,
|
||
minPurchase: 0,
|
||
stackable: false,
|
||
allowedStores: new bytes32[](0),
|
||
issuer: issuer,
|
||
faceValue: 50e6
|
||
});
|
||
|
||
vm.prank(admin);
|
||
uint256 typeId = factory.mintBatch1155(issuer, 50e6, 5000, config1155);
|
||
|
||
// 验证两种标准互不干扰
|
||
assertEq(coupon.ownerOf(tokenIds[0]), issuer);
|
||
assertEq(couponBatch.balanceOf(issuer, typeId), 5000);
|
||
|
||
// BatchInfo 共享 nextBatchId 计数器
|
||
CouponFactory.BatchInfo memory batch0 = factory.getBatchInfo(0);
|
||
assertEq(batch0.tokenIds.length, 3); // ERC-721
|
||
|
||
CouponFactory.BatchInfo memory batch1 = factory.getBatchInfo(1);
|
||
assertEq(batch1.tokenIds.length, 0); // ERC-1155
|
||
assertEq(batch1.quantity, 5000);
|
||
}
|
||
|
||
/// @notice ERC-721 走 Settlement 二级市场,ERC-1155 走 Redemption1155 兑付
|
||
function test_DualTrackSettlementAndRedemption() public {
|
||
// ERC-721: 铸造 + 一级购买 + 兑付
|
||
ICoupon.CouponConfig memory config721 = ICoupon.CouponConfig({
|
||
couponType: ICoupon.CouponType.Utility,
|
||
transferable: true,
|
||
maxResaleCount: 3,
|
||
maxPrice: 100e6,
|
||
expiryDate: block.timestamp + 180 days,
|
||
minPurchase: 0,
|
||
stackable: false,
|
||
allowedStores: new bytes32[](0),
|
||
issuer: issuer,
|
||
faceValue: 100e6
|
||
});
|
||
|
||
vm.prank(admin);
|
||
uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config721);
|
||
|
||
vm.prank(admin);
|
||
settlement.executeSwap(tokenIds[0], consumer1, issuer, 100e6, address(usdc));
|
||
assertEq(coupon.ownerOf(tokenIds[0]), consumer1);
|
||
|
||
vm.prank(consumer1);
|
||
redemption.redeem(tokenIds[0], storeA);
|
||
|
||
// ERC-1155: 铸造 + 分发 + 兑付
|
||
ICouponBatch.BatchCouponConfig memory config1155 = ICouponBatch.BatchCouponConfig({
|
||
transferable: true,
|
||
maxPrice: 20e6,
|
||
expiryDate: block.timestamp + 90 days,
|
||
minPurchase: 0,
|
||
stackable: true,
|
||
allowedStores: new bytes32[](0),
|
||
issuer: issuer,
|
||
faceValue: 20e6
|
||
});
|
||
|
||
vm.prank(admin);
|
||
uint256 typeId = factory.mintBatch1155(issuer, 20e6, 50000, config1155);
|
||
|
||
vm.prank(issuer);
|
||
couponBatch.safeTransferFrom(issuer, consumer1, typeId, 200, "");
|
||
|
||
vm.prank(consumer1);
|
||
redemption1155.redeem(typeId, 10, storeA);
|
||
|
||
assertEq(couponBatch.balanceOf(consumer1, typeId), 190);
|
||
}
|
||
|
||
/// @notice 合规系统同时影响两种标准
|
||
function test_ComplianceAffectsBothStandards() public {
|
||
// 铸造 ERC-1155
|
||
ICouponBatch.BatchCouponConfig memory config1155 = ICouponBatch.BatchCouponConfig({
|
||
transferable: true,
|
||
maxPrice: 100e6,
|
||
expiryDate: block.timestamp + 180 days,
|
||
minPurchase: 0,
|
||
stackable: false,
|
||
allowedStores: new bytes32[](0),
|
||
issuer: issuer,
|
||
faceValue: 100e6
|
||
});
|
||
|
||
vm.prank(admin);
|
||
uint256 typeId = factory.mintBatch1155(issuer, 100e6, 1000, config1155);
|
||
|
||
vm.prank(issuer);
|
||
couponBatch.safeTransferFrom(issuer, consumer1, typeId, 50, "");
|
||
|
||
// 将 consumer1 加入黑名单
|
||
vm.prank(admin);
|
||
compliance.addToBlacklist(consumer1, "OFAC");
|
||
|
||
// ERC-1155 兑付被拒绝
|
||
vm.prank(consumer1);
|
||
vm.expectRevert("Redemption1155: blacklisted");
|
||
redemption1155.redeem(typeId, 1, storeA);
|
||
}
|
||
|
||
/// @notice 非可转让 ERC-1155 券不能转移
|
||
function test_NonTransferableERC1155() public {
|
||
ICouponBatch.BatchCouponConfig memory config = ICouponBatch.BatchCouponConfig({
|
||
transferable: false,
|
||
maxPrice: 30e6,
|
||
expiryDate: block.timestamp + 90 days,
|
||
minPurchase: 0,
|
||
stackable: false,
|
||
allowedStores: new bytes32[](0),
|
||
issuer: issuer,
|
||
faceValue: 30e6
|
||
});
|
||
|
||
vm.prank(admin);
|
||
uint256 typeId = factory.mintBatch1155(issuer, 30e6, 1000, config);
|
||
|
||
// 不可转让
|
||
vm.prank(issuer);
|
||
vm.expectRevert("CouponBatch: non-transferable");
|
||
couponBatch.safeTransferFrom(issuer, consumer1, typeId, 10, "");
|
||
|
||
// 但发行方自己可以兑付(通过 BURNER_ROLE 销毁)
|
||
// — 不过 Redemption1155 检查的是 msg.sender 的 balance
|
||
assertEq(couponBatch.balanceOf(issuer, typeId), 1000);
|
||
}
|
||
}
|