107 lines
3.5 KiB
Solidity
107 lines
3.5 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/ICompliance.sol";
|
||
|
||
/// @title Redemption — 兑付合约
|
||
/// @notice 消费者直接与发行方结算,平台不介入;兑付时销毁券
|
||
/// @dev 使用 Transparent Proxy 部署
|
||
contract Redemption is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
|
||
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
|
||
|
||
ICoupon public couponContract;
|
||
ICompliance public compliance;
|
||
|
||
// 兑付记录
|
||
struct RedemptionRecord {
|
||
uint256 tokenId;
|
||
address consumer;
|
||
address issuer;
|
||
bytes32 storeId;
|
||
uint256 redeemedAt;
|
||
}
|
||
|
||
uint256 public totalRedemptions;
|
||
mapping(uint256 => RedemptionRecord) public redemptions; // redemptionId → record
|
||
|
||
// --- Events ---
|
||
event CouponRedeemed(
|
||
uint256 indexed tokenId,
|
||
address indexed consumer,
|
||
address indexed issuer,
|
||
bytes32 storeId,
|
||
uint256 redemptionId
|
||
);
|
||
|
||
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);
|
||
}
|
||
|
||
/// @notice 消费者兑付券 — 销毁 NFT,触发事件通知发行方
|
||
/// @param tokenId 券 token ID
|
||
/// @param storeId 门店 ID(bytes32 哈希)
|
||
function redeem(uint256 tokenId, bytes32 storeId) external nonReentrant {
|
||
// 所有权验证
|
||
require(couponContract.ownerOf(tokenId) == msg.sender, "Redemption: not owner");
|
||
|
||
// 合规检查
|
||
require(!compliance.isBlacklisted(msg.sender), "Redemption: blacklisted");
|
||
|
||
ICoupon.CouponConfig memory config = couponContract.getConfig(tokenId);
|
||
|
||
// 到期验证
|
||
require(block.timestamp <= config.expiryDate, "Redemption: coupon expired");
|
||
|
||
// 门店限定验证
|
||
if (config.allowedStores.length > 0) {
|
||
require(_isAllowedStore(storeId, config.allowedStores), "Redemption: store not allowed");
|
||
}
|
||
|
||
// 销毁券(burn)
|
||
couponContract.burn(tokenId);
|
||
|
||
// 记录兑付
|
||
uint256 redemptionId = totalRedemptions++;
|
||
redemptions[redemptionId] = RedemptionRecord({
|
||
tokenId: tokenId,
|
||
consumer: msg.sender,
|
||
issuer: config.issuer,
|
||
storeId: storeId,
|
||
redeemedAt: block.timestamp
|
||
});
|
||
|
||
// 通知发行方(event),平台不介入消费数据
|
||
emit CouponRedeemed(tokenId, msg.sender, config.issuer, storeId, redemptionId);
|
||
}
|
||
|
||
/// @notice 查询兑付记录
|
||
function getRedemption(uint256 redemptionId) external view returns (RedemptionRecord memory) {
|
||
return redemptions[redemptionId];
|
||
}
|
||
|
||
// --- Internal ---
|
||
|
||
/// @dev 检查门店是否在允许列表中
|
||
function _isAllowedStore(bytes32 storeId, bytes32[] memory allowedStores) internal pure returns (bool) {
|
||
for (uint256 i = 0; i < allowedStores.length; i++) {
|
||
if (allowedStores[i] == storeId) return true;
|
||
}
|
||
return false;
|
||
}
|
||
}
|