gcx/blockchain/genex-contracts/src/Coupon.sol

140 lines
5.0 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "./interfaces/ICoupon.sol";
/// @title Coupon — 券 NFT 合约
/// @notice ERC-721 实现,支持不可转让限制、转售计数、批量操作
/// @dev 使用 Transparent Proxy 部署,券类型铸造后不可修改(安全红线)
contract Coupon is Initializable, ERC721Upgradeable, AccessControlUpgradeable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant SETTLER_ROLE = keccak256("SETTLER_ROLE");
bytes32 public constant FACTORY_ROLE = keccak256("FACTORY_ROLE");
uint256 private _nextTokenId;
// tokenId → 券配置(铸造后 couponType 不可修改 — 安全红线)
mapping(uint256 => ICoupon.CouponConfig) private _configs;
// tokenId → 面值
mapping(uint256 => uint256) private _faceValues;
// tokenId → 转售次数(安全红线:不可被升级绕过)
mapping(uint256 => uint256) private _resaleCount;
// --- Events ---
event CouponMinted(uint256 indexed tokenId, address indexed issuer, uint256 faceValue, ICoupon.CouponType couponType);
event CouponBurned(uint256 indexed tokenId, address indexed burner);
event ResaleCountIncremented(uint256 indexed tokenId, uint256 newCount);
function initialize(string memory name, string memory symbol, address admin) external initializer {
__ERC721_init(name, symbol);
__AccessControl_init();
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(MINTER_ROLE, admin);
_nextTokenId = 1;
}
/// @notice 铸造单个券 NFT
function mint(
address to,
uint256 faceValue,
ICoupon.CouponConfig calldata config
) external onlyRole(FACTORY_ROLE) returns (uint256 tokenId) {
tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_configs[tokenId] = config;
_faceValues[tokenId] = faceValue;
_resaleCount[tokenId] = 0;
emit CouponMinted(tokenId, config.issuer, faceValue, config.couponType);
}
/// @notice 销毁券(兑付时调用)
function burn(uint256 tokenId) external {
require(
_isApprovedOrOwner(msg.sender, tokenId) || hasRole(SETTLER_ROLE, msg.sender),
"Coupon: not authorized to burn"
);
address owner = ownerOf(tokenId);
_burn(tokenId);
emit CouponBurned(tokenId, owner);
}
/// @notice 获取券配置
function getConfig(uint256 tokenId) external view returns (ICoupon.CouponConfig memory) {
require(_exists(tokenId), "Coupon: nonexistent token");
return _configs[tokenId];
}
/// @notice 获取面值
function getFaceValue(uint256 tokenId) external view returns (uint256) {
require(_exists(tokenId), "Coupon: nonexistent token");
return _faceValues[tokenId];
}
/// @notice 获取转售次数
function getResaleCount(uint256 tokenId) external view returns (uint256) {
return _resaleCount[tokenId];
}
/// @notice 增加转售次数(仅 Settlement 合约可调用)
function incrementResaleCount(uint256 tokenId) external onlyRole(SETTLER_ROLE) {
_resaleCount[tokenId]++;
emit ResaleCountIncremented(tokenId, _resaleCount[tokenId]);
}
/// @notice 批量转移
function batchTransfer(
address from,
address to,
uint256[] calldata tokenIds
) external {
for (uint256 i = 0; i < tokenIds.length; i++) {
safeTransferFrom(from, to, tokenIds[i]);
}
}
/// @notice 重写 transfer 钩子 — 不可转让券直接 revert
/// @dev 铸造from=0和销毁to=0不受限
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId,
uint256 batchSize
) internal virtual override {
super._beforeTokenTransfer(from, to, tokenId, batchSize);
// 铸造和销毁不受限
if (from == address(0) || to == address(0)) return;
ICoupon.CouponConfig memory config = _configs[tokenId];
// 不可转让券revert
require(config.transferable, "Coupon: non-transferable");
// 转售次数检查(安全红线)
require(
_resaleCount[tokenId] < config.maxResaleCount,
"Coupon: max resale count exceeded"
);
}
/// @dev 内部辅助检查授权override OZ v4
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view override returns (bool) {
return super._isApprovedOrOwner(spender, tokenId);
}
// --- ERC165 ---
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721Upgradeable, AccessControlUpgradeable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}