Skip to main content

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

DimensionCCMTC
OriginUser deposit (DevNet simulated, MainNet wallet transfer — see CC Deposit Flow)Platform mint, automatic on every trade
Storageledger.accounts.asset = 'CC' (off-chain bookkeeping)TokenHolding Daml contract on Canton
TransferUser deposits / platform custody(Future) P2P at the Daml-contract level
Balance querySingle SQL rowAggregate 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 required
  • MiningReward.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 claim
  • TokenHolding.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

FileRole
daml-contracts/exchange-app/daml/ExchangeApp.damlMiningReward + TokenHolding template definitions
mtc-exchange/src/lib/canton.tsEXCHANGE_PKG_ID, REWARD_RATE = 1.0, submitCommand helper
mtc-exchange/src/app/api/perp/rewards/mint/route.tsMint — called after every fill, CreateCommand(MiningReward)
mtc-exchange/src/app/api/claim/route.tsClaim — user-triggered, ExerciseCommand(ClaimReward)
mtc-exchange/src/app/api/history/route.tsAggregate — traverses ledger updates from the app-provider view, computes pending / claimed / holdings totals
mtc-exchange/src/components/TopNav.tsxTopNav 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 MiningReward contract itself (add a rewardRate field) 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/mint twice for the same tradeId, two contracts will be created.
  • Off-chain: the dashboard's /api/history aggregator deduplicates by orderHash for 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 orderHash before minting. Or include the trade UUID in the commandId as an idempotency token (perp-reward-${orderHash.slice(0,12)}-${Date.now()} — currently used, but Date.now() makes the commandId not strictly idempotent).

This needs fixing before production launch.


7. Privacy

Recall the Canton privacy model:

ContractSignatoryObserverWho sees
MiningRewardexchangeminerRocky + the user
TokenHoldingexchange (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.


  • Canton Network
  • CC Deposit Flow
  • Repo source:
    • daml-contracts/exchange-app/daml/ExchangeApp.daml — Daml source
    • Canton_Custody_Wallet_Architecture.md — custody model (affects future TokenHolding transfer design)