Skip to main content

Architecture

mercatr is the cadastre. mercatr_market is the market layer on top of it.

For a product-level overview, see How It Works → Two Layers. This page stays close to the Move source.

note

This page covers the on-chain protocol modules only. Paint is a separate engagement layer that runs off-chain alongside the protocol — it is not part of the Move source tree and is not included in any of the diagrams below. For the user-facing description see Paint, and for the conceptual fit see Whitepaper → Engagement Surface.

Package and module tree

The repository currently ships these production Move modules.

mercatr

  • math/
    • signed
    • morton
  • geometry/
    • aabb
    • sat
    • topology
    • polygon
  • core/
    • index
    • metadata_store
    • metadata
    • mutations
    • admin

sources/tests.move and sources/poc_tests.move are test code, not protocol modules.

mercatr_market

  • market
  • pricing
  • revenue
  • tax
  • trading
  • parcel_ops
  • marks
  • proposals

The market package now has eight modules. marks and proposals are live modules, not future work.

Module Dependency Graph

The protocol package builds upward from math to geometry to core:

  • signed and morton sit at the bottom.
  • aabb, sat, and topology provide geometry primitives.
  • polygon builds parcel geometry and topology validation.
  • index stores polygons in the quadtree.
  • metadata_store and metadata attach parcel metadata to an index.
  • mutations rewrites parcel geometry through the index.
  • admin governs index lifecycle and capability rotation.

The market package sits on top of that core:

  • market owns the shared market object, level registry, treasury accessors, and tax wrappers.
  • pricing is pure math.
  • revenue splits payments between owner, treasury, and hierarchy flow.
  • tax stores deferred tax state as a dynamic field on the market object.
  • trading handles register, buyout, price bump, price drop, and governed remove.
  • parcel_ops handles expand, acquire, rebalance, split, and merge.
  • marks adds paid reactions on parcels and accepted POIs.
  • proposals manages POI proposal boards, escrow, acceptance, rejection, expiry, and per-parcel minimum offers.

Collision pipeline

Registration still uses a three-step collision pipeline:

  1. Broadphase: index::candidates probes overlapping quadtree cells.
  2. Midphase: aabb::intersects drops candidates with disjoint bounding boxes.
  3. Narrowphase: polygon::intersects runs SAT against surviving part pairs.

The current index implementation also tracks occupied_depths and enforces three broadphase limits from Config:

  • max_broadphase_span
  • max_cell_occupancy
  • max_probes_per_call

If the broadphase exceeds those limits, the call aborts with EBroadphaseBudgetExceeded or ECellOccupancyExceeded.

Shared object layout

mercatr::index::Config

Config stores mutable safety limits for one index:

  • max_vertices: u64
  • max_parts_per_polygon: u64
  • scaling_factor: u64
  • max_broadphase_span: u64
  • max_cell_occupancy: u64
  • max_probes_per_call: u64

max_depth is not part of Config. It lives directly on Index and stays fixed after creation because quadtree keys depend on it.

Default values from index::new():

  • cell_size = 1_000_000
  • max_depth = 20
  • max_vertices = polygon::max_vertices_per_part() which is 64
  • max_parts_per_polygon = polygon::max_parts() which is 10
  • max_broadphase_span = 1024
  • max_cell_occupancy = 64
  • max_probes_per_call = 2_000_000
  • scaling_factor = 1_000_000

mercatr::index::Index

Index is the shared quadtree object for one registry level.

  • id: UID
  • cells: Table<u64, vector<ID>>
  • polygons: Table<ID, Polygon>
  • cell_size: u64
  • max_depth: u8
  • count: u64
  • occupied_depths: u32
  • config: Config
  • authorized_caps: VecSet<ID>
  • authorization_sealed: bool

Two details matter for current integrations:

  • The polygon table is named polygons, not parcels.
  • Capability authorization is keyed by capability object ID, not by owner address.

Market object

The market object is versioned. It is not a flat struct.

Outer wrapper:

  • id: UID
  • paused: bool
  • inner: Versioned

Loaded inner value MarketInner:

  • version: u64
  • transfer_cap: index::TransferCap
  • lifecycle_cap: index::LifecycleCap
  • treasury: Balance<SUI>
  • price_states: Table<ID, PriceState>
  • levels: vector<Level>

Level stores:

  • index_id: ID
  • price_per_km2_mist: u64
  • min_area_m2: u64
  • max_area_m2: u64

Tax state attached to Market

Deferred tax state is not stored inside MarketInner. tax.move attaches a TaxStore dynamic field to the market UID.

TaxStore contains:

  • tax_fund: Balance<SUI>
  • version_counters: Table<ID, u64>
  • tax_buckets: Table<TaxBucketKey, TaxBucket>
  • tax_levels: vector<TaxLevelCfg>
  • self_claimable: Table<ID, u64>
  • polygon_buckets: Table<ID, vector<TaxBucketKey>>

Capability model

