183 lines
6.6 KiB
Solidity
183 lines
6.6 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 "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||
import "./interfaces/ICoupon.sol";
|
||
import "./interfaces/ICompliance.sol";
|
||
|
||
/// @title Settlement — 交易结算合约
|
||
/// @notice 原子交换:券 ↔ 稳定币 同时转移,支持多稳定币
|
||
/// @dev 使用 Transparent Proxy 部署
|
||
contract Settlement is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
|
||
bytes32 public constant SETTLER_ROLE = keccak256("SETTLER_ROLE");
|
||
bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE");
|
||
|
||
ICoupon public couponContract;
|
||
ICompliance public compliance;
|
||
|
||
// 多稳定币支持
|
||
mapping(address => bool) public supportedStablecoins;
|
||
address public defaultStablecoin; // USDC
|
||
|
||
// --- Events ---
|
||
event TradeSettled(
|
||
uint256 indexed tokenId,
|
||
address indexed buyer,
|
||
address indexed seller,
|
||
uint256 price,
|
||
address stablecoin
|
||
);
|
||
event RefundExecuted(
|
||
uint256 indexed tokenId,
|
||
address indexed buyer,
|
||
address indexed seller,
|
||
uint256 refundAmount,
|
||
address stablecoin
|
||
);
|
||
event StablecoinAdded(address indexed token);
|
||
event StablecoinRemoved(address indexed token);
|
||
|
||
function initialize(
|
||
address _couponContract,
|
||
address _compliance,
|
||
address _defaultStablecoin,
|
||
address admin
|
||
) external initializer {
|
||
__AccessControl_init();
|
||
__ReentrancyGuard_init();
|
||
|
||
couponContract = ICoupon(_couponContract);
|
||
compliance = ICompliance(_compliance);
|
||
defaultStablecoin = _defaultStablecoin;
|
||
supportedStablecoins[_defaultStablecoin] = true;
|
||
|
||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||
_grantRole(SETTLER_ROLE, admin);
|
||
_grantRole(GOVERNANCE_ROLE, admin);
|
||
}
|
||
|
||
/// @notice 原子交换:券 ↔ 稳定币 同时转移
|
||
/// @param tokenId 券 token ID
|
||
/// @param buyer 买方地址
|
||
/// @param seller 卖方地址
|
||
/// @param price 成交价格(USDC 精度)
|
||
/// @param stablecoin 使用的稳定币地址
|
||
function executeSwap(
|
||
uint256 tokenId,
|
||
address buyer,
|
||
address seller,
|
||
uint256 price,
|
||
address stablecoin
|
||
) external onlyRole(SETTLER_ROLE) nonReentrant {
|
||
require(supportedStablecoins[stablecoin], "Settlement: unsupported stablecoin");
|
||
require(buyer != address(0) && seller != address(0), "Settlement: zero address");
|
||
require(price > 0, "Settlement: zero price");
|
||
|
||
// 合规检查
|
||
ICoupon.CouponConfig memory config = couponContract.getConfig(tokenId);
|
||
compliance.preTradeCheck(buyer, seller, price, config.couponType);
|
||
|
||
// Utility Track: 价格 ≤ 面值上限
|
||
if (config.couponType == ICoupon.CouponType.Utility) {
|
||
require(price <= config.maxPrice, "Utility: price exceeds max price");
|
||
}
|
||
|
||
// 转售次数检查
|
||
require(
|
||
couponContract.getResaleCount(tokenId) < config.maxResaleCount,
|
||
"Settlement: max resale count exceeded"
|
||
);
|
||
|
||
// 到期检查
|
||
require(block.timestamp <= config.expiryDate, "Settlement: coupon expired");
|
||
|
||
// 原子交换(要么全部成功,要么全部回滚)
|
||
IERC20(stablecoin).transferFrom(buyer, seller, price);
|
||
couponContract.safeTransferFrom(seller, buyer, tokenId);
|
||
couponContract.incrementResaleCount(tokenId);
|
||
|
||
emit TradeSettled(tokenId, buyer, seller, price, stablecoin);
|
||
}
|
||
|
||
/// @notice 使用默认稳定币的原子交换(便捷方法)
|
||
function executeSwap(
|
||
uint256 tokenId,
|
||
address buyer,
|
||
address seller,
|
||
uint256 price
|
||
) external onlyRole(SETTLER_ROLE) nonReentrant {
|
||
_executeSwapInternal(tokenId, buyer, seller, price, defaultStablecoin);
|
||
}
|
||
|
||
/// @notice 退款:反向原子交换
|
||
function executeRefund(
|
||
uint256 tokenId,
|
||
address buyer,
|
||
address seller,
|
||
uint256 refundAmount,
|
||
address stablecoin
|
||
) external onlyRole(SETTLER_ROLE) nonReentrant {
|
||
require(supportedStablecoins[stablecoin], "Settlement: unsupported stablecoin");
|
||
require(refundAmount > 0, "Settlement: zero refund");
|
||
|
||
// 合规检查
|
||
require(!compliance.isBlacklisted(buyer), "Settlement: buyer blacklisted");
|
||
require(!compliance.isBlacklisted(seller), "Settlement: seller blacklisted");
|
||
|
||
// 反向原子交换
|
||
couponContract.safeTransferFrom(buyer, seller, tokenId);
|
||
IERC20(stablecoin).transferFrom(seller, buyer, refundAmount);
|
||
|
||
emit RefundExecuted(tokenId, buyer, seller, refundAmount, stablecoin);
|
||
}
|
||
|
||
/// @notice 添加支持的稳定币
|
||
function addStablecoin(address token) external onlyRole(GOVERNANCE_ROLE) {
|
||
require(token != address(0), "Settlement: zero address");
|
||
supportedStablecoins[token] = true;
|
||
emit StablecoinAdded(token);
|
||
}
|
||
|
||
/// @notice 移除稳定币支持
|
||
function removeStablecoin(address token) external onlyRole(GOVERNANCE_ROLE) {
|
||
require(token != defaultStablecoin, "Settlement: cannot remove default");
|
||
supportedStablecoins[token] = false;
|
||
emit StablecoinRemoved(token);
|
||
}
|
||
|
||
// --- Internal ---
|
||
|
||
function _executeSwapInternal(
|
||
uint256 tokenId,
|
||
address buyer,
|
||
address seller,
|
||
uint256 price,
|
||
address stablecoin
|
||
) internal {
|
||
require(buyer != address(0) && seller != address(0), "Settlement: zero address");
|
||
require(price > 0, "Settlement: zero price");
|
||
|
||
ICoupon.CouponConfig memory config = couponContract.getConfig(tokenId);
|
||
compliance.preTradeCheck(buyer, seller, price, config.couponType);
|
||
|
||
if (config.couponType == ICoupon.CouponType.Utility) {
|
||
require(price <= config.maxPrice, "Utility: price exceeds max price");
|
||
}
|
||
|
||
require(
|
||
couponContract.getResaleCount(tokenId) < config.maxResaleCount,
|
||
"Settlement: max resale count exceeded"
|
||
);
|
||
require(block.timestamp <= config.expiryDate, "Settlement: coupon expired");
|
||
|
||
IERC20(stablecoin).transferFrom(buyer, seller, price);
|
||
couponContract.safeTransferFrom(seller, buyer, tokenId);
|
||
couponContract.incrementResaleCount(tokenId);
|
||
|
||
emit TradeSettled(tokenId, buyer, seller, price, stablecoin);
|
||
}
|
||
}
|