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.
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/signedmorton
geometry/aabbsattopologypolygon
core/indexmetadata_storemetadatamutationsadmin
sources/tests.move and sources/poc_tests.move are test code, not protocol modules.
mercatr_market
marketpricingrevenuetaxtradingparcel_opsmarksproposals
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:
signedandmortonsit at the bottom.aabb,sat, andtopologyprovide geometry primitives.polygonbuilds parcel geometry and topology validation.indexstores polygons in the quadtree.metadata_storeandmetadataattach parcel metadata to an index.mutationsrewrites parcel geometry through the index.admingoverns index lifecycle and capability rotation.
The market package sits on top of that core:
marketowns the shared market object, level registry, treasury accessors, and tax wrappers.pricingis pure math.revenuesplits payments between owner, treasury, and hierarchy flow.taxstores deferred tax state as a dynamic field on the market object.tradinghandles register, buyout, price bump, price drop, and governed remove.parcel_opshandles expand, acquire, rebalance, split, and merge.marksadds paid reactions on parcels and accepted POIs.proposalsmanages POI proposal boards, escrow, acceptance, rejection, expiry, and per-parcel minimum offers.
Collision pipeline
Registration still uses a three-step collision pipeline:
- Broadphase:
index::candidatesprobes overlapping quadtree cells. - Midphase:
aabb::intersectsdrops candidates with disjoint bounding boxes. - Narrowphase:
polygon::intersectsruns SAT against surviving part pairs.
The current index implementation also tracks occupied_depths and enforces three broadphase limits from Config:
max_broadphase_spanmax_cell_occupancymax_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: u64max_parts_per_polygon: u64scaling_factor: u64max_broadphase_span: u64max_cell_occupancy: u64max_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_000max_depth = 20max_vertices = polygon::max_vertices_per_part()which is 64max_parts_per_polygon = polygon::max_parts()which is 10max_broadphase_span = 1024max_cell_occupancy = 64max_probes_per_call = 2_000_000scaling_factor = 1_000_000
mercatr::index::Index
Index is the shared quadtree object for one registry level.
id: UIDcells: Table<u64, vector<ID>>polygons: Table<ID, Polygon>cell_size: u64max_depth: u8count: u64occupied_depths: u32config: Configauthorized_caps: VecSet<ID>authorization_sealed: bool
Two details matter for current integrations:
- The polygon table is named
polygons, notparcels. - 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: UIDpaused: boolinner: Versioned
Loaded inner value MarketInner:
version: u64transfer_cap: index::TransferCaplifecycle_cap: index::LifecycleCaptreasury: Balance<SUI>price_states: Table<ID, PriceState>levels: vector<Level>
Level stores:
index_id: IDprice_per_km2_mist: u64min_area_m2: u64max_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>.
LifecycleCapgatesregister,remove, andtransfer_ownership.TransferCapgatesforce_transfer.- Minting a cap auto-authorizes it on the minting index.
admin::authorize_capadds an existing cap ID to another index.admin::revoke_transfer_capandadmin::revoke_lifecycle_capremove cap IDs from an index.
authorization_sealed locks capability expansion on an index:
- When
authorization_sealedistrue, minting and authorization abort withEIndexSealed. - 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
aabbEInvalidBox1000EBadVertices1001EMismatch1002
satEBadVertices1001EMismatch1002EZeroAxis1004
signedEOverflow1008
polygonEEmpty2001ETooManyParts2002ENotConvex2003EBadVertices2004EMismatch2005EPartOverlap2006EInvalidMultipartContact2007EDisconnectedMultipart2008EInvalidBoundary2009EEdgeTooShort2010ECompactnessTooLow2011EAreaConservationViolation2012ECoordinateOutOfWorld2013EArithmeticOverflow2014
mortonEDepthTooLarge3001ECannotGetParentOfRoot3002EBitsOverflow3003
Core protocol
indexEBadVertices4001EMismatch4002ENotFound4005ENotOwner4006EBadMaxDepth4009ETooManyParts4010EOverlap4012EIndexNotEmpty4013EBadCellSize4014ENotAuthorized4015ECoordinateTooLarge4016EZeroAreaParcel4017EBadConfig4018ECapIndexMismatch4019ECapRevoked4020EQueryTooLarge4021EIndexEmpty4022EBroadphaseBudgetExceeded4023ECellOccupancyExceeded4024EIndexSealed4025
mutationsENotContained5001EOverlap5002ESelfRepartition5003ENotAdjacent5004EOwnerMismatch5005ESelfMerge5006EInvalidChildCount5007ETooManyChildren5008EAreaShrunk5009
metadataENotOwner6000EMetadataNotFound6001ECidTooLong6002
Market layer
marketEInvalidLevelPrice3100EInvalidAreaRange3101EDuplicateLevel3102EInvalidRate3103EInvalidFee3104EUnknownIndex3105EAreaOutOfRange3107EInsufficientPayment3109ENotOwner3110ENotRegistered3111EZeroAreaSlice3112ESameOwnerSlice3113EDifferentOwners3114EMarketPaused3115EWrongVersion3116EZeroPriceParcel3118EPriceOverflow3119
tradingESelfPurchase3106EAreaOutOfRange3107EZeroAreaParcel3108EInsufficientPayment3109ENotOwner3110ENotRegistered3111EMarketPaused3115EZeroPriceParcel3118EPremiumAtCeiling3120EPremiumAtFloor3121
taxETaxNotInitialized3200EBatchTooLarge3201EWrongLevel3202EBucketNotExpired3203EAlreadyInitialized3206EInvalidTaxConfig3207EUnknownLevel3208
marksEInvalidMarkType3300EInsufficientFee3301
proposalsENotOwner3401ENotProposer3402EInsufficientFee3403ENameTooLong3404EIconTooLong3405EPolygonNotInBlockIndex3406EMaxProposalsPerPolygon3407EMaxProposalsPerProposer3408EProposalNotPending3409EProposalNotExpired3410EOfferBelowMinimum3411
See also: Geometry Rules → Error Code Reference and API Reference.