229 lines
8.7 KiB
Solidity
229 lines
8.7 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 "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
|
||
import "./interfaces/ICoupon.sol";
|
||
import "./interfaces/ICompliance.sol";
|
||
|
||
/// @title CouponBackedSecurity — CBS 资产证券化合约
|
||
/// @notice 券收益流打包为资产支持证券,Securities Track + Broker-Dealer 牌照下运营
|
||
/// @dev 使用 Transparent Proxy 部署
|
||
contract CouponBackedSecurity is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable, ERC721Holder {
|
||
bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE");
|
||
bytes32 public constant SETTLER_ROLE = keccak256("SETTLER_ROLE");
|
||
bytes32 public constant RATING_AGENCY_ROLE = keccak256("RATING_AGENCY_ROLE");
|
||
bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE");
|
||
|
||
ICompliance public compliance;
|
||
ICoupon public couponContract;
|
||
IERC20 public stablecoin;
|
||
|
||
struct Pool {
|
||
uint256[] couponIds; // 底层券资产
|
||
uint256 totalFaceValue; // 底层面值总额
|
||
uint256 totalShares; // 份额总数
|
||
uint256 soldShares; // 已售份额
|
||
uint256 maturityDate; // 到期日
|
||
string creditRating; // 信用评级
|
||
address issuer; // 池创建方
|
||
bool active; // 是否活跃
|
||
uint256 createdAt;
|
||
}
|
||
|
||
mapping(uint256 => Pool) private _pools;
|
||
// 份额持有:poolId → holder → shares
|
||
mapping(uint256 => mapping(address => uint256)) public shares;
|
||
// 收益分配记录:poolId → holder → claimed amount
|
||
mapping(uint256 => mapping(address => uint256)) public claimedYield;
|
||
// 池的总收益
|
||
mapping(uint256 => uint256) public poolYield;
|
||
|
||
uint256 public nextPoolId;
|
||
|
||
// --- Events ---
|
||
event PoolCreated(
|
||
uint256 indexed poolId,
|
||
address indexed issuer,
|
||
uint256 couponCount,
|
||
uint256 totalFaceValue,
|
||
uint256 totalShares
|
||
);
|
||
event SharesPurchased(uint256 indexed poolId, address indexed buyer, uint256 shareCount, uint256 amount);
|
||
event CreditRatingSet(uint256 indexed poolId, string rating);
|
||
event YieldDeposited(uint256 indexed poolId, uint256 totalYield);
|
||
event YieldClaimed(uint256 indexed poolId, address indexed holder, uint256 amount);
|
||
event PoolDeactivated(uint256 indexed poolId);
|
||
|
||
function initialize(
|
||
address _couponContract,
|
||
address _compliance,
|
||
address _stablecoin,
|
||
address admin
|
||
) external initializer {
|
||
__AccessControl_init();
|
||
__ReentrancyGuard_init();
|
||
|
||
couponContract = ICoupon(_couponContract);
|
||
compliance = ICompliance(_compliance);
|
||
stablecoin = IERC20(_stablecoin);
|
||
|
||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||
_grantRole(GOVERNANCE_ROLE, admin);
|
||
}
|
||
|
||
// ========================
|
||
// 池管理
|
||
// ========================
|
||
|
||
/// @notice 创建 CBS 池 — 将一组券的收益流打包
|
||
/// @param couponIds 底层券 token IDs
|
||
/// @param totalShares 份额总数
|
||
/// @param maturityDate 到期日
|
||
function createPool(
|
||
uint256[] calldata couponIds,
|
||
uint256 totalShares,
|
||
uint256 maturityDate
|
||
) external onlyRole(ISSUER_ROLE) nonReentrant returns (uint256 poolId) {
|
||
require(couponIds.length > 0, "CBS: empty pool");
|
||
require(totalShares > 0, "CBS: zero shares");
|
||
require(maturityDate > block.timestamp, "CBS: invalid maturity");
|
||
|
||
// 合规检查:创建者必须 KYC L3 + Broker-Dealer 资质
|
||
compliance.requireKycLevel(msg.sender, 3);
|
||
|
||
uint256 totalFace = 0;
|
||
for (uint256 i = 0; i < couponIds.length; i++) {
|
||
ICoupon.CouponConfig memory config = couponContract.getConfig(couponIds[i]);
|
||
require(
|
||
config.couponType == ICoupon.CouponType.Security,
|
||
"CBS: only Security coupons"
|
||
);
|
||
totalFace += couponContract.getFaceValue(couponIds[i]);
|
||
// 锁定底层券到池中
|
||
couponContract.safeTransferFrom(msg.sender, address(this), couponIds[i]);
|
||
}
|
||
|
||
poolId = nextPoolId++;
|
||
_pools[poolId].couponIds = couponIds;
|
||
_pools[poolId].totalFaceValue = totalFace;
|
||
_pools[poolId].totalShares = totalShares;
|
||
_pools[poolId].soldShares = 0;
|
||
_pools[poolId].maturityDate = maturityDate;
|
||
_pools[poolId].creditRating = "";
|
||
_pools[poolId].issuer = msg.sender;
|
||
_pools[poolId].active = true;
|
||
_pools[poolId].createdAt = block.timestamp;
|
||
|
||
emit PoolCreated(poolId, msg.sender, couponIds.length, totalFace, totalShares);
|
||
}
|
||
|
||
/// @notice 购买 CBS 份额 — 仅合格投资者(KYC L2+)
|
||
function purchaseShares(uint256 poolId, uint256 shareCount) external nonReentrant {
|
||
require(_pools[poolId].active, "CBS: pool not active");
|
||
require(shareCount > 0, "CBS: zero shares");
|
||
require(
|
||
_pools[poolId].soldShares + shareCount <= _pools[poolId].totalShares,
|
||
"CBS: insufficient shares"
|
||
);
|
||
|
||
// 合规检查
|
||
compliance.requireKycLevel(msg.sender, 2);
|
||
|
||
uint256 price = (_pools[poolId].totalFaceValue * shareCount) / _pools[poolId].totalShares;
|
||
require(price > 0, "CBS: price is zero");
|
||
|
||
stablecoin.transferFrom(msg.sender, address(this), price);
|
||
shares[poolId][msg.sender] += shareCount;
|
||
_pools[poolId].soldShares += shareCount;
|
||
|
||
emit SharesPurchased(poolId, msg.sender, shareCount, price);
|
||
}
|
||
|
||
// ========================
|
||
// 信用评级
|
||
// ========================
|
||
|
||
/// @notice 链下评级机构写入信用评级
|
||
function setCreditRating(uint256 poolId, string calldata rating)
|
||
external
|
||
onlyRole(RATING_AGENCY_ROLE)
|
||
{
|
||
require(_pools[poolId].active, "CBS: pool not active");
|
||
_pools[poolId].creditRating = rating;
|
||
emit CreditRatingSet(poolId, rating);
|
||
}
|
||
|
||
// ========================
|
||
// 收益分配
|
||
// ========================
|
||
|
||
/// @notice 存入收益(由 clearing-service 链下计算后调用)
|
||
function depositYield(uint256 poolId, uint256 totalYield) external onlyRole(SETTLER_ROLE) nonReentrant {
|
||
require(_pools[poolId].active, "CBS: pool not active");
|
||
require(totalYield > 0, "CBS: zero yield");
|
||
|
||
stablecoin.transferFrom(msg.sender, address(this), totalYield);
|
||
poolYield[poolId] += totalYield;
|
||
|
||
emit YieldDeposited(poolId, totalYield);
|
||
}
|
||
|
||
/// @notice 持有人领取收益 — 按份额比例
|
||
function claimYield(uint256 poolId) external nonReentrant {
|
||
uint256 holderShares = shares[poolId][msg.sender];
|
||
require(holderShares > 0, "CBS: no shares");
|
||
|
||
uint256 totalEntitled = (poolYield[poolId] * holderShares) / _pools[poolId].totalShares;
|
||
uint256 alreadyClaimed = claimedYield[poolId][msg.sender];
|
||
uint256 claimable = totalEntitled - alreadyClaimed;
|
||
require(claimable > 0, "CBS: nothing to claim");
|
||
|
||
claimedYield[poolId][msg.sender] = totalEntitled;
|
||
stablecoin.transfer(msg.sender, claimable);
|
||
|
||
emit YieldClaimed(poolId, msg.sender, claimable);
|
||
}
|
||
|
||
/// @notice 停用池
|
||
function deactivatePool(uint256 poolId) external onlyRole(GOVERNANCE_ROLE) {
|
||
_pools[poolId].active = false;
|
||
emit PoolDeactivated(poolId);
|
||
}
|
||
|
||
// ========================
|
||
// 查询
|
||
// ========================
|
||
|
||
/// @notice 获取池信息
|
||
function getPool(uint256 poolId) external view returns (
|
||
uint256 totalFaceValue,
|
||
uint256 totalShares,
|
||
uint256 soldShares,
|
||
uint256 maturityDate,
|
||
string memory creditRating,
|
||
address issuer,
|
||
bool active
|
||
) {
|
||
Pool storage p = _pools[poolId];
|
||
return (p.totalFaceValue, p.totalShares, p.soldShares, p.maturityDate, p.creditRating, p.issuer, p.active);
|
||
}
|
||
|
||
/// @notice 获取池中的券 IDs
|
||
function getPoolCouponIds(uint256 poolId) external view returns (uint256[] memory) {
|
||
return _pools[poolId].couponIds;
|
||
}
|
||
|
||
/// @notice 计算持有人可领取的收益
|
||
function getClaimableYield(uint256 poolId, address holder) external view returns (uint256) {
|
||
uint256 holderShares = shares[poolId][holder];
|
||
if (holderShares == 0 || _pools[poolId].totalShares == 0) return 0;
|
||
|
||
uint256 totalEntitled = (poolYield[poolId] * holderShares) / _pools[poolId].totalShares;
|
||
return totalEntitled - claimedYield[poolId][holder];
|
||
}
|
||
}
|