Each index trusts capability object IDs through authorized_caps: VecSet<ID>.

  • LifecycleCap gates register, remove, and transfer_ownership.
  • TransferCap gates force_transfer.
  • Minting a cap auto-authorizes it on the minting index.
  • admin::authorize_cap adds an existing cap ID to another index.
  • admin::revoke_transfer_cap and admin::revoke_lifecycle_cap remove cap IDs from an index.

authorization_sealed locks capability expansion on an index:

  • When authorization_sealed is true, minting and authorization abort with EIndexSealed.
  • Rotation PTBs must unseal, mint or authorize, then reseal in the same transaction.
  • Revocation remains available as the safety path.

TransferCap

TransferCap authorizes index::force_transfer. The market stores one in MarketInner and uses it for forced-sale ownership changes.

admin::force_remove is disabled. The code is commented out in core/admin.move because bypassing the market layer would orphan tax state. Use the market removal path instead.

Error code reference

This list reflects the constants declared in the current Move sources.

Geometry and math

  • aabb
    • EInvalidBox 1000
    • EBadVertices 1001
    • EMismatch 1002
  • sat
    • EBadVertices 1001
    • EMismatch 1002
    • EZeroAxis 1004
  • signed
    • EOverflow 1008
  • polygon
    • EEmpty 2001
    • ETooManyParts 2002
    • ENotConvex 2003
    • EBadVertices 2004
    • EMismatch 2005
    • EPartOverlap 2006
    • EInvalidMultipartContact 2007
    • EDisconnectedMultipart 2008
    • EInvalidBoundary 2009
    • EEdgeTooShort 2010
    • ECompactnessTooLow 2011
    • EAreaConservationViolation 2012
    • ECoordinateOutOfWorld 2013
    • EArithmeticOverflow 2014
  • morton
    • EDepthTooLarge 3001
    • ECannotGetParentOfRoot 3002
    • EBitsOverflow 3003

Core protocol

  • index
    • EBadVertices 4001
    • EMismatch 4002
    • ENotFound 4005
    • ENotOwner 4006
    • EBadMaxDepth 4009
    • ETooManyParts 4010
    • EOverlap 4012
    • EIndexNotEmpty 4013
    • EBadCellSize 4014
    • ENotAuthorized 4015
    • ECoordinateTooLarge 4016
    • EZeroAreaParcel 4017
    • EBadConfig 4018
    • ECapIndexMismatch 4019
    • ECapRevoked 4020
    • EQueryTooLarge 4021
    • EIndexEmpty 4022
    • EBroadphaseBudgetExceeded 4023
    • ECellOccupancyExceeded 4024
    • EIndexSealed 4025
  • mutations
    • ENotContained 5001
    • EOverlap 5002
    • ESelfRepartition 5003
    • ENotAdjacent 5004
    • EOwnerMismatch 5005
    • ESelfMerge 5006
    • EInvalidChildCount 5007
    • ETooManyChildren 5008
    • EAreaShrunk 5009
  • metadata
    • ENotOwner 6000
    • EMetadataNotFound 6001
    • ECidTooLong 6002

Market layer

  • market
    • EInvalidLevelPrice 3100
    • EInvalidAreaRange 3101
    • EDuplicateLevel 3102
    • EInvalidRate 3103
    • EInvalidFee 3104
    • EUnknownIndex 3105
    • EAreaOutOfRange 3107
    • EInsufficientPayment 3109
    • ENotOwner 3110
    • ENotRegistered 3111
    • EZeroAreaSlice 3112
    • ESameOwnerSlice 3113
    • EDifferentOwners 3114
    • EMarketPaused 3115
    • EWrongVersion 3116
    • EZeroPriceParcel 3118
    • EPriceOverflow 3119
  • trading
    • ESelfPurchase 3106
    • EAreaOutOfRange 3107
    • EZeroAreaParcel 3108
    • EInsufficientPayment 3109
    • ENotOwner 3110
    • ENotRegistered 3111
    • EMarketPaused 3115
    • EZeroPriceParcel 3118
    • EPremiumAtCeiling 3120
    • EPremiumAtFloor 3121
  • tax
    • ETaxNotInitialized 3200
    • EBatchTooLarge 3201
    • EWrongLevel 3202
    • EBucketNotExpired 3203
    • EAlreadyInitialized 3206
    • EInvalidTaxConfig 3207
    • EUnknownLevel 3208
  • marks
    • EInvalidMarkType 3300
    • EInsufficientFee 3301
  • proposals
    • ENotOwner 3401
    • ENotProposer 3402
    • EInsufficientFee 3403
    • ENameTooLong 3404
    • EIconTooLong 3405
    • EPolygonNotInBlockIndex 3406
    • EMaxProposalsPerPolygon 3407
    • EMaxProposalsPerProposer 3408
    • EProposalNotPending 3409
    • EProposalNotExpired 3410
    • EOfferBelowMinimum 3411

See also: Geometry Rules → Error Code Reference and API Reference.