162 lines
5.5 KiB
Solidity
162 lines
5.5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.20;
|
||
|
||
import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol";
|
||
import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol";
|
||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||
import "./interfaces/ICouponBatch.sol";
|
||
|
||
/// @title CouponBatch — ERC-1155 同质化券合约
|
||
/// @notice 支持批量铸造相同配置的券,适用于促销折扣券、空投券等场景
|
||
/// @dev Utility Track 专用,无转售次数追踪。Transparent Proxy 部署。
|
||
contract CouponBatch is
|
||
Initializable,
|
||
ERC1155Upgradeable,
|
||
ERC1155SupplyUpgradeable,
|
||
AccessControlUpgradeable
|
||
{
|
||
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
||
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
|
||
bytes32 public constant FACTORY_ROLE = keccak256("FACTORY_ROLE");
|
||
|
||
uint256 private _nextTypeId;
|
||
|
||
/// @dev typeId → 券配置(铸造后不可更改)
|
||
mapping(uint256 => ICouponBatch.BatchCouponConfig) private _configs;
|
||
/// @dev typeId → 原始铸造数量(用于统计)
|
||
mapping(uint256 => uint256) private _mintedQuantities;
|
||
|
||
// --- Events ---
|
||
event BatchCouponMinted(
|
||
uint256 indexed typeId,
|
||
address indexed issuer,
|
||
uint256 faceValue,
|
||
uint256 quantity
|
||
);
|
||
event BatchCouponBurned(
|
||
uint256 indexed typeId,
|
||
address indexed account,
|
||
uint256 amount
|
||
);
|
||
|
||
function initialize(string memory uri_, address admin) external initializer {
|
||
__ERC1155_init(uri_);
|
||
__ERC1155Supply_init();
|
||
__AccessControl_init();
|
||
|
||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||
_grantRole(MINTER_ROLE, admin);
|
||
_nextTypeId = 1;
|
||
}
|
||
|
||
/// @notice 创建新券类型并铸造 `quantity` 单位到 `to`
|
||
/// @param to 接收者地址
|
||
/// @param faceValue 面值
|
||
/// @param quantity 数量
|
||
/// @param config 券配置
|
||
/// @return typeId 新创建的 token type ID
|
||
function mint(
|
||
address to,
|
||
uint256 faceValue,
|
||
uint256 quantity,
|
||
ICouponBatch.BatchCouponConfig calldata config
|
||
) external onlyRole(FACTORY_ROLE) returns (uint256 typeId) {
|
||
require(to != address(0), "CouponBatch: zero address");
|
||
require(faceValue > 0, "CouponBatch: zero face value");
|
||
require(quantity > 0, "CouponBatch: zero quantity");
|
||
|
||
typeId = _nextTypeId++;
|
||
|
||
// 存储配置(确保 faceValue 和 issuer 字段一致)
|
||
_configs[typeId] = ICouponBatch.BatchCouponConfig({
|
||
transferable: config.transferable,
|
||
maxPrice: config.maxPrice,
|
||
expiryDate: config.expiryDate,
|
||
minPurchase: config.minPurchase,
|
||
stackable: config.stackable,
|
||
allowedStores: config.allowedStores,
|
||
issuer: config.issuer,
|
||
faceValue: faceValue
|
||
});
|
||
_mintedQuantities[typeId] = quantity;
|
||
|
||
// 单次 ERC-1155 mint — O(1) 存储
|
||
_mint(to, typeId, quantity, "");
|
||
|
||
emit BatchCouponMinted(typeId, config.issuer, faceValue, quantity);
|
||
}
|
||
|
||
/// @notice 销毁 `amount` 单位的 typeId 券
|
||
/// @param account 被销毁的地址
|
||
/// @param typeId 券类型 ID
|
||
/// @param amount 销毁数量
|
||
function burn(
|
||
address account,
|
||
uint256 typeId,
|
||
uint256 amount
|
||
) external {
|
||
require(
|
||
account == _msgSender()
|
||
|| isApprovedForAll(account, _msgSender())
|
||
|| hasRole(BURNER_ROLE, _msgSender()),
|
||
"CouponBatch: not authorized to burn"
|
||
);
|
||
_burn(account, typeId, amount);
|
||
emit BatchCouponBurned(typeId, account, amount);
|
||
}
|
||
|
||
// --- 查询函数 ---
|
||
|
||
/// @notice 获取券类型配置
|
||
function getConfig(uint256 typeId) external view returns (ICouponBatch.BatchCouponConfig memory) {
|
||
require(exists(typeId), "CouponBatch: nonexistent type");
|
||
return _configs[typeId];
|
||
}
|
||
|
||
/// @notice 获取面值
|
||
function getFaceValue(uint256 typeId) external view returns (uint256) {
|
||
require(exists(typeId), "CouponBatch: nonexistent type");
|
||
return _configs[typeId].faceValue;
|
||
}
|
||
|
||
/// @notice 获取原始铸造数量
|
||
function getMintedQuantity(uint256 typeId) external view returns (uint256) {
|
||
return _mintedQuantities[typeId];
|
||
}
|
||
|
||
// --- 转让限制 ---
|
||
|
||
/// @dev 重写 _beforeTokenTransfer 强制非转让检查
|
||
function _beforeTokenTransfer(
|
||
address operator,
|
||
address from,
|
||
address to,
|
||
uint256[] memory ids,
|
||
uint256[] memory amounts,
|
||
bytes memory data
|
||
) internal virtual override(ERC1155Upgradeable, ERC1155SupplyUpgradeable) {
|
||
super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
|
||
|
||
// 铸造 (from=0) 和销毁 (to=0) 不受限
|
||
if (from == address(0) || to == address(0)) return;
|
||
|
||
// 检查每个 token type 的可转让性
|
||
for (uint256 i = 0; i < ids.length; i++) {
|
||
ICouponBatch.BatchCouponConfig memory config = _configs[ids[i]];
|
||
require(config.transferable, "CouponBatch: non-transferable");
|
||
}
|
||
}
|
||
|
||
// --- ERC165 ---
|
||
|
||
function supportsInterface(bytes4 interfaceId)
|
||
public
|
||
view
|
||
override(ERC1155Upgradeable, AccessControlUpgradeable)
|
||
returns (bool)
|
||
{
|
||
return super.supportsInterface(interfaceId);
|
||
}
|
||
}
|