// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/Settlement.sol"; import "../src/Coupon.sol"; import "../src/Compliance.sol"; import "../src/interfaces/ICoupon.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockUSDC is ERC20 { constructor() ERC20("USD Coin", "USDC") { _mint(msg.sender, 1_000_000e6); } function decimals() public pure override returns (uint8) { return 6; } function mint(address to, uint256 amount) external { _mint(to, amount); } } contract SettlementTest is Test { Settlement settlement; Coupon coupon; Compliance compliance; MockUSDC usdc; MockUSDC usdt; address admin = address(1); address buyer = address(4); address seller = address(5); function setUp() public { vm.startPrank(admin); usdc = new MockUSDC(); usdt = new MockUSDC(); compliance = new Compliance(); compliance.initialize(admin); coupon = new Coupon(); coupon.initialize("Genex Coupon", "GXC", admin); settlement = new Settlement(); settlement.initialize(address(coupon), address(compliance), address(usdc), admin); // 授权 coupon.grantRole(keccak256("FACTORY_ROLE"), admin); coupon.grantRole(keccak256("SETTLER_ROLE"), address(settlement)); // KYC compliance.setKycLevel(buyer, 1); compliance.setKycLevel(seller, 1); // 给买方 USDC usdc.mint(buyer, 1000e6); vm.stopPrank(); // Buyer approve settlement vm.prank(buyer); usdc.approve(address(settlement), type(uint256).max); // Seller approve coupon to settlement vm.prank(seller); coupon.setApprovalForAll(address(settlement), true); } function test_AtomicSwapSuccess() public { uint256 tokenId = _mintCouponToSeller(100e6); vm.prank(admin); settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdc)); assertEq(coupon.ownerOf(tokenId), buyer); assertEq(usdc.balanceOf(seller), 85e6); } function test_UtilityCannotExceedMaxPrice() public { uint256 tokenId = _mintCouponToSeller(100e6); vm.prank(admin); vm.expectRevert("Utility: price exceeds max price"); settlement.executeSwap(tokenId, buyer, seller, 110e6, address(usdc)); } function test_RefundReverseSwap() public { uint256 tokenId = _mintCouponToSeller(100e6); // 先正常交易 vm.prank(admin); settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdc)); // Seller 退 USDC approve vm.prank(seller); usdc.approve(address(settlement), type(uint256).max); // Buyer approve coupon vm.prank(buyer); coupon.setApprovalForAll(address(settlement), true); // 退款 vm.prank(admin); settlement.executeRefund(tokenId, buyer, seller, 85e6, address(usdc)); assertEq(coupon.ownerOf(tokenId), seller); } function test_BlacklistedBuyerReverts() public { uint256 tokenId = _mintCouponToSeller(100e6); vm.prank(admin); compliance.addToBlacklist(buyer, "OFAC"); vm.prank(admin); vm.expectRevert("Compliance: buyer blacklisted"); settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdc)); } function test_MaxResaleCountBlocks() public { uint256 tokenId = _mintCouponToSeller(100e6); // 交易 3 次达到上限 for (uint256 i = 0; i < 3; i++) { vm.prank(admin); settlement.executeSwap(tokenId, buyer, seller, 50e6, address(usdc)); // Reset: transfer back + fund buyer (skip after last iteration // because resaleCount == maxResaleCount and P2P transfer would fail) if (i < 2) { vm.prank(buyer); coupon.safeTransferFrom(buyer, seller, tokenId); vm.prank(admin); usdc.mint(buyer, 50e6); } } // 第 4 次应失败 vm.prank(admin); vm.expectRevert("Settlement: max resale count exceeded"); settlement.executeSwap(tokenId, buyer, seller, 50e6, address(usdc)); } function test_ExpiredCouponReverts() public { uint256 tokenId = _mintCouponToSeller(100e6); // 快进到过期 vm.warp(block.timestamp + 200 days); vm.prank(admin); vm.expectRevert("Settlement: coupon expired"); settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdc)); } function test_UnsupportedStablecoinReverts() public { uint256 tokenId = _mintCouponToSeller(100e6); vm.prank(admin); vm.expectRevert("Settlement: unsupported stablecoin"); settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdt)); } function test_AddStablecoin() public { vm.prank(admin); settlement.addStablecoin(address(usdt)); assertTrue(settlement.supportedStablecoins(address(usdt))); } // --- Helpers --- function _mintCouponToSeller(uint256 faceValue) internal returns (uint256) { bytes32[] memory stores = new bytes32[](0); ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ couponType: ICoupon.CouponType.Utility, transferable: true, maxResaleCount: 3, maxPrice: faceValue, expiryDate: block.timestamp + 180 days, minPurchase: 0, stackable: false, allowedStores: stores, issuer: admin, faceValue: faceValue }); vm.prank(admin); return coupon.mint(seller, faceValue, config); } }