gcx/blockchain/genex-contracts/test/Integration.t.sol

303 lines
11 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/CouponFactory.sol";
import "../src/Coupon.sol";
import "../src/Settlement.sol";
import "../src/Redemption.sol";
import "../src/Compliance.sol";
import "../src/Treasury.sol";
import "../src/interfaces/ICoupon.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockUSDCIntegration 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 端到端集成测试
/// @notice 完整流程: 发行 → 一级市场购买 → 二级市场交易 → 兑付核销
contract IntegrationTest is Test {
CouponFactory factory;
Coupon coupon;
Settlement settlement;
Redemption redemption;
Compliance compliance;
Treasury treasury;
MockUSDCIntegration usdc;
address admin = address(1);
address issuer = address(2); // 发行方
address buyer1 = address(3); // 一级市场买方
address buyer2 = address(4); // 二级市场买方
address feeCollector = address(5); // 平台手续费
function setUp() public {
vm.startPrank(admin);
usdc = new MockUSDCIntegration();
// 部署所有合约
compliance = new Compliance();
compliance.initialize(admin);
coupon = new Coupon();
coupon.initialize("Genex Coupon", "GXC", 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);
treasury = new Treasury();
treasury.initialize(address(usdc), feeCollector, admin);
// 角色授权
coupon.grantRole(keccak256("FACTORY_ROLE"), address(factory));
coupon.grantRole(keccak256("SETTLER_ROLE"), address(settlement));
coupon.grantRole(keccak256("SETTLER_ROLE"), address(redemption));
// KYC 设置
compliance.setKycLevel(issuer, 3); // L3 发行方
compliance.setKycLevel(buyer1, 1); // L1 普通用户
compliance.setKycLevel(buyer2, 1); // L1 普通用户
// 资金准备
usdc.mint(buyer1, 500_000e6);
usdc.mint(buyer2, 500_000e6);
usdc.mint(issuer, 100_000e6);
vm.stopPrank();
// 全局 approve
vm.prank(buyer1);
usdc.approve(address(settlement), type(uint256).max);
vm.prank(buyer2);
usdc.approve(address(settlement), type(uint256).max);
vm.prank(buyer1);
coupon.setApprovalForAll(address(settlement), true);
vm.prank(buyer2);
coupon.setApprovalForAll(address(settlement), true);
vm.prank(issuer);
coupon.setApprovalForAll(address(settlement), true);
}
/// @notice 完整 Utility 券生命周期: 铸造 → 一级购买 → 二级交易 → 兑付
function test_FullUtilityLifecycle() public {
// ========== Step 1: 发行方铸造券 ==========
bytes32[] memory stores = new bytes32[](0);
ICoupon.CouponConfig memory config = ICoupon.CouponConfig({
couponType: ICoupon.CouponType.Utility,
transferable: true,
maxResaleCount: 3,
maxPrice: 100e6, // 面值上限 $100
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: stores,
issuer: issuer,
faceValue: 100e6
});
vm.prank(admin);
uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 5, config);
assertEq(tokenIds.length, 5);
assertEq(coupon.ownerOf(tokenIds[0]), issuer);
// ========== Step 2: 一级市场 — 发行方卖给 buyer1 ==========
vm.prank(admin);
settlement.executeSwap(tokenIds[0], buyer1, issuer, 100e6, address(usdc));
assertEq(coupon.ownerOf(tokenIds[0]), buyer1);
assertEq(usdc.balanceOf(issuer), 100_000e6 + 100e6);
assertEq(coupon.getResaleCount(tokenIds[0]), 1);
// ========== Step 3: 二级市场 — buyer1 以 $85 卖给 buyer2 ==========
vm.prank(admin);
settlement.executeSwap(tokenIds[0], buyer2, buyer1, 85e6, address(usdc));
assertEq(coupon.ownerOf(tokenIds[0]), buyer2);
assertEq(coupon.getResaleCount(tokenIds[0]), 2);
// ========== Step 4: 兑付 — buyer2 去门店使用 ==========
vm.prank(buyer2);
redemption.redeem(tokenIds[0], keccak256("ANY_STORE"));
// 券已销毁
vm.expectRevert();
coupon.ownerOf(tokenIds[0]);
// 验证兑付记录
Redemption.RedemptionRecord memory record = redemption.getRedemption(0);
assertEq(record.consumer, buyer2);
assertEq(record.issuer, issuer);
}
/// @notice Utility 券价格上限保护
function test_UtilityPriceCap() public {
bytes32[] memory stores = new bytes32[](0);
ICoupon.CouponConfig memory config = 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
});
vm.prank(admin);
uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config);
// 尝试以超过面值的价格交易
vm.prank(admin);
vm.expectRevert("Utility: price exceeds max price");
settlement.executeSwap(tokenIds[0], buyer1, issuer, 120e6, address(usdc));
// 以面值或更低价格交易成功
vm.prank(admin);
settlement.executeSwap(tokenIds[0], buyer1, issuer, 95e6, address(usdc));
}
/// @notice 不可转让券测试
function test_NonTransferableCoupon() public {
bytes32[] memory stores = new bytes32[](0);
ICoupon.CouponConfig memory config = ICoupon.CouponConfig({
couponType: ICoupon.CouponType.Utility,
transferable: false,
maxResaleCount: 0,
maxPrice: 50e6,
expiryDate: block.timestamp + 90 days,
minPurchase: 0,
stackable: false,
allowedStores: stores,
issuer: issuer,
faceValue: 50e6
});
vm.prank(admin);
uint256[] memory tokenIds = factory.mintBatch(issuer, 50e6, 1, config);
// 不可转让券不能在二级市场交易
// With maxResaleCount=0, Settlement's own resale count check (0 < 0 == false)
// fires before the Coupon's _beforeTokenTransfer non-transferable check
vm.prank(admin);
vm.expectRevert("Settlement: max resale count exceeded");
settlement.executeSwap(tokenIds[0], buyer1, issuer, 50e6, address(usdc));
}
/// @notice 合规检查集成
function test_ComplianceBlocksBlacklistedTrader() public {
bytes32[] memory stores = new bytes32[](0);
ICoupon.CouponConfig memory config = 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
});
vm.prank(admin);
uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config);
// 将 buyer1 加入 OFAC 黑名单
vm.prank(admin);
compliance.addToBlacklist(buyer1, "OFAC SDN Match");
// 交易被拒绝
vm.prank(admin);
vm.expectRevert("Compliance: buyer blacklisted");
settlement.executeSwap(tokenIds[0], buyer1, issuer, 100e6, address(usdc));
}
/// @notice 过期券不能交易也不能兑付
function test_ExpiredCouponBlocked() public {
bytes32[] memory stores = new bytes32[](0);
ICoupon.CouponConfig memory config = ICoupon.CouponConfig({
couponType: ICoupon.CouponType.Utility,
transferable: true,
maxResaleCount: 3,
maxPrice: 100e6,
expiryDate: block.timestamp + 30 days,
minPurchase: 0,
stackable: false,
allowedStores: stores,
issuer: issuer,
faceValue: 100e6
});
vm.prank(admin);
uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config);
// 快进到过期
vm.warp(block.timestamp + 31 days);
// 交易被拒绝
vm.prank(admin);
vm.expectRevert("Settlement: coupon expired");
settlement.executeSwap(tokenIds[0], buyer1, issuer, 100e6, address(usdc));
// 兑付也被拒绝
vm.prank(issuer);
vm.expectRevert("Redemption: coupon expired");
redemption.redeem(tokenIds[0], keccak256("STORE"));
}
/// @notice 转售次数用尽后不能再交易
function test_MaxResaleCountBlocksFurtherTrades() public {
bytes32[] memory stores = new bytes32[](0);
ICoupon.CouponConfig memory config = ICoupon.CouponConfig({
couponType: ICoupon.CouponType.Utility,
transferable: true,
maxResaleCount: 2,
maxPrice: 100e6,
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: stores,
issuer: issuer,
faceValue: 100e6
});
vm.prank(admin);
uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config);
// 第1次: issuer → buyer1
vm.prank(admin);
settlement.executeSwap(tokenIds[0], buyer1, issuer, 100e6, address(usdc));
// 第2次: buyer1 → buyer2
vm.prank(admin);
settlement.executeSwap(tokenIds[0], buyer2, buyer1, 90e6, address(usdc));
// 第3次: 应被拒绝已转售2次达到上限
vm.prank(buyer2);
coupon.setApprovalForAll(address(settlement), true);
vm.prank(admin);
usdc.mint(buyer1, 100e6);
vm.prank(admin);
vm.expectRevert("Settlement: max resale count exceeded");
settlement.executeSwap(tokenIds[0], buyer1, buyer2, 80e6, address(usdc));
// 但兑付仍然可以
vm.prank(buyer2);
redemption.redeem(tokenIds[0], keccak256("STORE"));
}
}