RWA 代币化 —— 结算与领取

本文介绍 RWA 认购的后半程流程:项目结算、超额认购处理、代币领取与退款。

1. 概述

认购期结束后,进入结算阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────┐
│ 结算流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 认购期结束 ──► 项目方结算 ──► 用户领取 │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 转入 RWA 代币 │ │
│ │ 扣除保证金到财务 │ │
│ │ 剩余资金到项目方 │ │
│ └──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 计算用户分配 │ │
│ │ 发放 RWA 代币 │ │
│ │ 退还超额部分 │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘

2. 超额认购处理

2.1 什么是超额认购

当募集金额超过目标金额时,即发生超额认购:

1
2
3
4
项目目标:募集 1,000,000 USDC,发售 10,000 代币
实际募集:1,500,000 USDC

超额认购率 = 1,500,000 / 1,000,000 = 150%

2.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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function _calculateUserAllocation(
uint256 projectId,
address user
) private view returns (uint256 allocatedTokens, uint256 refundAmount) {
SubscriptionProject memory project = _projects[projectId];
UserSubscription memory userSub = _userSubscriptions[projectId][user];

if (userSub.subscriptionAmount == 0) {
return (0, 0);
}

// 获取代币精度
uint8 rwaDecimals = IERC20Metadata(project.rwaToken).decimals();
uint8 paymentDecimals = IERC20Metadata(project.paymentToken).decimals();

// 计算用户请求的代币数量
uint256 requestedTokens = PriceCalculationLib.calculateTokenAmount(
userSub.subscriptionAmount,
project.tokenPrice,
paymentDecimals,
rwaDecimals
);

if (project.raisedAmount <= project.targetAmount) {
// 未超额:用户获得全部请求的代币
allocatedTokens = requestedTokens;
refundAmount = 0;
} else {
// 超额认购:按比例分配

// 1. 计算所有用户请求的总代币数
uint256 totalRequestedTokens = PriceCalculationLib.calculateTokenAmount(
project.raisedAmount,
project.tokenPrice,
paymentDecimals,
rwaDecimals
);

// 2. 用户按比例获得代币
allocatedTokens = (requestedTokens * project.totalTokensOffered) / totalRequestedTokens;

// 3. 计算实际使用的支付金额
uint256 usedAmount = PriceCalculationLib.calculatePaymentAmount(
allocatedTokens,
project.tokenPrice,
paymentDecimals,
rwaDecimals
);

// 4. 计算退款金额
refundAmount = userSub.subscriptionAmount > usedAmount
? userSub.subscriptionAmount - usedAmount
: 0;
}
}

2.3 分配示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
项目参数:
├── 目标募集:1,000,000 USDC
├── 发售代币:10,000 RWA
└── 单价:100 USDC/代币

认购情况:
├── Alice 认购:100,000 USDC(请求 1,000 代币)
├── Bob 认购:200,000 USDC(请求 2,000 代币)
└── 其他用户:1,200,000 USDC(请求 12,000 代币)

总计:1,500,000 USDC(请求 15,000 代币)
超额认购率:150%

分配结果:
├── Alice:
│ ├── 分配代币 = 1,000 * 10,000 / 15,000 = 666.67 代币
│ ├── 使用金额 = 666.67 * 100 = 66,667 USDC
│ └── 退款 = 100,000 - 66,667 = 33,333 USDC

├── Bob:
│ ├── 分配代币 = 2,000 * 10,000 / 15,000 = 1,333.33 代币
│ ├── 使用金额 = 133,333 USDC
│ └── 退款 = 66,667 USDC

3. 项目结算

3.1 settleProject

