Architecture
The protocol is split into two packages:
mercatr— The pure spatial cadastre (geometry, collision detection, spatial index)mercatr_market— The economic layer (pricing, payments, forced-sale land market)
Module Dependency Graph
mercatr::signed mercatr::morton
└── mercatr::aabb
└── mercatr::sat
└── mercatr::polygon
└── mercatr::index
├── mercatr::mutations
└── mercatr::admin
|
v
mercatr_market::price
|
v
mercatr_market::market_v2
Each module depends only on modules above it. signed and morton have no internal dependencies.
mercatr::mutations sits alongside mercatr::admin — both consume index internals. Mutations provides area-conserving geometry operations (reshape, repartition, split, merge) gated by LifecycleCap.
mercatr::admin owns the deploy-time AdminCap, creates owned Index objects on demand, and mints the TransferCap / LifecycleCap consumed by the market layer.
The mercatr_market package sits on top of mercatr, utilizing Index, TransferCap, Polygon, and mutations through the production market_v2 module.
Three-Phase Collision Pipeline
| Phase | Function | Input | Output | Aborts |
|---|---|---|---|---|
| Broadphase | index::candidates | query_id: ID | vector<ID> — parcels in overlapping quadtree cells (all depths) | ENotFound |
| Midphase | aabb::intersects | two AABB values | bool | — |
| Narrowphase | sat::overlaps / polygon::intersects | vertex arrays or Polygon refs | bool | EBadVertices, EMismatch |
The pipeline runs inside index::register. Broadphase prunes the search space to parcels
in overlapping quadtree cells across all depths. Midphase drops candidates whose bounding boxes do not have
positive-area overlap. Narrowphase runs SAT on every remaining part-pair. If any pair overlaps,
registration aborts with EOverlap.
register() Data Flow
- Caller passes
parts_xs: vector<vector<u64>>andparts_ys: vector<vector<u64>>. - Each part is validated: vertex count, convexity, and cached part AABB in
polygon::part. - A
Polygonobject is constructed viapolygon::new— computes global AABB, owner/epoch metadata, and enforces strict multipart topology (pairwise no-overlap, exact shared-edge connectivity, hole-free outer boundary, compactness floor). - Natural depth is computed: the shallowest quadtree level where the global AABB fits in a single cell. The polygon is stored at that single depth-prefixed Morton key.
- Broadphase:
index::candidatescollects IDs from all quadtree cells (all depths) that intersect the AABB. - For each candidate, midphase AABB check runs first; failing candidates are skipped.
- For each midphase survivor,
polygon::intersectsruns full SAT per part-pair. - Any positive-area overlap aborts with
EOverlap. - Polygon is inserted into
index.polygonsand its ID is added to its single cell inindex.cells. Registeredevent is emitted withdepth: u8indicating the natural quadtree depth.
Polygon semantics now require a single cadastral region: parts must be interior-disjoint, connected through exact shared edges, and induce one valid hole-free outer boundary.
Additional polygon aborts introduced by strict multipart validation:
2006—EPartOverlap2007—EInvalidMultipartContact2008—EDisconnectedMultipart2009—EInvalidBoundary2010—EEdgeTooShort2011—ECompactnessTooLow
Shared Object Layout
Cadastre Index Object
mercatr::index::Index is the shared spatial object for one registry level. A deployment can have multiple shared Index objects, with their IDs registered inside the market.
| Field | Type | Description |
|---|---|---|
id | UID | Sui object identity |
cells | Table<u64, vector<ID>> | Depth-prefixed Morton key → list of polygon IDs stored at that cell |
polygons | Table<ID, Polygon> | Polygon ID → stored Polygon object |
cell_size | u64 | Finest-level cell size in coordinate units (default 1_000_000) |
max_depth | u8 | Maximum quadtree depth (max 31, deploy default varies) |
count | u64 | Total registered parcels |
config | Config | Protocol safety limits (vertices, parts, depth) |
Market Object
mercatr_market::market_v2::MarketV2 is the shared object handling economic state.
| Field | Type | Description |
|---|---|---|
id | UID | Sui object identity |
transfer_cap | TransferCap | Auth capability to force-transfer polygons |
lifecycle_cap | LifecycleCap | Auth capability for register / remove / transfer_ownership |
treasury | Balance<SUI> | Immediate treasury accumulation |
price_states | Table<ID, PriceState> | Per-polygon v2 pricing state keyed by polygon ID |
levels | vector<Level> | Ordered level registry: index_id, price_per_km2_mist, min_area_m2, max_area_m2 |
paused | bool | Global kill-switch for high-impact market operations |
Deferred tax state is intentionally not stored directly on MarketV2. Instead, the production implementation attaches a dynamic TaxStore field to MarketV2.id. That store holds:
tax_fundversion_counterstax_buckets(keyed byTaxBucketKey: polygon_id, version, bucket_id, origin_level_rank, ancestor_distance, accrual_epoch)tax_levels(bucket_epochs/expiry_epochsactive; legacy retain/collector fields zeroed by setter)self_claimablepolygon_buckets
The registry is intentionally minimal:
- vector position is the canonical level rank
- UI labels and zoom bands stay off-chain
- public clients can discover every active shared
Indexby reading theMarketV2object alone
Default Config values:
| Parameter | Default |
|---|---|
max_vertices | 64 |
max_parts_per_polygon | 10 |
max_depth | up to 31 (Morton limit) |
scaling_factor | 1,000,000 |
Access Control and Capability Model
The protocol enforces two distinct capability types for privileged operations, with an authorized_caps registry on each Index controlling which caps are trusted.
Authorization Model — authorized_caps
Each Index maintains an authorized_caps: VecSet<ID> — the set of capability object IDs trusted by that index. Every cap-gated function (register, remove, transfer_ownership, force_transfer) asserts that the presented cap's ID is in the target index's authorized_caps set, aborting with ECapRevoked otherwise.
This model provides:
- Cross-index isolation: A cap not in an index's authorized set is rejected — no blast radius across indexes.
- Revocation:
admin::revoke_transfer_cap/admin::revoke_lifecycle_capremoves a cap from the set. - Multi-index authorization:
admin::authorize_cap(admin_cap, index, cap_id)registers an existing cap on additional indexes, enabling a single market contract to operate across multiple indexes without minting duplicate caps.
Both TransferCap and LifecycleCap structs carry an index_id: ID field recording which index minted them, but this field is not asserted at runtime — authorized_caps subsumes it.
LifecycleCap
register, remove, and transfer_ownership all require a &LifecycleCap as their first parameter. No call to these functions succeeds without one.
LifecycleCap is minted exclusively via admin::mint_lifecycle_cap(_: &AdminCap, index, ctx), which calls the package-internal index::mint_lifecycle_cap(index, ctx). Minting automatically adds the cap to the index's authorized_caps. The MarketV2 object holds a LifecycleCap at construction time and routes all lifecycle calls through it. This means:
- Registration is exposed through
market_v2::register, which supplies the cap internally while still enforcing market-level pricing and area rules. - Governance/emergency removal goes through
market_v2::remove, which supplies the cap internally and additionally requiresMarketV2AdminCap. - Direct ownership transfer goes through
index::transfer_ownershipby whichever authorized contract or caller holds a validLifecycleCap.
TransferCap
force_transfer requires a &TransferCap. This bypasses the owner check entirely and is used by market_v2::buy_full to reassign ownership after a forced buyout. The MarketV2 object holds the TransferCap alongside the LifecycleCap.
Admin Escape Hatches
admin::force_remove calls index::remove_unchecked directly — a public(package) function that skips both the capability check and the ownership check. This is an emergency path for governance, not a normal removal route. It requires AdminCap.
Capability Minting Summary
| Capability | Minted by | Visibility | Purpose |
|---|---|---|---|
AdminCap | init() (once, at deploy) | transferred to deployer | Governance |
TransferCap | admin::mint_transfer_cap / admin::mint_caps_with_ids | admin only | Forced ownership transfer (market buyout) |
LifecycleCap | admin::mint_lifecycle_cap / admin::mint_caps_with_ids | admin only | Register / remove / transfer_ownership |
Capability Lifecycle Management
| Operation | Function | Effect |
|---|---|---|
| Mint | admin::mint_*_cap | Creates cap, adds to minting index's authorized_caps |
| Authorize on additional index | admin::authorize_cap | Adds existing cap ID to another index's authorized_caps |
| Revoke | admin::revoke_*_cap | Removes cap ID from index's authorized_caps |
Error Code Reference
The complete error code reference is maintained in docs/yellowpaper.md §6.3. Key codes by module:
Index (4xxx): EBadVertices (4001), EMismatch (4002), ENotFound (4005), ENotOwner (4006), EBadMaxDepth (4009), ETooManyParts (4010), EOverlap (4012), EIndexNotEmpty (4013), EBadCellSize (4014), ENotAuthorized (4015), ECoordinateTooLarge (4016), ECapIndexMismatch (4019, unused — retained for reference), ECapRevoked (4020).
Polygon (2xxx): EEmpty (2001), ETooManyParts (2002), ENotConvex (2003), EBadVertices (2004), EMismatch (2005), EPartOverlap (2006), EInvalidMultipartContact (2007), EDisconnectedMultipart (2008), EInvalidBoundary (2009), EEdgeTooShort (2010), ECompactnessTooLow (2011), EAreaConservationViolation (2012).
Mutations (5xxx): ENotContained (5001), EOverlap (5002), ESelfRepartition (5003), ENotAdjacent (5004), EOwnerMismatch (5005), ESelfMerge (5006).
Market V2 (31xx): EInvalidPrice (3100), EInvalidAreaRange (3101), EDuplicateLevel (3102), EInvalidRate (3103), EInvalidFee (3104), EUnknownIndex (3105), ESelfPurchase (3106), EAreaOutOfRange (3107), EZeroAreaParcel (3108), EInsufficientPayment (3109), ENotOwner (3110), ENotRegistered (3111), EZeroAreaSlice (3112), ESameOwnerSlice (3113), EDifferentOwners (3114).
Tax (32xx): ETaxNotInitialized (3200), EBatchTooLarge (3201), EWrongLevel (3202), EBucketNotExpired (3203), ETaxNotOwner (3204), ENoSelfClaimable (3205), EAlreadyInitialized (3206), EInvalidTaxConfig (3207), EUnknownLevel (3208).
Cascading Tax Runtime
The production market layer exposes cascading tax behavior through market_v2:
init_taxinitializesTaxStore.set_tax_level_cfgconfigures per-level bucket cadence and expiry; legacy retain/collector fields are zeroed by the setter.collect_batch/collect_batch_flatrealize cascading parent collection with multi-match lookup, anti-retro guard (parent.created_epoch <= bucket.accrual_epoch), and delete-after-collect semantics.withdraw_taxlets owners withdrawself_claimablebalances.sweep_expiredpermissionlessly moves expired bucket remainders to treasury.
Each sale creates one deferred bucket on the sold polygon. The direct parent collects, keeps a weighted share, and forwards the remainder up the hierarchy by opening a cascade bucket on itself. The cascade preserves accrual_epoch from the original sale so anti-retro eligibility propagates through the chain.
Tax accrual occurs on all payment-bearing flows: register, buy_full, expand_unclaimed, and acquire_slice. Geometry-changing operations close tax versions, while remove and merge_owned sweep child-side tax state so bucket accounting remains aligned with current polygons.