RWA 代币化 —— 认购模块

认购模块是 RWA 平台的核心业务模块,管理投资项目的创建、白名单验证、用户认购等功能。

1. 概述

RWASubscription 实现了完整的代币认购流程:

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────────┐
│ 认购流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 创建项目 ──► 认购期开始 ──► 用户认购 ──► 认购期结束 │
│ │
│ ┌──────────────────────┐ │
│ │ 白名单签名验证 │ │
│ │ KYC 状态检查 │ │
│ │ 额度限制检查 │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘

2. 角色与权限

1
2
3
bytes32 public constant PROJECT_ADMIN_ROLE = keccak256("PROJECT_ADMIN_ROLE");
bytes32 public constant WHITELIST_SIGNER_ROLE = keccak256("WHITELIST_SIGNER_ROLE");
bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE");
角色 权限
DEFAULT_ADMIN_ROLE 管理角色、设置财务参数、升级合约
PROJECT_ADMIN_ROLE 创建项目
WHITELIST_SIGNER_ROLE 签发白名单签名
EMERGENCY_ROLE 紧急暂停项目

3. 数据结构

3.1 认购项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct SubscriptionProject {
address rwaToken; // RWA 代币地址
address paymentToken; // 支付代币地址(如 USDC)
uint256 tokenPrice; // 代币单价(18 位精度)
uint256 totalTokensOffered; // 发售代币总量
uint256 targetAmount; // 目标募集金额
uint256 raisedAmount; // 已募集金额
uint256 startTime; // 认购开始时间
uint256 endTime; // 认购结束时间
uint256 minSubscription; // 最小认购额
uint256 maxSubscription; // 最大认购额
SubscriptionStatus status; // 项目状态
bool requiresKYC; // 是否需要 KYC
bool requiresWhitelist; // 是否需要白名单
uint256 depositBasisPoints; // 保证金比例(基点)
string projectMetadata; // 项目元数据
}

3.2 用户认购记录

1
2
3
4
5
6
7
struct UserSubscription {
address user; // 用户地址
uint256 projectId; // 项目 ID
uint256 subscriptionAmount; // 认购金额
AllocationStatus status; // 分配状态
uint256 timestamp; // 认购时间
}

3.3 状态枚举

1
2
3
4
5
6
7
8
9
10
11
enum SubscriptionStatus {
Active, // 活跃(可认购)
Settled, // 已结算
Cancelled // 已取消
}

enum AllocationStatus {
Pending, // 待处理
Claimed, // 已领取
Cancelled // 已取消
}

4. 项目创建

4.1 createProject

创建新的认购项目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createProject(
address rwaToken, // RWA 代币合约
address paymentToken, // 支付代币合约
uint256 tokenPrice, // 代币单价(18 位精度)
uint256 totalTokensOffered, // 发售代币总量
uint256 startTime, // 开始时间
uint256 endTime, // 结束时间
uint256 minSubscription, // 最小认购额
uint256 maxSubscription, // 最大认购额
bool requiresKYC, // 是否需要 KYC
bool requiresWhitelist, // 是否需要白名单
uint256 depositBasisPoints, // 保证金比例
string calldata metadata // 项目元数据
) external onlyProjectAdmin whenNotPaused returns (uint256 projectId);

参数验证

1
2
3
4
5
6
7
8
require(rwaToken != address(0), "Invalid RWA token");
require(paymentToken != address(0), "Invalid payment token");
require(tokenPrice > 0, "Invalid token price");
require(totalTokensOffered > 0, "Invalid token amount");
require(startTime > block.timestamp, "Invalid start time");
require(endTime > startTime, "Invalid end time");
require(maxSubscription >= minSubscription, "Invalid subscription limits");
require(depositBasisPoints <= 5000, "Deposit rate too high"); // 最高 50%

目标金额计算

1
2
3
4
5
6
7
// 使用辅助库计算目标募集金额
project.targetAmount = PriceCalculationLib.calculatePaymentAmount(
totalTokensOffered,
tokenPrice,
paymentDecimals,
rwaDecimals
);

