124 lines
3.9 KiB
Solidity
124 lines
3.9 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/ICouponBatch.sol";
|
||
import "./interfaces/ICompliance.sol";
|
||
|
||
/// @title Redemption1155 — ERC-1155 券兑付合约
|
||
/// @notice 支持多单位一次性兑付(burn),平台不介入资金
|
||
/// @dev Transparent Proxy 部署。消费者持券到店兑付,合约负责销毁。
|
||
contract Redemption1155 is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
|
||
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
|
||
|
||
ICouponBatch public couponBatchContract;
|
||
ICompliance public compliance;
|
||
|
||
struct RedemptionRecord {
|
||
uint256 typeId;
|
||
uint256 amount; // 本次兑付数量
|
||
address consumer;
|
||
address issuer;
|
||
bytes32 storeId;
|
||
uint256 redeemedAt;
|
||
}
|
||
|
||
uint256 public totalRedemptions;
|
||
mapping(uint256 => RedemptionRecord) public redemptions;
|
||
|
||
// --- Events ---
|
||
event BatchCouponRedeemed(
|
||
uint256 indexed typeId,
|
||
uint256 amount,
|
||
address indexed consumer,
|
||
address indexed issuer,
|
||
bytes32 storeId,
|
||
uint256 redemptionId
|
||
);
|
||
|
||
function initialize(
|
||
address _couponBatchContract,
|
||
address _compliance,
|
||
address admin
|
||
) external initializer {
|
||
__AccessControl_init();
|
||
__ReentrancyGuard_init();
|
||
|
||
couponBatchContract = ICouponBatch(_couponBatchContract);
|
||
compliance = ICompliance(_compliance);
|
||
|
||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||
_grantRole(ADMIN_ROLE, admin);
|
||
}
|
||
|
||
/// @notice 兑付 `amount` 单位的 typeId 券
|
||
/// @param typeId ERC-1155 token type ID
|
||
/// @param amount 兑付数量
|
||
/// @param storeId 门店 ID 哈希
|
||
function redeem(
|
||
uint256 typeId,
|
||
uint256 amount,
|
||
bytes32 storeId
|
||
) external nonReentrant {
|
||
require(amount > 0, "Redemption1155: zero amount");
|
||
|
||
// 余额检查(隐含所有权验证)
|
||
require(
|
||
couponBatchContract.balanceOf(msg.sender, typeId) >= amount,
|
||
"Redemption1155: insufficient balance"
|
||
);
|
||
|
||
// 合规检查
|
||
require(!compliance.isBlacklisted(msg.sender), "Redemption1155: blacklisted");
|
||
|
||
ICouponBatch.BatchCouponConfig memory config = couponBatchContract.getConfig(typeId);
|
||
|
||
// 到期检查
|
||
require(block.timestamp <= config.expiryDate, "Redemption1155: coupon expired");
|
||
|
||
// 门店限制检查
|
||
if (config.allowedStores.length > 0) {
|
||
require(
|
||
_isAllowedStore(storeId, config.allowedStores),
|
||
"Redemption1155: store not allowed"
|
||
);
|
||
}
|
||
|
||
// 销毁券
|
||
couponBatchContract.burn(msg.sender, typeId, amount);
|
||
|
||
// 记录兑付
|
||
uint256 redemptionId = totalRedemptions++;
|
||
redemptions[redemptionId] = RedemptionRecord({
|
||
typeId: typeId,
|
||
amount: amount,
|
||
consumer: msg.sender,
|
||
issuer: config.issuer,
|
||
storeId: storeId,
|
||
redeemedAt: block.timestamp
|
||
});
|
||
|
||
emit BatchCouponRedeemed(
|
||
typeId, amount, msg.sender, config.issuer, storeId, redemptionId
|
||
);
|
||
}
|
||
|
||
/// @notice 查询兑付记录
|
||
function getRedemption(uint256 redemptionId) external view returns (RedemptionRecord memory) {
|
||
return redemptions[redemptionId];
|
||
}
|
||
|
||
/// @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;
|
||
}
|
||
}
|