Skip to main content

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

PhaseFunctionInputOutputAborts
Broadphaseindex::candidatesquery_id: IDvector<ID> — parcels in overlapping quadtree cells (all depths)ENotFound
Midphaseaabb::intersectstwo AABB valuesbool
Narrowphasesat::overlaps / polygon::intersectsvertex arrays or Polygon refsboolEBadVertices, 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

  1. Caller passes parts_xs: vector<vector<u64>> and parts_ys: vector<vector<u64>>.
  2. Each part is validated: vertex count, convexity, and cached part AABB in polygon::part.
  3. A Polygon object is constructed via polygon::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).
  4. 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.
  5. Broadphase: index::candidates collects IDs from all quadtree cells (all depths) that intersect the AABB.
  6. For each candidate, midphase AABB check runs first; failing candidates are skipped.
  7. For each midphase survivor, polygon::intersects runs full SAT per part-pair.
  8. Any positive-area overlap aborts with EOverlap.
  9. Polygon is inserted into index.polygons and its ID is added to its single cell in index.cells.
  10. Registered event is emitted with depth: u8 indicating 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:

  • 2006EPartOverlap
  • 2007EInvalidMultipartContact
  • 2008EDisconnectedMultipart
  • 2009EInvalidBoundary
  • 2010EEdgeTooShort
  • 2011ECompactnessTooLow

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.

FieldTypeDescription
idUIDSui object identity
cellsTable<u64, vector<ID>>Depth-prefixed Morton key → list of polygon IDs stored at that cell
polygonsTable<ID, Polygon>Polygon ID → stored Polygon object
cell_sizeu64Finest-level cell size in coordinate units (default 1_000_000)
max_depthu8Maximum quadtree depth (max 31, deploy default varies)
countu64Total registered parcels
configConfigProtocol safety limits (vertices, parts, depth)

Market Object

mercatr_market::market_v2::MarketV2 is the shared object handling economic state.

FieldTypeDescription
idUIDSui object identity
transfer_capTransferCapAuth capability to force-transfer polygons
lifecycle_capLifecycleCapAuth capability for register / remove / transfer_ownership
treasuryBalance<SUI>Immediate treasury accumulation
price_statesTable<ID, PriceState>Per-polygon v2 pricing state keyed by polygon ID
levelsvector<Level>Ordered level registry: index_id, price_per_km2_mist, min_area_m2, max_area_m2
pausedboolGlobal 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_fund
  • version_counters
  • tax_buckets (keyed by TaxBucketKey: polygon_id, version, bucket_id, origin_level_rank, ancestor_distance, accrual_epoch)
  • tax_levels (bucket_epochs / expiry_epochs active; legacy retain/collector fields zeroed by setter)
  • self_claimable
  • polygon_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 Index by reading the MarketV2 object alone

Default Config values:

ParameterDefault
max_vertices64
max_parts_per_polygon10
max_depthup to 31 (Morton limit)
scaling_factor1,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_cap removes 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 requires MarketV2AdminCap.
  • Direct ownership transfer goes through index::transfer_ownership by whichever authorized contract or caller holds a valid LifecycleCap.

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

CapabilityMinted byVisibilityPurpose
AdminCapinit() (once, at deploy)transferred to deployerGovernance
TransferCapadmin::mint_transfer_cap / admin::mint_caps_with_idsadmin onlyForced ownership transfer (market buyout)
LifecycleCapadmin::mint_lifecycle_cap / admin::mint_caps_with_idsadmin onlyRegister / remove / transfer_ownership

Capability Lifecycle Management

OperationFunctionEffect
Mintadmin::mint_*_capCreates cap, adds to minting index's authorized_caps
Authorize on additional indexadmin::authorize_capAdds existing cap ID to another index's authorized_caps
Revokeadmin::revoke_*_capRemoves 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_tax initializes TaxStore.
  • set_tax_level_cfg configures per-level bucket cadence and expiry; legacy retain/collector fields are zeroed by the setter.
  • collect_batch / collect_batch_flat realize cascading parent collection with multi-match lookup, anti-retro guard (parent.created_epoch <= bucket.accrual_epoch), and delete-after-collect semantics.
  • withdraw_tax lets owners withdraw self_claimable balances.
  • sweep_expired permissionlessly 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.