4.2 价格计算库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
library PriceCalculationLib {
uint256 constant PRICE_PRECISION = 1e18;

/**
* @dev 计算购买指定数量代币需要的支付金额
* @param tokenAmount 代币数量(代币精度)
* @param tokenPrice 代币单价(18 位精度)
* @param paymentDecimals 支付代币精度
* @param rwaDecimals RWA 代币精度
*/
function calculatePaymentAmount(
uint256 tokenAmount,
uint256 tokenPrice,
uint8 paymentDecimals,
uint8 rwaDecimals
) internal pure returns (uint256) {
// payment = tokenAmount * tokenPrice / PRICE_PRECISION
// 调整精度差异
return (tokenAmount * tokenPrice * 10**paymentDecimals)
/ (PRICE_PRECISION * 10**rwaDecimals);
}

/**
* @dev 计算指定支付金额可购买的代币数量
*/
function calculateTokenAmount(
uint256 paymentAmount,
uint256 tokenPrice,
uint8 paymentDecimals,
uint8 rwaDecimals
) internal pure returns (uint256) {
return (paymentAmount * PRICE_PRECISION * 10**rwaDecimals)
/ (tokenPrice * 10**paymentDecimals);
}
}

4.3 创建示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建一个房产代币认购项目
uint256 projectId = subscription.createProject(
buildingToken, // RWA 代币
USDC, // 支付代币
100 * 1e18, // 单价:100 USDC/代币
10000 * 1e18, // 发售:10000 代币
block.timestamp + 1 days, // 明天开始
block.timestamp + 31 days, // 持续 30 天
100 * 1e6, // 最小认购:100 USDC
10000 * 1e6, // 最大认购:10000 USDC
true, // 需要 KYC
true, // 需要白名单
1000, // 保证金:10%
"ipfs://Qm..." // 项目元数据
);

// 目标募集金额 = 10000 * 100 = 1,000,000 USDC

5. 白名单签名验证

项目可以设置 requiresWhitelist = true,要求用户持有有效签名才能认购。

5.1 签名结构(EIP-712)

1
2
bytes32 private constant WHITELIST_TYPEHASH =
keccak256("WhitelistPermit(uint256 projectId,address user,uint256 nonce,uint256 deadline)");

5.2 签名验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function _verifyWhitelistSignature(
uint256 projectId,
address user,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) internal view returns (bool) {
// 检查签名是否过期
require(block.timestamp <= deadline, "Signature expired");
// 检查 nonce 是否正确
require(nonce == _nonces[user], "Invalid nonce");

// 构造 EIP-712 结构化数据哈希
bytes32 structHash = keccak256(
abi.encode(
WHITELIST_TYPEHASH,
projectId,
user,
nonce,
deadline
)
);

bytes32 hash = _hashTypedDataV4(structHash);
address signer = hash.recover(signature);

// 验证签名者是否有 WHITELIST_SIGNER_ROLE
return hasRole(WHITELIST_SIGNER_ROLE, signer);
}

5.3 Nonce 管理

每个用户有独立的 nonce,每次成功认购后递增,防止签名重放。

1
2
3
4
5
6
7
8
9
10
11
12
mapping(address => uint256) private _nonces;

// 获取用户当前 nonce
function nonces(address user) external view returns (uint256) {
return _nonces[user];
}

// 紧急情况下作废用户签名
function invalidateNonce(address user) external onlyWhitelistSigner {
_nonces[user]++;
emit NonceInvalidated(user, _nonces[user]);
}

