marketplace spec
contract interfaces, data structures, state schemas, message types, and cross-contract sequences for the marketplace protocol.
for economic rationale, product overview, and phasing see cyberia/protocol/marketplace.
data model
Asset
one cw-asset instance per physical or logical asset.
Asset
Capacity
SaleOffer
RevenueSample
Issuance
one cw-issuance instance per event, visa round, or production batch.
Issuance
IssuanceStatus
Category
ClaimClass
state machines:
NF: Submitted → Approved → Closed
└→ Declined
└→ Cancelled (from Submitted or Approved)
F: Submitted → Approved → Open → Closed → Expired
└→ Declined
└→ Cancelled (from Submitted or Approved)
Claim (NF)
stored as cw721 extension on cw-claim global contract.
ClaimExt
Claim (F)
balance on a cw-coin contract. no per-unit record.
CoinConfig
Audience
Audience
AudienceGate
oracle
annual_GR
pipeline is committed revenue (already paid, already recorded in issuance.networth) from approved issuances whose windows fall in the forward 12 months.
V_implied
returns zero for new assets with no history. auto-listing falls back to reserve_price.
current_ask
None when sale_offer is None: asset not for sale.
pricing stack
four layers applied to every claim purchase:
discount: one discount per claim, max of category and audience, not additive.
bonding curve v1: piecewise linear.
curve = [(supply_0, price_0), (supply_1, price_1), ...]
price_at(u) = linear interpolation over supply breakpoints
continuous curves (constant product, exponential) in v2.
calendar (Slot + NF)
base unit: 1 hour. interval list, not per-hour bitmap. one booking = one storage write regardless of duration. overlap check = two B-tree lookups.
SCHEDULE:
Booking
invariants enforced at allocate time:
duration ≥ asset.min_duration_hours
start ≥ now_h + asset.cancel_lead_hours
start ≤ now_h + asset.max_horizon_hours // e.g. 8760 = 1 year forward
tier counts (Spot + NF)
TIER_COUNTS:
tier escalation (upgrade without re-issuance):
non-fungible flows
submit issuance (slot event)
shaman → registry.CreateIssuance {
asset, window_start, duration_hours,
categories, deposit
}
├─ instantiate cw-issuance(asset, ...) → addr I
├─ I.submsg: asset.Allocate(window_start, duration_hours, I)
├─ I.submsg: usdt.TransferFrom(shaman → I, deposit)
├─ registry: claim.AddMinter(I)
└─ registry indexes I
Allocate fails if window overlaps an existing booking → CreateIssuance rolls back.
submit issuance (spot visa round)
authority → registry.CreateIssuance {
asset, tier, cap,
window_duration, deposit
}
├─ instantiate cw-issuance(asset, ...) → addr I
├─ asset: ACTIVE_ISSUANCES.insert(I)
├─ registry: claim.AddMinter(I)
└─ registry indexes I
buy claim (NF)
buyer → issuance.BuyClaim {
category_id, audience_id, qty,
class, max_price
}
├─ resolve audience from audiences_snapshot
├─ final = final_price(curve, utilization, audience, category, qty)
├─ require final ≤ max_price
├─ require category.sold + qty ≤ category.cap
├─ submsg: usdt.TransferFrom(buyer → issuance, final)
├─ submsg: claim.Mint(buyer, ClaimExt { asset, issuance, tier, ... })
├─ if Spot: submsg asset.IncrementTier(tier, qty)
├─ category.sold += qty
├─ networth += final
└─ submsg: asset.RecordRevenue(final, ClaimSale)
preempt flexible claim (phase 6)
new_buyer → issuance.PreemptFlexible { token_id }
├─ require claim.class == Flexible
├─ compensation = claim.price_paid × (1 + flexible_premium_bps / 10_000)
├─ new_price = current ask for this tier / category
├─ require funds_attached ≥ compensation + (new_price − claim.price_paid)
├─ submsg: usdt.Transfer(compensation → original holder)
├─ submsg: usdt.Transfer(incremental → issuance)
├─ submsg: claim.TransferNft(token_id → new_buyer)
├─ claim.price_paid = new_price
├─ networth += incremental
└─ emit ClaimPreempted
close issuance (NF)
operator → issuance.Close
├─ require status == Approved
├─ require now_h > window_end (slot) OR operator discretion (spot round)
├─ split networth: dao_bps + master_bps + provider_bps = 10_000
├─ submsg: usdt.Transfer(provider_share → asset.owner)
├─ submsg: usdt.Transfer(dao_share → dao_addr)
├─ submsg: usdt.Transfer(deposit_locked → creator)
├─ submsg: asset.Free(window_start) // slot
│ OR submsg: asset.DecrementTier(tier, sold) // spot
├─ submsg: asset.RecordRevenue(networth, ClaimSale)
└─ status = Closed
fungible flows
submit fungible issuance
operator → registry.CreateIssuance {
asset, kind: Fungible,
supply_cap, window_start, window_duration,
base_curve, deposit
}
├─ instantiate cw-issuance(asset, ...) → addr I
├─ I.submsg: usdt.TransferFrom(operator → I, deposit)
└─ registry indexes I
approve fungible issuance
operator → I.Approve
├─ status: Approved → Open
├─ submsg: instantiate cw-coin {
│ asset, issuance: I, supply_cap,
│ valid_from: window_start,
│ valid_until: window_start + window_duration,
│ minter: I,
│ } → addr C
├─ I.coin = C
└─ asset: ACTIVE_ISSUANCES.insert(I)
mint units (F)
buyer → I.MintUnits { qty, audience_id, max_price }
├─ price = curve.integrate(supply_minted, supply_minted + qty)
├─ apply audience discount
├─ require price ≤ max_price
├─ require supply_minted + qty ≤ supply_cap
├─ submsg: usdt.TransferFrom(buyer → I, price)
├─ submsg: cw-coin.Mint(buyer, qty)
├─ supply_minted += qty
├─ networth += price
└─ submsg: asset.RecordRevenue(price, ClaimSale)
bonding curve for N units: price = ∫[s → s+N] f(x) dx where f(x) is the curve function and s = supply_minted.
redeem units (F)
operator → I.Redeem { holder, qty, evidence }
├─ require status == Open
├─ require now_h ∈ [valid_from, valid_until]
├─ submsg: cw-coin.BurnFrom(holder, qty)
└─ emit CoinRedeemed { holder, qty, asset, evidence }
evidence is a free-form delivery receipt ID, weighbridge ticket, or API call reference. paper trail only — off-chain fraud prevention is the asset's responsibility.
close fungible issuance
automatic at valid_until OR operator → I.Close
├─ if supply_minted > redeemed: submsg cw-coin.BurnRemaining
├─ split networth
├─ submsg: usdt.Transfer(provider_share → asset.owner)
├─ submsg: usdt.Transfer(dao_share → dao_addr)
├─ submsg: usdt.Transfer(deposit_locked → creator)
├─ submsg: asset.RecordRevenue(networth, ClaimSale)
└─ status = Closed
what fungible flows omit (intentional)
| feature | reason absent |
|---|---|
| Categories | units are units; no "early bird gravel" subdivisions |
| Guaranteed / Flexible class | cannot preempt a cubic meter already sold |
| calendar overlap check | bulk delivery is continuous, not slot-bookable |
| per-claim extension record | claims are balances, not identities |
| secondary preemption market | secondary trading is standard cw20 transfer |
contracts
cw-registry (1 instance)
factory, address book, minter grant for cw-claim.
ExecuteMsg:
CreateAsset {
kind, claim_fungibility, cap_rate_bps,
meta_ipfs, audiences
} # OPERATOR
CreateIssuance {
asset, window_start, window_duration,
categories?, supply_cap?, base_curve?,
deposit
} # any neuron OR OPERATOR
GrantOperator { addr } # OPERATOR
RevokeOperator { addr } # OPERATOR
QueryMsg:
Assets { start_after, limit }
Issuances { start_after, limit }
IssuancesByAsset { asset, start_after, limit }
IssuancesByCreator { creator, start_after, limit }
IsOperator { addr }
State:
CONFIG {
asset_code_id, issuance_code_id,
claim_addr, coin_code_id,
usdt_addr, dao_addr,
dao_bps, master_bps
}
OPERATORS: Set<Addr>
ASSETS: Vec<Addr>
ISSUANCES: Vec<Addr>
ASSET_BY_OWNER: Map<Addr, Vec<Addr>>
ISSUANCE_BY_ASSET: Map<Addr, Vec<Addr>>
ISSUANCE_BY_CREATOR: Map<Addr, Vec<Addr>>
cw-asset (N instances)
per-asset state. variant selected at instantiate from kind × claim_fungibility.
ExecuteMsg (all variants):
UpdateMeta { meta_ipfs }
SetAudiences { audiences }
SetTierCurve { horizon_hours, curve }
SetSaleOffer { reserve_price, premium_bps }
ClearSaleOffer
BuyAsset { } # attach funds ≥ current_ask
TransferOwnership { new_owner } # OPERATOR only
RecordRevenue { amount, kind } # active issuance only
RecordConsumption { qty } # active issuance only (F)
ExecuteMsg (Slot + NF only):
Allocate { start_hour, duration_hours, issuance }
Free { start_hour }
ExecuteMsg (Spot + NF only):
IncrementTier { tier, by, issuance }
DecrementTier { tier, by }
SetTierCap { tier, cap }
EscalateClaim { token_id, new_tier }
QueryMsg:
Config
Audiences
TierCurve { horizon_hours }
CurrentAsk
ImpliedValuation
GrossRevenue { window_hours }
Utilization { tier? }
Calendar { from_hour, to_hour } # Slot + NF only
TierCounts # Spot + NF only
State:
CONFIG, AUDIENCES, TIER_CURVES, SALE_OFFER
GROSS_REVENUE_LOG: Map<u64, RevenueSample>
ACTIVE_ISSUANCES: Set<Addr>
SCHEDULE: Map<u64, Booking> # Slot + NF only
TIER_COUNTS: Map<u32, (u32, u32)> # Spot + NF only
cw-issuance (N instances)
per-issuance state. lifecycle and revenue tracking.
ExecuteMsg (NF):
UpdateMeta { meta_ipfs } # creator while Submitted
AddCategory { category } # creator while Submitted
UpdateCategory { id, updates } # creator while Submitted
Approve # OPERATOR
Decline { reason } # OPERATOR
Cancel { reason } # OPERATOR or creator while Submitted
Close # OPERATOR
BuyClaim {
category_id, audience_id, qty,
class, max_price
}
PreemptFlexible { token_id, new_buyer } # phase 6
ExecuteMsg (F):
UpdateMeta { meta_ipfs } # creator while Submitted
UpdateCurve { curve } # creator while Submitted
Approve # OPERATOR; instantiates cw-coin
Decline { reason } # OPERATOR
Cancel { reason } # OPERATOR or creator while Submitted
Close # OPERATOR or automatic at valid_until
MintUnits { qty, audience_id, max_price }
Redeem { holder, qty, evidence } # OPERATOR
QueryMsg:
Config
Status
Networth
AudiencesSnapshot
Categories # NF
Buyers { start_after, limit } # NF
Coin # F: returns cw-coin address
Supply # F: (cap, minted, redeemed)
CurveAt { supply } # F: price at given supply level
State:
CONFIG
STATUS: IssuanceStatus
AUDIENCES_SNAPSHOT: Vec<Audience>
DEPOSIT_LOCKED: Uint128
NETWORTH: Uint128
CATEGORIES: Vec<Category> # NF
BUYERS: Map<Addr, Vec<TokenId>> # NF
COIN_ADDR: Option<Addr> # F
SUPPLY_MINTED: u64 # F
CURVE: BondingCurve # F
cw-claim (1 instance, global cw721)
unified non-fungible claim store. cw721 base + ClaimExt.
ExecuteMsg:
Mint { to, ext: ClaimExt } # active NF issuance only (MINTER role)
TransferNft { recipient, token_id } # standard cw721
SendNft { contract, token_id, msg } # standard cw721
Approve { spender, token_id, expires } # standard cw721
Revoke { spender, token_id } # standard cw721
ApproveAll { operator, expires } # standard cw721
RevokeAll { operator } # standard cw721
Redeem { token_id } # OPERATOR: marks redeemed
RevokeToken { token_id, reason } # OPERATOR: marks revoked
AddMinter { addr } # registry only
RemoveMinter { addr } # registry only
QueryMsg:
OwnerOf, Approval, Approvals, AllOperators # cw721 standard
NumTokens, ContractInfo, NftInfo, AllNftInfo
Tokens, AllTokens
ClaimInfo { token_id } # returns ClaimExt
ClaimsByAsset { asset, start_after, limit }
ClaimsByIssuance { issuance, start_after, limit }
ClaimsByOwner { owner, start_after, limit }
IsRedeemed { token_id }
IsValid { token_id, at_hour? } # checks revoked, redeemed, valid_until
State:
cw721_base
CLAIM_EXT: Map<TokenId, ClaimExt>
MINTERS: Set<Addr>
cw-coin (N instances, per fungible batch)
cw20-base with supply cap enforcement, time-bounded validity, single-minter.
ExecuteMsg:
Transfer { recipient, amount } # cw20 standard
Send { contract, amount, msg } # cw20 standard
Burn { amount } # cw20 standard
BurnFrom { owner, amount } # cw20 standard (via allowance)
IncreaseAllowance, DecreaseAllowance # cw20 standard
TransferFrom # cw20 standard (via allowance)
Mint { recipient, amount } # minter only; enforces supply_cap
ExpireSupply # callable after valid_until; burns remainder
QueryMsg:
Balance, TokenInfo, Allowance, AllAllowances # cw20 standard
Minter
ValidityWindow # (valid_from, valid_until)
SupplyCap
SupplyMinted
State:
cw20_base
MINTER: Addr
SUPPLY_CAP: u64
VALIDITY: (u64, u64) // (valid_from, valid_until)
instantiated by cw-issuance on Approve. one instance per (issuance, tier, audience_id, class) combination.
cw-bridge-eth + cw-usdt
threshold attestation model. k-of-n independent relayers must confirm before any mint or withdrawal executes. there is no single-attestor mode.
cw-bridge-eth:
InstantiateMsg:
attestors: Vec<Addr>,
threshold: u32, // k in k-of-n; e.g. 3 of 5
ExecuteMsg:
Attest { eth_tx_hash, recipient, amount } // any attestor; idempotent per (attestor, tx)
Withdraw { bostrom_sender, eth_recipient, amount } // relayers co-sign off-chain, submit here
AddAttestor { addr } // admin; raises n
RemoveAttestor { addr } // admin; must keep n > threshold
UpdateThreshold { new_k } // admin; must keep k ≤ n
QueryMsg:
Config // (attestors, threshold)
AttestationStatus { eth_tx_hash } // (confirmations_so_far, minted)
PendingWithdrawals
State:
CONFIG { attestors: Set<Addr>, threshold: u32 }
ATTESTATIONS: Map<EthTxHash, Set<Addr>> // which attestors confirmed each tx
PROCESSED_TX: Set<EthTxHash> // finalized; idempotency guard
PENDING_WITHDRAWALS: Map<TxId, WithdrawalRequest>
Invariants:
// mint triggers only when ATTESTATIONS[tx].len() >= threshold
// each attestor can confirm a tx at most once
// threshold update rejected if new_k > current n
mint flow:
relayer_i → Attest { eth_tx_hash: H, recipient: R, amount: A }
├─ require sender ∈ ATTESTORS
├─ require H ∉ PROCESSED_TX
├─ require ATTESTATIONS[H] does not already contain sender
├─ ATTESTATIONS[H].insert(sender)
├─ if ATTESTATIONS[H].len() < threshold: return Ok (not yet)
└─ else:
├─ PROCESSED_TX.insert(H)
├─ submsg: cw-usdt.Mint(R, A)
└─ emit Minted { eth_tx_hash: H, recipient: R, amount: A }
withdrawal flow (burn on bostrom → pay on ETH):
holder → cw-usdt.Burn { amount, eth_recipient }
├─ cw20 burn reduces supply
├─ emit WithdrawalRequested { bostrom_sender, eth_recipient, amount, id }
└─ relayers observe event off-chain, co-sign payout, execute ETH transfer
cw-usdt (cw20):
ExecuteMsg:
cw20 standard (Transfer, Send, Burn, BurnFrom, etc.)
Mint { recipient, amount } // bridge only
Burn { amount, eth_recipient: String } // holder withdrawal initiation
State:
cw20_base
BRIDGE: Addr // only address that can call Mint
decisions confirmed
- denomination: bridged USDT only
- bridge: threshold k-of-n attestation from deployment; no single-key mode
- buyer authentication: bot provides custodial signing as a UX convenience; contracts accept any valid signed tx; user-controlled signing via cyberia/cw-cyber-passport when ready
- discount stacking: max(category, audience); not additive
- audience gate: Operator (bot-enforced via telegram group) as initial gate; on-chain allowlist and badge gates available in the same contract from deployment
- registry: single instance; factory + index
- claim representation NF: global cw721 cw-claim with ClaimExt
- claim representation F: cw-coin per (issuance, tier, audience_id, class)
- calendar granularity (NF Slot): 1 hour; interval list storage, O(log N) overlap check
- oracle formula: GIM approach — annual_GR / r_gross; r_gross absorbs OPEX expectations per asset class (see cap rate table in cyberia/protocol/marketplace)
- GR window: trailing 12m realized + forward 12m committed pipeline
- V_implied = 0 for new assets with no revenue history; auto-listing shows reserve_price — correct behavior, not a missing feature
- auto-listing: max(reserve_price, V_implied × (1 + premium_bps / 10_000))
- claim classes NF: both Guaranteed and Flexible supported from deployment; operators choose which to offer per issuance
- preemption compensation: price_paid × (1 + flexible_premium_bps)
- pricing discovery: bonding curve seeds liquidity; limit-order book overlay as later enhancement; curve IS the AMM for F flows
- fungible supply expiry: automatic burn at valid_until
- combinatorial auction: separate module; not required for protocol correctness
decisions deferred (genuinely open, not simplifications)
- exact dao_bps and master_bps for revenue split — DAO governance decision
- cap_rate_bps defaults per class — DAO config; indicative r_gross table in cyberia/protocol/marketplace
- initial bridge attestor set and threshold k — operational decision before deploy (minimum: 3-of-5)
- bridge fee on mint — possibly ε for treasury, not required for correctness
- whether seed tier_curves are required or owner may disable them
- tier escalation refund rules for spot downgrades
- whether spot tier-capacity changes grandfather existing holders
- bonding curve shape for F: piecewise linear vs exponential — empirical question; both are correct
- fungible coin cross-audience transfer — risk of audience-bypass arbitrage requires explicit policy decision
discover all concepts