MTC Mining Reward
MTC (Mining Token Credit) is Rocky's on-chain trading-mining reward token. Every completed perp trade causes Rocky to mint a MiningReward Daml contract; users can claim at any time to convert the reward into a transferable TokenHolding. This page covers the contract design, lifecycle, and the rationale for the two-phase "mint then claim" model.
Concepts used: Canton Party, Daml signatory / controller / choice. Read Canton Network first if needed.
1. Concept
1.1 Why an on-chain mining reward
Rocky's matching engine + account ledger live in an off-chain database (ledger.accounts) — fast but not third-party auditable. To make trading incentives have non-repudiable on-chain evidence, Rocky's design is: "every fill triggers an on-chain reward contract":
- Off-chain: high-frequency order book, matching, margin
- On-chain: a single reward minted per fill (low-frequency, auditable)
This is also the core demonstration of how Rocky integrates with Canton Network.
1.2 MTC vs CC
| Dimension | CC | MTC |
|---|---|---|
| Origin | User deposit (DevNet simulated, MainNet wallet transfer — see CC Deposit Flow) | Platform mint, automatic on every trade |
| Storage | ledger.accounts.asset = 'CC' (off-chain bookkeeping) | TokenHolding Daml contract on Canton |
| Transfer | User deposits / platform custody | (Future) P2P at the Daml-contract level |
| Balance query | Single SQL row | Aggregate active contracts via ledger updates |
CC is "spendable balance"; MTC is "incentive token". They coexist independently.
2. Daml contract design
2.1 Two core templates
File: exchange-app/daml/ExchangeApp.daml
template MiningReward
with
exchange : Party -- signatory (Rocky's app-provider)
miner : Party -- observer (user's Canton party)
rewardAmount : Decimal -- MTC amount for this reward
orderHash : Text -- source trade UUID, idempotency key
tradeValueUsdt : Decimal -- notional value of the triggering fill
rewardTokenSymbol : Text -- always "MTC"
where
signatory exchange
observer miner
ensure rewardAmount > 0.0
-- User actively claims → generates transferable 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 -- holder
issuer : Party -- issuer (Rocky)
amount : Decimal
tokenName : Text -- currently "MyTestCoin" (legacy name from the MTC era; pre-Rocky rebrand)
tokenSymbol : Text -- "MTC"
sourceOrderHash : Optional Text
where
signatory issuer
observer owner
-- Future extensions: transfer / split / merge choices
2.2 Why two-phase (Mint + Claim)
One might ask: why not have Rocky create TokenHolding directly, skipping MiningReward?
| Single-phase (direct TokenHolding mint) | Two-phase (MiningReward → ClaimReward → TokenHolding) |
|---|---|
| ❌ User receives tokens without signing (violates stakeholder consent) | ✅ User actively exercises choice — explicit acceptance |
| ❌ User wallet/UI must track every contract in real time | ✅ "Pending rewards" is a distinct state, easy to display |
| ❌ No explicit "trade → reward" provenance | ✅ MiningReward carries orderHash + tradeValueUsdt, auditable on-chain |
| ❌ Hard to batch refund / revoke | ✅ Can archive MiningReward before claim to revoke a fraudulent trade |
Cost: the user has one extra "Claim" step, but the UI can offer "Claim all" — UX cost is manageable.
2.3 Signature / controller design
MiningReward.signatory = exchange→ Rocky mints unilaterally, no user signature requiredMiningReward.observer = miner→ user can see this contract in their ledger view (privacy model ensures only exchange + miner can see)ClaimReward.controller = miner→ only the user themselves can trigger claimTokenHolding.signatory = issuer (= exchange)→ Rocky retains issuer responsibility; future transfer choices will require the owner's signature too
3. Lifecycle
1. User submits a perp order → matching fill
│
▼
2. Frontend gets the fill event, calls:
POST /api/perp/rewards/mint
body: { traderParty, tradeId, tradeValueUsdt }
│
▼
3. Backend uses app-provider party to sign + create MiningReward
rewardAmount = tradeValueUsdt × REWARD_RATE
orderHash = tradeId
│
▼
4. Contract written to Canton Synchronizer (visible only to exchange + miner)
│
▼
5. Dashboard shows "Pending reward: X MTC"
(/api/history aggregates all un-archived MiningRewards)
│
▼ User clicks Claim
6. Frontend → POST /api/claim
body: { token, party, rewardContractId }
│
▼
7. Backend uses user's token + party to:
ExerciseCommand(MiningReward, ClaimReward, {})
│
▼
8. Canton simultaneously archives the MiningReward + creates a TokenHolding
│
▼
9. Dashboard shows "Holding: X MTC"
(TokenHolding owner = party)
4. Source map
| File | Role |
|---|---|
daml-contracts/exchange-app/daml/ExchangeApp.daml | MiningReward + TokenHolding template definitions |
mtc-exchange/src/lib/canton.ts | EXCHANGE_PKG_ID, REWARD_RATE = 1.0, submitCommand helper |
mtc-exchange/src/app/api/perp/rewards/mint/route.ts | Mint — called after every fill, CreateCommand(MiningReward) |
mtc-exchange/src/app/api/claim/route.ts | Claim — user-triggered, ExerciseCommand(ClaimReward) |
mtc-exchange/src/app/api/history/route.ts | Aggregate — traverses ledger updates from the app-provider view, computes pending / claimed / holdings totals |
mtc-exchange/src/components/TopNav.tsx | TopNav modal displays MTC balance (from /api/history's totals.mtcBalance) |
5. Configuration
5.1 REWARD_RATE
mtc-exchange/src/lib/canton.ts:
export const REWARD_RATE = 1.0;
Meaning: 1 MTC minted per $1 of notional trade value.
Currently a fixed value; future plans include tiered rates (VIP / volume tiers) or halving-over-time. Notes:
- Changing this constant means all new trades mint at the new rate immediately. Already-minted-but-unclaimed MiningRewards are unaffected.
- For "rate switches over time periods," the rate should be controlled server-side (not client-side) and written into the
MiningRewardcontract itself (add arewardRatefield) for on-chain auditability.
5.2 EXCHANGE_PKG_ID
Daml package id locating the ExchangeApp module. Changes every time the Daml templates are modified + redeployed. Read from env:
export const EXCHANGE_PKG_ID =
process.env.EXCHANGE_PKG_ID ?? "<fallback>";
Set via .env.local at deploy time.
5.3 Reward token symbol
Hardcoded in the mint-route call: rewardTokenSymbol: "MTC". If Rocky later issues multiple reward tokens (e.g. "RKY"), the frontend could pass this per user / per campaign.
6. Idempotency
orderHash = String(tradeId) (trade UUIDv7).
- On-chain: the MiningReward template itself has no unique constraint on
orderHash. In principle if the Rocky backend calls/api/perp/rewards/minttwice for the same tradeId, two contracts will be created. - Off-chain: the dashboard's
/api/historyaggregator deduplicates byorderHashfor display (keeps the earliest). Actual claim only operates on the specific contract id the user picked. - Recommended: the backend should check for an existing active MiningReward with the same
orderHashbefore minting. Or include the trade UUID in thecommandIdas an idempotency token (perp-reward-${orderHash.slice(0,12)}-${Date.now()}— currently used, butDate.now()makes the commandId not strictly idempotent).
This needs fixing before production launch.
7. Privacy
Recall the Canton privacy model:
| Contract | Signatory | Observer | Who sees |
|---|---|---|---|
| MiningReward | exchange | miner | Rocky + the user |
| TokenHolding | exchange (issuer) | miner (owner) | Rocky + the user |
Other users, other validators, and Synchronizer nodes cannot see the reward amount or orderHash. Rocky can aggregate platform-wide stats internally because the exchange is the signatory.
8. Roadmap
8.1 Claim-all in one click
Current UI claims one at a time. Batch claim choice:
template MiningRewardBatch
with
miner : Party
rewards : [ContractId MiningReward]
...
Or do "batch ExerciseCommand" on the frontend.
8.2 Auto-claim
Backend periodically scans for un-archived MiningRewards and claims on the user's behalf? Currently impossible, because ClaimReward.controller = miner — only the miner can sign. If the backend wants to claim for users, a new AutoClaimToken contract pre-authorized by the user is needed.
8.3 Transfer / split
Add Transfer / Split / Merge choices to TokenHolding, making MTC a truly circulating token. Open questions:
- Can same-issuer holdings be merged?
- When transferring to a different owner, who is the new contract's signatory?
- Compatibility with the Splice standard token interface?
8.4 Revoke incorrect rewards
If a trade is found to be fraudulent / cancelled, archive the MiningReward before the user claims:
choice CancelReward : ()
controller exchange
do return ()
Single-side controller=exchange is enough, but only works pre-claim.
8.5 Upgrade to Splice standard token interface
TokenHolding is currently a Rocky-private template. Splice provides a standardized token interface (similar to ERC-20) that would let Rocky MTC work with other Canton-ecosystem wallets. A hard requirement for MainNet launch.
9. Related docs
- Canton Network
- CC Deposit Flow
- Repo source:
daml-contracts/exchange-app/daml/ExchangeApp.daml— Daml sourceCanton_Custody_Wallet_Architecture.md— custody model (affects future TokenHolding transfer design)