// 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); } }