跳到主要内容

MTC 挖矿奖励

MTC(Mining Token Credit)是 Rocky 在 Canton 链上发行的交易挖矿奖励代币。每完成一笔 perp 交易,Rocky 平台铸造一份 MiningReward Daml 合约;用户可以随时主动 claim,把奖励变成可转移的 TokenHolding。本文介绍整套合约设计、生命周期、以及为什么采用"先铸后领"的两段式。

涉及概念:Canton Party、Daml signatory / controller / choice。如果不熟悉,请先阅读 Canton 网络介绍


一、核心概念

1.1 为什么需要"挖矿奖励"

Rocky 的撮合 + 资金账户都在链下数据库 (ledger.accounts),速度快但不可被第三方审计。为了让交易激励有不可抵赖的链上证据,Rocky 把"每笔交易都触发一份链上奖励合约"作为机制设计:

  • 链下:高频订单簿、撮合、margin
  • 链上:每笔成交后的奖励铸造(低频、可审计)

这也是 Rocky 与 Canton 网络结合的核心价值演示。

1.2 MTC 和 CC 的区别

维度CCMTC
来源用户充值(DevNet 模拟 / MainNet 钱包转账,见 CC 充值流程平台铸造,每笔交易自动触发
持有方式ledger.accounts.asset = 'CC'(链下记账)Canton 上的 TokenHolding Daml 合约
转账用户充进 / 平台代管(未来)用户之间 P2P,Daml 合约级转账
余额查询单一 SQL row遍历 ledger updates 聚合活跃合约

CC 是"可花的余额",MTC 是"激励代币"。两者并行存在,互不影响。


二、Daml 合约设计

2.1 两个核心模板

文件:exchange-app/daml/ExchangeApp.daml

template MiningReward
with
exchange : Party -- signatory(Rocky's app-provider)
miner : Party -- observer(用户的 Canton party)
rewardAmount : Decimal -- 本笔奖励的 MTC 数量
orderHash : Text -- 来源 trade UUID,幂等键
tradeValueUsdt : Decimal -- 触发本次奖励的成交名义价值
rewardTokenSymbol : Text -- 固定为 "MTC"
where
signatory exchange
observer miner
ensure rewardAmount > 0.0

-- 用户主动领取 -> 生成可转移的 TokenHolding
choice ClaimReward : ContractId TokenHolding
controller miner
do
create TokenHolding with
owner = miner
issuer = exchange
amount = rewardAmount
tokenName = "MyTestCoin"
tokenSymbol = rewardTokenSymbol
sourceOrderHash = Some orderHash

template TokenHolding
with
owner : Party -- 持有人
issuer : Party -- 发行方(Rocky)
amount : Decimal
tokenName : Text -- 当前为 "MyTestCoin"(历史名,对应 Rocky 时代的 MTC 简称)
tokenSymbol : Text -- "MTC"
sourceOrderHash : Optional Text
where
signatory issuer
observer owner
-- 未来扩展:transfer / split / merge choices

2.2 为什么是两段式(Mint + Claim)

可能有人会问:为什么不让 Rocky 直接创建 TokenHolding,跳过 MiningReward 中间态?

单段式(直接 mint TokenHolding)两段式(MiningReward → ClaimReward → TokenHolding)
❌ 用户无须签名就被强塞代币(违反 stakeholder consent)✅ 用户主动 exercise choice,等于明确接受
❌ 用户钱包/UI 必须实时跟踪所有合约✅ "待领取奖励"在 UI 中是独立状态,容易展示
❌ 没有显式的"trade → reward"溯源✅ MiningReward 自带 orderHash + tradeValueUsdt,链上即可审计
❌ 不易批量退还/撤销✅ 若发现作弊交易,可在 Claim 前 archive MiningReward

代价是用户多了一步"Claim"操作,但 UI 可以做成"一键全部领取",UX 损失可控。

2.3 关键签名/控制者设计

  • MiningReward.signatory = exchange → Rocky 单方铸造,用户无须签名
  • MiningReward.observer = miner → 用户可在自己 ledger 视图中看到这份合约(隐私模型保证只有 exchange + miner 可见)
  • ClaimReward.controller = miner → 只有用户本人可以触发 claim
  • TokenHolding.signatory = issuer (= exchange) → 由 Rocky 维持发行人责任;后续转账 choice 会需要 owner 共同签名

三、生命周期 (Lifecycle)

1. 用户在 perp 页下单 → 撮合成交


2. Frontend 拿到 fill 事件后调用:
POST /api/perp/rewards/mint
body: { traderParty, tradeId, tradeValueUsdt }


3. Backend 用 app-provider party 签名创建 MiningReward 合约
rewardAmount = tradeValueUsdt × REWARD_RATE
orderHash = tradeId


4. 合约写入 Canton Synchronizer(只对 exchange + miner 可见)


5. Dashboard 显示"待领取奖励 X MTC"
(/api/history 聚合所有未 archive 的 MiningReward)

▼ 用户点击 Claim
6. Frontend → POST /api/claim
body: { token, party, rewardContractId }


7. Backend 用用户 token + party 触发:
ExerciseCommand(MiningReward, ClaimReward, {})


8. Canton 同时 archive 旧 MiningReward + create 新 TokenHolding


9. Dashboard 显示"已持有 X MTC"
(TokenHolding owner = party)

四、代码导航

文件作用
daml-contracts/exchange-app/daml/ExchangeApp.damlMiningReward + TokenHolding 模板定义
mtc-exchange/src/lib/canton.tsEXCHANGE_PKG_IDREWARD_RATE = 1.0submitCommand 辅助
mtc-exchange/src/app/api/perp/rewards/mint/route.tsMint:每笔交易后调用,CreateCommand(MiningReward)
mtc-exchange/src/app/api/claim/route.tsClaim:用户主动领取,ExerciseCommand(ClaimReward)
mtc-exchange/src/app/api/history/route.ts聚合:遍历 app-provider party 视角的 ledger updates,统计 pending / claimed / holdings
mtc-exchange/src/components/TopNav.tsxTopNav modal 中显示 MTC 余额(来自 /api/historytotals.mtcBalance

五、配置项

5.1 REWARD_RATE

mtc-exchange/src/lib/canton.ts:

export const REWARD_RATE = 1.0;

含义:每 $1 名义成交价值,铸造 1 MTC。

当前为固定值;未来计划改为分层(VIP / 交易量阶梯)或随时间衰减("halving")。修改要点:

  • 改变这个常量后,所有新交易立即按新费率铸造,已经创建但未 claim 的 MiningReward 不受影响
  • 若需要"按时间段切换费率",应在后端而非前端配置,并将费率写入 MiningReward 合约本身(增加 rewardRate 字段)以便链上审计

5.2 EXCHANGE_PKG_ID

Daml package id,定位 ExchangeApp 模块。每次 Daml 模板修改后重新部署都会变。当前从环境变量读取:

export const EXCHANGE_PKG_ID =
process.env.EXCHANGE_PKG_ID ?? "<fallback>";

部署时通过 .env.local 设置。

5.3 reward token symbol

写死在 mint 路由调用里:rewardTokenSymbol: "MTC"。未来若 Rocky 发行多种奖励代币(如 "RKY"),可以让前端按用户/活动传入。


六、幂等性

orderHash = String(tradeId)(trade UUIDv7)。

  • 链上:MiningReward 模板本身没有 unique key on orderHash。理论上 Rocky 后端如果对同一 tradeId 调用两次 /api/perp/rewards/mint,会创建两份合约。
  • 链下:Dashboard 的 /api/history 在聚合时按 orderHash 去重显示(取最早一份),实际 claim 时只会 claim 用户选中的那份合约 id。
  • 建议:后端在 mint 前先查 MiningReward 是否存在 same orderHash 的活跃合约。或在 commandId 里加 trade UUID 作为 idempotency token(perp-reward-${orderHash.slice(0,12)}-${Date.now()} — 当前实现已部分采用,但 Date.now() 让 commandId 不严格幂等)。

正式上线前需要修正这一处。


七、隐私 (Privacy)

回顾 Canton 隐私模型

合约SignatoryObserver谁能看见
MiningRewardexchangeminerRocky + 用户本人
TokenHoldingexchange (issuer)miner (owner)Rocky + 用户本人

其他用户、其他 validator、Synchronizer 节点都看不到具体奖励数额或 orderHash。Rocky 内部因为 signatory 是 exchange,能聚合 dashboard 的全平台统计。


八、未来扩展 (Roadmap)

8.1 一键全部领取

当前 UI 是逐份 claim。批量 claim choice:

template MiningRewardBatch
with
miner : Party
rewards : [ContractId MiningReward]
...

或在 frontend 做"批量 ExerciseCommand"。

8.2 自动领取

后端定时扫描 unarchived MiningReward 并代用户 claim?目前不行,因为 ClaimReward.controller = miner,只有 miner 能签名。如果后端要代领,需要新增 AutoClaimToken 合约由用户预先签名授权。

8.3 转账 / 拆分

TokenHoldingTransfer / Split / Merge choices,让 MTC 真正成为可流通代币。需要解决:

  • 同 issuer 的 holding 是否可以合并?
  • 不同 owner 之间转账时新的合约 signatory 是谁?
  • 与 Splice 标准 token interface 的兼容?

8.4 取消错误奖励

发现交易作弊 / 撤销时,需要在 user claim 之前 archive MiningReward:

choice CancelReward : ()
controller exchange
do return ()

仅 controller=exchange 即可单方撤销,但前提是合约还没被 claim。

8.5 升级到 Splice 标准 token interface

TokenHolding 目前是 Rocky 私有模板。Splice 项目提供了标准化的 token interface(类似以太坊 ERC-20),让 Rocky MTC 能与 Canton 生态其他 wallet 兼容。是 MainNet 上线的硬要求。


九、相关文档

  • Canton 网络介绍
  • CC 充值流程
  • 仓库技术决策:
    • daml-contracts/exchange-app/daml/ExchangeApp.daml — Daml 源码
    • Canton_Custody_Wallet_Architecture.md — 托管模型(影响 TokenHolding transfer 设计)