5.4 链下签名生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 使用 ethers.js 生成白名单签名
async function generateWhitelistSignature(
signer,
contractAddress,
projectId,
userAddress,
nonce,
deadline,
) {
const domain = {
name: "RWASubscription",
version: "1",
chainId: await signer.getChainId(),
verifyingContract: contractAddress,
};

const types = {
WhitelistPermit: [
{ name: "projectId", type: "uint256" },
{ name: "user", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};

const value = {
projectId,
user: userAddress,
nonce,
deadline,
};

return await signer._signTypedData(domain, types, value);
}

6. 用户认购

6.1 带白名单签名认购

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function subscribe(
uint256 projectId,
uint256 amount,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) external projectExists(projectId) whenNotPaused notEmergencyPaused(projectId) nonReentrant {
// 验证前置条件
SubscriptionProject storage project = _validateSubscriptionPrerequisites(projectId, amount);

// 验证白名单签名
if (project.requiresWhitelist) {
require(
_verifyWhitelistSignature(projectId, msg.sender, nonce, deadline, signature),
"Invalid whitelist signature"
);
_nonces[msg.sender]++;
}

// 执行认购
_executeSubscription(projectId, amount, project);
}

6.2 无签名认购

用于不需要白名单的项目。

1
2
3
4
5
6
7
8
9
10
11
function subscribe(
uint256 projectId,
uint256 amount
) external projectExists(projectId) whenNotPaused notEmergencyPaused(projectId) nonReentrant {
SubscriptionProject storage project = _validateSubscriptionPrerequisites(projectId, amount);

// 验证项目不需要白名单
require(!project.requiresWhitelist, "Project requires whitelist signature");

_executeSubscription(projectId, amount, project);
}

6.3 前置条件验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function _validateSubscriptionPrerequisites(
uint256 projectId,
uint256 amount
) internal view returns (SubscriptionProject storage) {
SubscriptionProject storage project = _projects[projectId];

// 项目状态检查
require(project.status == SubscriptionStatus.Active, "Project not active");

// 时间窗口检查
require(block.timestamp >= project.startTime, "Subscription not started yet");
require(block.timestamp <= project.endTime, "Subscription period ended");

// KYC 检查
if (project.requiresKYC) {
require(identityRegistry.isVerified(msg.sender), "KYC not verified");
}

// 额度检查
UserSubscription storage userSub = _userSubscriptions[projectId][msg.sender];
uint256 currentTotal = userSub.subscriptionAmount;

require(currentTotal + amount >= project.minSubscription, "Below minimum subscription");
require(currentTotal + amount <= project.maxSubscription, "Exceeds max subscription limit");

return project;
}

6.4 执行认购

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function _executeSubscription(
uint256 projectId,
uint256 amount,
SubscriptionProject storage project
) internal {
// 转移支付代币到合约
IERC20(project.paymentToken).safeTransferFrom(msg.sender, address(this), amount);

UserSubscription storage userSub = _userSubscriptions[projectId][msg.sender];

bool isFirstSubscription = (userSub.subscriptionAmount == 0);

if (isFirstSubscription) {
// 首次认购
userSub.user = msg.sender;
userSub.projectId = projectId;
userSub.subscriptionAmount = amount;
userSub.status = AllocationStatus.Pending;
userSub.timestamp = block.timestamp;

// 添加到索引
_projectSubscribers[projectId].push(msg.sender);
_userProjects[msg.sender].push(projectId);
} else {
// 追加认购
userSub.subscriptionAmount += amount;
userSub.timestamp = block.timestamp;
}

// 更新项目募集金额
project.raisedAmount += amount;

emit SubscriptionMade(projectId, msg.sender, amount);
}

7. 取消认购

在认购期内,用户可以取消认购并获得全额退款。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function cancelSubscription(
uint256 projectId
) external projectExists(projectId) nonReentrant {
SubscriptionProject storage project = _projects[projectId];
UserSubscription storage userSub = _userSubscriptions[projectId][msg.sender];

require(userSub.subscriptionAmount > 0, "No subscription found");
require(userSub.status == AllocationStatus.Pending, "Already processed");
require(project.status == SubscriptionStatus.Active, "Cannot cancel now");

uint256 refundAmount = userSub.subscriptionAmount;

// 更新项目统计
project.raisedAmount -= refundAmount;

// 清除认购记录
userSub.subscriptionAmount = 0;
userSub.status = AllocationStatus.Cancelled;

// 退还支付代币
IERC20(project.paymentToken).safeTransfer(msg.sender, refundAmount);

emit SubscriptionCancelled(projectId, msg.sender, refundAmount);
}

8. 查询函数

8.1 项目查询

1
2
3
4
5
6
7
8
9
10
11
// 获取项目详情
function getProject(uint256 projectId) external view returns (SubscriptionProject memory);

// 获取项目订阅者列表
function getProjectSubscribers(uint256 projectId) external view returns (address[] memory);

// 获取活跃项目列表
function getActiveProjects() external view returns (uint256[] memory);

// 获取认购窗口
function getSubscriptionWindow(uint256 projectId) public view returns (uint256 startTime, uint256 endTime);

8.2 用户查询

1
2
3
4
5
6
7
8
// 获取用户认购记录
function getUserSubscription(uint256 projectId, address user) external view returns (UserSubscription memory);

// 获取用户参与的项目
function getUserProjects(address user) external view returns (uint256[] memory);

// 获取用户剩余额度
function getRemainingQuota(uint256 projectId, address user) external view returns (uint256);

8.3 资格检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function canSubscribe(uint256 projectId, address user) external view returns (bool canSub, string memory reason) {
// 项目验证
if (projectId == 0 || projectId >= _nextProjectId) {
return (false, "Invalid project");
}

SubscriptionProject memory project = _projects[projectId];

// 状态检查
if (project.status != SubscriptionStatus.Active) {
return (false, "Project not active");
}

// KYC 检查
if (project.requiresKYC && !identityRegistry.isVerified(user)) {
return (false, "KYC not verified");
}

// 额度检查
uint256 currentTotal = _userSubscriptions[projectId][user].subscriptionAmount;
if (currentTotal >= project.maxSubscription) {
return (false, "Max subscription limit reached");
}

// 时间窗口检查
(uint256 windowStart, uint256 windowEnd) = getSubscriptionWindow(projectId);
if (block.timestamp < windowStart) {
return (false, "Subscription window not started");
}
if (block.timestamp > windowEnd) {
return (false, "Subscription window ended");
}

// 白名单提示
if (project.requiresWhitelist) {
return (true, "Can subscribe (signature required)");
}

return (true, "Can subscribe");
}

8.4 项目统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getProjectStatistics(uint256 projectId) external view returns (
uint256 totalSubscribers, // 总订阅人数
uint256 totalRaised, // 总募集金额
uint256 averageSubscription, // 平均认购额
uint256 oversubscriptionRate // 超额认购率(百分比)
) {
SubscriptionProject memory project = _projects[projectId];
address[] memory subscribers = _projectSubscribers[projectId];

totalSubscribers = subscribers.length;
totalRaised = project.raisedAmount;
averageSubscription = totalSubscribers > 0 ? totalRaised / totalSubscribers : 0;
oversubscriptionRate = project.targetAmount > 0 ? (totalRaised * 100) / project.targetAmount : 0;
}

9. 紧急控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 紧急暂停单个项目
function emergencyPause(uint256 projectId) external onlyRole(EMERGENCY_ROLE) projectExists(projectId) {
_projectEmergencyPaused[projectId] = true;
}

// 恢复项目
function emergencyUnpause(uint256 projectId) external onlyRole(EMERGENCY_ROLE) projectExists(projectId) {
_projectEmergencyPaused[projectId] = false;
}

// 全局暂停
function pause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_pause();
}

function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}

10. 事件

1
2
3
4
event ProjectCreated(uint256 indexed projectId, address indexed rwaToken, uint256 tokenPrice, uint256 totalTokensOffered);
event SubscriptionMade(uint256 indexed projectId, address indexed user, uint256 amount);
event SubscriptionCancelled(uint256 indexed projectId, address indexed user, uint256 amount);
event NonceInvalidated(address indexed user, uint256 newNonce);

11. 安全考虑

11.1 签名安全

  • EIP-712 结构化签名防止跨合约/跨链重放
  • Nonce 机制防止同一签名重复使用
  • Deadline 限制签名有效期

11.2 金额安全

  • 使用 SafeERC20 处理代币转账
  • 先检查后转账,避免重入
  • 精度计算使用专门的库函数

11.3 状态一致性

  • 所有状态更新在单个交易中完成
  • 使用 storage 指针避免数据不一致
  • ReentrancyGuard 防止重入攻击