260 lines
8.2 KiB
Solidity
260 lines
8.2 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "forge-std/Test.sol";
|
|
import "../src/CouponBackedSecurity.sol";
|
|
import "../src/Coupon.sol";
|
|
import "../src/Compliance.sol";
|
|
import "../src/interfaces/ICoupon.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
|
|
contract MockUSDC2 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); }
|
|
}
|
|
|
|
contract CouponBackedSecurityTest is Test {
|
|
CouponBackedSecurity cbs;
|
|
Coupon coupon;
|
|
Compliance compliance;
|
|
MockUSDC2 usdc;
|
|
|
|
address admin = address(1);
|
|
address cbsIssuer = address(2);
|
|
address investor1 = address(3);
|
|
address investor2 = address(4);
|
|
address settler = address(5);
|
|
address ratingAgency = address(6);
|
|
|
|
function setUp() public {
|
|
vm.startPrank(admin);
|
|
|
|
usdc = new MockUSDC2();
|
|
compliance = new Compliance();
|
|
compliance.initialize(admin);
|
|
|
|
coupon = new Coupon();
|
|
coupon.initialize("Genex Coupon", "GXC", admin);
|
|
coupon.grantRole(keccak256("FACTORY_ROLE"), admin);
|
|
|
|
cbs = new CouponBackedSecurity();
|
|
cbs.initialize(address(coupon), address(compliance), address(usdc), admin);
|
|
cbs.grantRole(keccak256("ISSUER_ROLE"), cbsIssuer);
|
|
cbs.grantRole(keccak256("SETTLER_ROLE"), settler);
|
|
cbs.grantRole(keccak256("RATING_AGENCY_ROLE"), ratingAgency);
|
|
|
|
// KYC
|
|
compliance.setKycLevel(cbsIssuer, 3);
|
|
compliance.setKycLevel(investor1, 2);
|
|
compliance.setKycLevel(investor2, 2);
|
|
|
|
// Fund
|
|
usdc.mint(investor1, 1_000_000e6);
|
|
usdc.mint(investor2, 1_000_000e6);
|
|
usdc.mint(settler, 1_000_000e6);
|
|
|
|
vm.stopPrank();
|
|
|
|
// Approvals
|
|
vm.prank(investor1);
|
|
usdc.approve(address(cbs), type(uint256).max);
|
|
vm.prank(investor2);
|
|
usdc.approve(address(cbs), type(uint256).max);
|
|
vm.prank(settler);
|
|
usdc.approve(address(cbs), type(uint256).max);
|
|
vm.prank(cbsIssuer);
|
|
coupon.setApprovalForAll(address(cbs), true);
|
|
}
|
|
|
|
function test_CreatePool() public {
|
|
uint256[] memory tokenIds = _mintSecurityCoupons(3, 1000e6);
|
|
|
|
vm.prank(cbsIssuer);
|
|
uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days);
|
|
|
|
(
|
|
uint256 totalFaceValue,
|
|
uint256 totalShares,
|
|
uint256 soldShares,
|
|
,
|
|
,
|
|
address issuer,
|
|
bool active
|
|
) = cbs.getPool(poolId);
|
|
|
|
assertEq(totalFaceValue, 3000e6);
|
|
assertEq(totalShares, 100);
|
|
assertEq(soldShares, 0);
|
|
assertEq(issuer, cbsIssuer);
|
|
assertTrue(active);
|
|
}
|
|
|
|
function test_PurchaseShares() public {
|
|
uint256[] memory tokenIds = _mintSecurityCoupons(2, 500e6);
|
|
vm.prank(cbsIssuer);
|
|
uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days);
|
|
|
|
vm.prank(investor1);
|
|
cbs.purchaseShares(poolId, 30);
|
|
|
|
assertEq(cbs.shares(poolId, investor1), 30);
|
|
(, , uint256 soldShares, , , , ) = cbs.getPool(poolId);
|
|
assertEq(soldShares, 30);
|
|
}
|
|
|
|
function test_PurchaseSharesInsufficientReverts() public {
|
|
uint256[] memory tokenIds = _mintSecurityCoupons(1, 100e6);
|
|
vm.prank(cbsIssuer);
|
|
uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days);
|
|
|
|
vm.prank(investor1);
|
|
vm.expectRevert("CBS: insufficient shares");
|
|
cbs.purchaseShares(poolId, 11);
|
|
}
|
|
|
|
function test_PurchaseSharesRequiresKycL2() public {
|
|
uint256[] memory tokenIds = _mintSecurityCoupons(1, 100e6);
|
|
vm.prank(cbsIssuer);
|
|
uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days);
|
|
|
|
address lowKyc = address(99);
|
|
vm.prank(admin);
|
|
compliance.setKycLevel(lowKyc, 1);
|
|
|
|
vm.prank(lowKyc);
|
|
vm.expectRevert("Compliance: insufficient KYC level");
|
|
cbs.purchaseShares(poolId, 1);
|
|
}
|
|
|
|
function test_OnlySecurityCouponsAllowed() public {
|
|
// Mint utility coupon
|
|
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: cbsIssuer,
|
|
faceValue: 100e6
|
|
});
|
|
vm.prank(admin);
|
|
uint256 tokenId = coupon.mint(cbsIssuer, 100e6, config);
|
|
|
|
uint256[] memory tokenIds = new uint256[](1);
|
|
tokenIds[0] = tokenId;
|
|
|
|
vm.prank(cbsIssuer);
|
|
vm.expectRevert("CBS: only Security coupons");
|
|
cbs.createPool(tokenIds, 10, block.timestamp + 365 days);
|
|
}
|
|
|
|
function test_SetCreditRating() public {
|
|
uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6);
|
|
vm.prank(cbsIssuer);
|
|
uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days);
|
|
|
|
vm.prank(ratingAgency);
|
|
cbs.setCreditRating(poolId, "AAA");
|
|
|
|
(, , , , string memory rating, , ) = cbs.getPool(poolId);
|
|
assertEq(rating, "AAA");
|
|
}
|
|
|
|
function test_DepositAndClaimYield() public {
|
|
uint256[] memory tokenIds = _mintSecurityCoupons(2, 500e6);
|
|
vm.prank(cbsIssuer);
|
|
uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days);
|
|
|
|
// 两个投资者各买 50 份
|
|
vm.prank(investor1);
|
|
cbs.purchaseShares(poolId, 50);
|
|
vm.prank(investor2);
|
|
cbs.purchaseShares(poolId, 50);
|
|
|
|
// Settler 存入收益 1000 USDC
|
|
vm.prank(settler);
|
|
cbs.depositYield(poolId, 1000e6);
|
|
|
|
// 各自领取 500 USDC
|
|
vm.prank(investor1);
|
|
cbs.claimYield(poolId);
|
|
assertEq(usdc.balanceOf(investor1), 1_000_000e6 - 500e6 + 500e6); // 购买花了500e6, 领回500e6
|
|
|
|
vm.prank(investor2);
|
|
cbs.claimYield(poolId);
|
|
}
|
|
|
|
function test_ClaimYieldNothingToClaimReverts() public {
|
|
uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6);
|
|
vm.prank(cbsIssuer);
|
|
uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days);
|
|
|
|
vm.prank(investor1);
|
|
cbs.purchaseShares(poolId, 5);
|
|
|
|
// 没有收益存入
|
|
vm.prank(investor1);
|
|
vm.expectRevert("CBS: nothing to claim");
|
|
cbs.claimYield(poolId);
|
|
}
|
|
|
|
function test_DeactivatePool() public {
|
|
uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6);
|
|
vm.prank(cbsIssuer);
|
|
uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days);
|
|
|
|
vm.prank(admin);
|
|
cbs.deactivatePool(poolId);
|
|
|
|
(, , , , , , bool active) = cbs.getPool(poolId);
|
|
assertFalse(active);
|
|
}
|
|
|
|
function test_GetClaimableYield() public {
|
|
uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6);
|
|
vm.prank(cbsIssuer);
|
|
uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days);
|
|
|
|
vm.prank(investor1);
|
|
cbs.purchaseShares(poolId, 25);
|
|
|
|
vm.prank(settler);
|
|
cbs.depositYield(poolId, 400e6);
|
|
|
|
uint256 claimable = cbs.getClaimableYield(poolId, investor1);
|
|
assertEq(claimable, 100e6); // 25% of 400
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
function _mintSecurityCoupons(uint256 count, uint256 faceValue) internal returns (uint256[] memory) {
|
|
uint256[] memory tokenIds = new uint256[](count);
|
|
bytes32[] memory stores = new bytes32[](0);
|
|
|
|
ICoupon.CouponConfig memory config = ICoupon.CouponConfig({
|
|
couponType: ICoupon.CouponType.Security,
|
|
transferable: true,
|
|
maxResaleCount: 10,
|
|
maxPrice: 0,
|
|
expiryDate: block.timestamp + 730 days,
|
|
minPurchase: 0,
|
|
stackable: false,
|
|
allowedStores: stores,
|
|
issuer: cbsIssuer,
|
|
faceValue: faceValue
|
|
});
|
|
|
|
for (uint256 i = 0; i < count; i++) {
|
|
vm.prank(admin);
|
|
tokenIds[i] = coupon.mint(cbsIssuer, faceValue, config);
|
|
}
|
|
|
|
return tokenIds;
|
|
}
|
|
}
|