212 lines
8.0 KiB
Solidity
212 lines
8.0 KiB
Solidity
// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.20;
|
||
|
||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
|
||
import "./interfaces/ICoupon.sol";
|
||
import "./interfaces/ICouponBatch.sol";
|
||
import "./interfaces/ICompliance.sol";
|
||
|
||
/// @title CouponFactory — 券发行工厂
|
||
/// @notice 负责批量铸造券,支持 ERC-721(逐张追踪)和 ERC-1155(同质化批量)双模式
|
||
/// @dev 使用 Transparent Proxy 部署,可升级
|
||
contract CouponFactory is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
|
||
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
||
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
|
||
|
||
ICoupon public couponContract;
|
||
ICouponBatch public couponBatchContract;
|
||
ICompliance public compliance;
|
||
|
||
uint256 public nextBatchId;
|
||
|
||
// 批次信息
|
||
struct BatchInfo {
|
||
address issuer;
|
||
uint256 faceValue;
|
||
uint256 quantity;
|
||
ICoupon.CouponType couponType;
|
||
uint256 createdAt;
|
||
uint256[] tokenIds;
|
||
}
|
||
|
||
mapping(uint256 => BatchInfo) public batches;
|
||
|
||
// --- Events ---
|
||
event CouponBatchMinted(
|
||
address indexed issuer,
|
||
uint256 indexed batchId,
|
||
uint256 faceValue,
|
||
uint256 quantity,
|
||
ICoupon.CouponType couponType
|
||
);
|
||
event CouponContractUpdated(address indexed newCouponContract);
|
||
event CouponBatchContractUpdated(address indexed newCouponBatchContract);
|
||
event ComplianceContractUpdated(address indexed newCompliance);
|
||
event CouponBatch1155Minted(
|
||
address indexed issuer,
|
||
uint256 indexed batchId,
|
||
uint256 typeId,
|
||
uint256 faceValue,
|
||
uint256 quantity
|
||
);
|
||
|
||
function initialize(
|
||
address _couponContract,
|
||
address _compliance,
|
||
address admin
|
||
) external initializer {
|
||
__AccessControl_init();
|
||
__ReentrancyGuard_init();
|
||
|
||
couponContract = ICoupon(_couponContract);
|
||
compliance = ICompliance(_compliance);
|
||
|
||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||
_grantRole(ADMIN_ROLE, admin);
|
||
_grantRole(MINTER_ROLE, admin);
|
||
}
|
||
|
||
/// @notice 批量铸造券
|
||
/// @param issuer 发行方地址
|
||
/// @param faceValue 面值(USDC 精度,6位小数)
|
||
/// @param quantity 数量
|
||
/// @param config 券配置
|
||
/// @return tokenIds 铸造的 token ID 数组
|
||
function mintBatch(
|
||
address issuer,
|
||
uint256 faceValue,
|
||
uint256 quantity,
|
||
ICoupon.CouponConfig calldata config
|
||
) external onlyRole(MINTER_ROLE) nonReentrant returns (uint256[] memory tokenIds) {
|
||
require(issuer != address(0), "CouponFactory: zero address issuer");
|
||
require(faceValue > 0, "CouponFactory: zero face value");
|
||
require(quantity > 0 && quantity <= 10000, "CouponFactory: invalid quantity");
|
||
|
||
// Utility Track 强制:价格上限 = 面值,最长12个月
|
||
if (config.couponType == ICoupon.CouponType.Utility) {
|
||
require(config.maxPrice <= faceValue, "Utility: maxPrice <= faceValue");
|
||
require(
|
||
config.expiryDate <= block.timestamp + 365 days,
|
||
"Utility: max 12 months"
|
||
);
|
||
require(config.expiryDate > block.timestamp, "Utility: expiry must be future");
|
||
}
|
||
|
||
// Securities Track 强制:必须通过合格投资者验证
|
||
if (config.couponType == ICoupon.CouponType.Security) {
|
||
require(config.expiryDate > 0, "Security: expiry required");
|
||
// 发行方必须 KYC L3
|
||
compliance.requireKycLevel(issuer, 3);
|
||
}
|
||
|
||
// 铸造
|
||
tokenIds = new uint256[](quantity);
|
||
ICoupon.CouponConfig memory mintConfig = ICoupon.CouponConfig({
|
||
couponType: config.couponType,
|
||
transferable: config.transferable,
|
||
maxResaleCount: config.maxResaleCount,
|
||
maxPrice: config.maxPrice,
|
||
expiryDate: config.expiryDate,
|
||
minPurchase: config.minPurchase,
|
||
stackable: config.stackable,
|
||
allowedStores: config.allowedStores,
|
||
issuer: issuer,
|
||
faceValue: faceValue
|
||
});
|
||
|
||
for (uint256 i = 0; i < quantity; i++) {
|
||
tokenIds[i] = couponContract.mint(issuer, faceValue, mintConfig);
|
||
}
|
||
|
||
// 记录批次
|
||
uint256 batchId = nextBatchId++;
|
||
batches[batchId] = BatchInfo({
|
||
issuer: issuer,
|
||
faceValue: faceValue,
|
||
quantity: quantity,
|
||
couponType: config.couponType,
|
||
createdAt: block.timestamp,
|
||
tokenIds: tokenIds
|
||
});
|
||
|
||
emit CouponBatchMinted(issuer, batchId, faceValue, quantity, config.couponType);
|
||
}
|
||
|
||
/// @notice 查询批次信息
|
||
function getBatchInfo(uint256 batchId) external view returns (BatchInfo memory) {
|
||
return batches[batchId];
|
||
}
|
||
|
||
/// @notice 更新 Coupon 合约地址
|
||
function setCouponContract(address _couponContract) external onlyRole(ADMIN_ROLE) {
|
||
require(_couponContract != address(0), "CouponFactory: zero address");
|
||
couponContract = ICoupon(_couponContract);
|
||
emit CouponContractUpdated(_couponContract);
|
||
}
|
||
|
||
/// @notice 更新 Compliance 合约地址
|
||
function setComplianceContract(address _compliance) external onlyRole(ADMIN_ROLE) {
|
||
require(_compliance != address(0), "CouponFactory: zero address");
|
||
compliance = ICompliance(_compliance);
|
||
emit ComplianceContractUpdated(_compliance);
|
||
}
|
||
|
||
// =====================================================
|
||
// ERC-1155 同质化券铸造(Utility Track 专用)
|
||
// =====================================================
|
||
|
||
/// @notice 铸造 ERC-1155 同质化券(Utility Track 专用)
|
||
/// @param issuer 发行方地址
|
||
/// @param faceValue 面值(USDC 精度,6位小数)
|
||
/// @param quantity 数量(上限 100,000,ERC-1155 为 O(1) 铸造)
|
||
/// @param config 券配置
|
||
/// @return typeId ERC-1155 token type ID
|
||
function mintBatch1155(
|
||
address issuer,
|
||
uint256 faceValue,
|
||
uint256 quantity,
|
||
ICouponBatch.BatchCouponConfig calldata config
|
||
) external onlyRole(MINTER_ROLE) nonReentrant returns (uint256 typeId) {
|
||
require(issuer != address(0), "CouponFactory: zero address issuer");
|
||
require(faceValue > 0, "CouponFactory: zero face value");
|
||
require(quantity > 0 && quantity <= 100000, "CouponFactory: invalid quantity");
|
||
require(
|
||
address(couponBatchContract) != address(0),
|
||
"CouponFactory: batch contract not set"
|
||
);
|
||
|
||
// Utility Track 强制(ERC-1155 仅限 Utility)
|
||
require(config.maxPrice <= faceValue, "Utility: maxPrice <= faceValue");
|
||
require(
|
||
config.expiryDate <= block.timestamp + 365 days,
|
||
"Utility: max 12 months"
|
||
);
|
||
require(config.expiryDate > block.timestamp, "Utility: expiry must be future");
|
||
|
||
// 通过 CouponBatch 合约铸造
|
||
typeId = couponBatchContract.mint(issuer, faceValue, quantity, config);
|
||
|
||
// 记录批次(tokenIds 为空,ERC-1155 使用 typeId + quantity)
|
||
uint256 batchId = nextBatchId++;
|
||
batches[batchId] = BatchInfo({
|
||
issuer: issuer,
|
||
faceValue: faceValue,
|
||
quantity: quantity,
|
||
couponType: ICoupon.CouponType.Utility,
|
||
createdAt: block.timestamp,
|
||
tokenIds: new uint256[](0)
|
||
});
|
||
|
||
emit CouponBatch1155Minted(issuer, batchId, typeId, faceValue, quantity);
|
||
}
|
||
|
||
/// @notice 设置 CouponBatch (ERC-1155) 合约地址
|
||
function setCouponBatchContract(address _couponBatchContract) external onlyRole(ADMIN_ROLE) {
|
||
require(_couponBatchContract != address(0), "CouponFactory: zero address");
|
||
couponBatchContract = ICouponBatch(_couponBatchContract);
|
||
emit CouponBatchContractUpdated(_couponBatchContract);
|
||
}
|
||
}
|