项目方调用结算函数,转入代币并收取募集资金。

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
function settleProject(
uint256 projectId
) external projectExists(projectId) {
SubscriptionProject storage project = _projects[projectId];

// 验证条件
require(project.status == SubscriptionStatus.Active, "Project not active");
require(block.timestamp > project.endTime, "Project not ended yet");

// 1. 项目方转入全部 RWA 代币
IERC20(project.rwaToken).safeTransferFrom(
msg.sender,
address(this),
project.totalTokensOffered
);

// 2. 计算实际有效募集金额(超额部分会退还)
uint256 realAmount = project.raisedAmount > project.targetAmount
? project.targetAmount
: project.raisedAmount;

// 3. 计算保证金(按项目设定的比例)
uint256 depositAmount = (realAmount * project.depositBasisPoints) / 10000;

// 4. 保证金转入财务账户
if (depositAmount > 0) {
IERC20(project.paymentToken).safeTransfer(treasury, depositAmount);
}

// 5. 剩余资金转给项目方
uint256 projectProceeds = realAmount - depositAmount;
IERC20(project.paymentToken).safeTransfer(msg.sender, projectProceeds);

// 6. 更新项目状态
project.status = SubscriptionStatus.Settled;

emit ProjectStatusChanged(projectId, SubscriptionStatus.Active, SubscriptionStatus.Settled);
}

3.2 结算流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────────┐
│ settleProject │
├─────────────────────────────────────────────────────────┤
│ │
│ 项目方 ────► 转入 10,000 RWA 代币 │
│ │
│ 合约持有 1,500,000 USDC │
│ │ │
│ ├──► 实际有效:1,000,000 USDC │
│ │ (超额的 500,000 待退还用户) │
│ │ │
│ ├──► 保证金(10%):100,000 USDC → 财务 │
│ │ │
│ └──► 项目收入:900,000 USDC → 项目方 │
│ │
└─────────────────────────────────────────────────────────┘

3.3 保证金说明

保证金(Deposit)用于:

  • 约束项目方履约
  • 可用于后续分红或赔偿
  • 比例在创建项目时设定(最高 50%)
1
2
3
4
保证金比例(基点):
1000 = 10%
2000 = 20%
5000 = 50%(最大)

4. 用户领取

4.1 claim

用户在项目结算后领取代币和退款。

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
function claim(
uint256 projectId
) external projectExists(projectId) nonReentrant {
SubscriptionProject storage project = _projects[projectId];
UserSubscription storage userSub = _userSubscriptions[projectId][msg.sender];

// 验证条件
require(project.status == SubscriptionStatus.Settled, "Project not settled");
require(userSub.status == AllocationStatus.Pending, "Already processed");
require(userSub.subscriptionAmount > 0, "No subscription");

// 动态计算用户分配
(uint256 allocatedTokens, uint256 refundAmount) = _calculateUserAllocation(projectId, msg.sender);
require(allocatedTokens > 0 || refundAmount > 0, "Nothing to claim");

// 更新状态(防止重复领取)
userSub.status = AllocationStatus.Claimed;

// 发放 RWA 代币
if (allocatedTokens > 0) {
IERC20(project.rwaToken).safeTransfer(msg.sender, allocatedTokens);
emit TokensClaimed(projectId, msg.sender, allocatedTokens);
}

// 退还超额支付
if (refundAmount > 0) {
IERC20(project.paymentToken).safeTransfer(msg.sender, refundAmount);
emit RefundProcessed(projectId, msg.sender, refundAmount);
}
}

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
用户调用 claim(projectId)


┌──────────────────┐
│ 验证项目已结算 │
│ 验证用户未领取 │
└──────────────────┘


┌──────────────────┐
│ 计算分配数量 │
│ 计算退款金额 │
└──────────────────┘


┌──────────────────┐
│ 更新状态为已领取 │
└──────────────────┘

├────────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 转 RWA 代币给用户 │ │ 转退款给用户 │
└──────────────────┘ └──────────────────┘

4.3 查询可领取金额

1
2
3
4
5
6
function getClaimableAmounts(
uint256 projectId,
address user
) external view returns (uint256 allocatedTokens, uint256 refundAmount) {
(allocatedTokens, refundAmount) = _calculateUserAllocation(projectId, user);
}

5. 完整流程示例

5.1 时间线

1
2
3
4
5
Day 0:  项目创建
Day 1: 认购开始
Day 30: 认购结束
Day 31: 项目方结算
Day 31+: 用户领取

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
// ===== 认购期结束后 =====

// 1. 项目方准备结算
// 首先 approve RWA 代币给 subscription 合约
rwaToken.approve(address(subscription), totalTokensOffered);

// 2. 执行结算
subscription.settleProject(projectId);
// → RWA 代币转入合约
// → 保证金转入财务
// → 剩余资金转给项目方

