193 lines
5.7 KiB
Solidity
193 lines
5.7 KiB
Solidity
// 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);
|
|
}
|
|
}
|