// ===== 用户领取 =====

// 3. Alice 查询可领取金额
(uint256 tokens, uint256 refund) = subscription.getClaimableAmounts(projectId, alice);
// tokens = 666.67 RWA
// refund = 33,333 USDC

// 4. Alice 领取
subscription.claim(projectId);
// → 收到 666.67 RWA 代币
// → 收到 33,333 USDC 退款

6. 边界情况处理

6.1 未满额认购

募集金额低于目标时:

  • 用户获得全部请求的代币
  • 无退款
  • 项目方收到实际募集金额(扣除保证金)
1
2
3
4
5
6
7
目标:1,000,000 USDC
实际:800,000 USDC

用户认购 100,000 USDC:
├── 分配代币 = 1,000(全额)
├── 退款 = 0
└── 项目方收入 = 800,000 * 90% = 720,000 USDC

6.2 刚好满额

1
2
3
4
目标:1,000,000 USDC
实际:1,000,000 USDC

所有用户获得全额分配,无退款

6.3 极端超额

1
2
3
4
5
6
7
目标:1,000,000 USDC
实际:10,000,000 USDC(10 倍超额)

用户认购 100,000 USDC:
├── 分配代币 = 1,000 * (1,000,000 / 10,000,000) = 100 代币
├── 使用金额 = 10,000 USDC
└── 退款 = 90,000 USDC(90% 退还)

7. 查询函数

7.1 分配计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 计算指定金额可购买的代币数量
function calculateUserAllocation(
uint256 projectId,
address user,
uint256 subscriptionAmount
) external view returns (uint256 allocation) {
SubscriptionProject memory project = _projects[projectId];

uint8 rwaDecimals = IERC20Metadata(project.rwaToken).decimals();
uint8 paymentDecimals = IERC20Metadata(project.paymentToken).decimals();

allocation = PriceCalculationLib.calculateTokenAmount(
subscriptionAmount,
project.tokenPrice,
paymentDecimals,
rwaDecimals
);
}

7.2 状态查询

1
2
3
4
5
// 获取用户认购详情
function getUserSubscription(uint256 projectId, address user) external view returns (UserSubscription memory);

// 获取项目详情
function getProject(uint256 projectId) external view returns (SubscriptionProject memory);

8. 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 项目状态变更
event ProjectStatusChanged(
uint256 indexed projectId,
SubscriptionStatus oldStatus,
SubscriptionStatus newStatus
);

// 代币领取
event TokensClaimed(
uint256 indexed projectId,
address indexed user,
uint256 amount
);

// 退款处理
event RefundProcessed(
uint256 indexed projectId,
address indexed user,
uint256 amount
);

9. 安全考虑

9.1 重复领取防护

1
2
3
4
5
// 状态检查确保只能领取一次
require(userSub.status == AllocationStatus.Pending, "Already processed");

// 状态更新在转账前
userSub.status = AllocationStatus.Claimed;

9.2 精度处理

  • 使用专门的 PriceCalculationLib 处理不同精度代币
  • 乘法在除法之前,减少精度损失
  • 向下取整保护合约资产

9.3 结算顺序

项目方必须在结算时转入足够的 RWA 代币:

1
2
3
4
5
6
// 使用 safeTransferFrom 确保转账成功
IERC20(project.rwaToken).safeTransferFrom(
msg.sender,
address(this),
project.totalTokensOffered
);

9.4 资金安全

  • 超额部分留在合约中直到用户领取
  • 每个用户的分配动态计算,确保总量不超支
  • 使用 nonReentrant 防止重入攻击

10. Gas 优化

10.1 动态计算 vs 预计算

当前实现采用动态计算分配:

  • 优点:节省结算时的 Gas
  • 缺点:每次 claim 需要计算

替代方案是结算时预计算

  • 优点:claim 时直接读取
  • 缺点:结算 Gas 高,可能超限

对于用户数量较多的项目,动态计算更优。

10.2 批量领取

可考虑添加批量领取函数:

1
2
3
4
5
function batchClaim(uint256[] calldata projectIds) external nonReentrant {
for (uint256 i = 0; i < projectIds.length; i++) {
// 内部调用 claim 逻辑
}
}