Skip to main content

merca.earth Cadastre Protocol

On-Chain Land Cadastre with Trustless Overlap Prevention on Sui

Yellow Paper v0.8.0

Production Ready — March 2026


Abstract

merca.earth is an on-chain land cadastre protocol for trustless registration, spatial indexing, and overlap prevention of two-dimensional land parcels on the Sui blockchain. The protocol enforces non-overlapping property boundaries through a three-phase collision detection pipeline executed entirely during registration: a hierarchical quadtree spatial index with Morton-coded cells for broadphase culling, axis-aligned bounding box (AABB) filtering for midphase refinement, and the Separating Axis Theorem (SAT) for narrowphase overlap detection. Any parcel that has a positive-area overlap with an existing registered parcel is automatically rejected, providing a protocol-level guarantee of cadastral integrity.

All computation and state reside entirely on-chain, implemented natively in Sui Move. It requires no off-chain components, oracles, or zero-knowledge proofs for its target operating envelope. The protocol supports both simple convex parcels and complex concave (multi-part) parcels through convex decomposition, enabling realistic land property geometries.

Primary use cases: Land registries, property ownership systems, territorial claims in autonomous worlds, and any application requiring exclusive spatial rights.


Table of Contents

1.  Notation and Conventions
2. System Model
2.1 Blockchain Layer
2.2 Operating Envelope
2.3 Cadastral Design Philosophy
3. Data Structures
3.1 Fixed-Point Coordinate Representation
3.2 Parcel Object (Multi-Part Support)
3.3 Index and Config
3.4 Capability Objects
3.5 Events
4. Algorithms and Lifecycle
4.1 AABB Computation
4.2 Quadtree Cell Mapping
4.3 Parcel Registration Pipeline
4.4 Overlap Semantics (Touching vs. Overlap)
4.5 Parcel Deletion
4.6 Ownership Transfer
4.7 Parcel Mutations
4.8 IPFS Metadata
5. Computational Complexity Analysis
5.1 Instruction Cost Model
5.2 Empirical Cost Bounds
6. Correctness and Security
6.1 SAT Correctness and Edge Cases
6.2 Quadtree Index Completeness
6.3 Error Handling and Invariant Breaches
Appendix A: Market V2 Mechanics and Cascading Tax
Appendix B: Evidence and Measurements
Appendix C: Roadmap and Future Extensions
Appendix D: Change Log

1. Notation and Conventions

SymbolDefinition
Z+\mathbb{Z}^+Positive integers
P\mathcal{P}A parcel, composed of one or more convex parts
V(P)V(\mathcal{P})Vertex set of a convex part of parcel P\mathcal{P}
n=V(P)n = \|V(\mathcal{P})\|Number of vertices of a part
B(P)\mathcal{B}(\mathcal{P})Axis-aligned bounding box (AABB) of P\mathcal{P}
T\mathcal{T}The hierarchical quadtree spatial index
σ\sigmaFinest-level cell size (fixed-point units)
κ\kappaFixed-point scaling factor, fixed at 10610^6
MIST\mathsf{MIST}Smallest unit of SUI: 1 SUI=109 MIST1\ \mathsf{SUI} = 10^9\ \mathsf{MIST}
CU\mathsf{CU}Computation Units in Sui gas metering
DmaxD_{max}Maximum quadtree depth, fixed at 31
premium_ppm\text{premium\_ppm}Per-parcel price multiplier in parts per million (106=1.0×10^6 = 1.0\times)

All coordinates are represented as unsigned 64-bit integers encoding fixed-point values with κ\kappa decimal precision. That is, the real-valued coordinate xRx \in \mathbb{R} is stored as x^=xκ[0,264)\hat{x} = \lfloor x \cdot \kappa \rfloor \in [0, 2^{64}).


2. System Model

2.1 Blockchain Layer

The protocol targets Sui mainnet. The relevant properties leveraged by the protocol are:

Object-centric state model: All persistent state is represented as Sui objects. The Spatial Index is a shared object, enabling concurrent multi-party access. Registered parcels are stored inside the shared index state (Index.polygons) and carry a logical owner field used by lifecycle and transfer rules.

Programmable Transaction Blocks (PTB): A single transaction may contain multiple commands executed atomically. The protocol leverages PTBs to perform insertion, spatial queries, and intersection checking atomically.

Gas metering and Storage Refunds: Sui charges for computation and storage. Because cadastral data is large, storage costs dominate registration. Deleting a parcel returns a substantial storage rebate under Sui's storage model. Storage refund mechanics are an implementation detail of the network, but they materially affect lifecycle economics.

Deterministic execution: All Move executions are deterministic. The protocol uses strict integer arithmetic for all collision detection, avoiding floating-point imprecision entirely.

2.2 Operating Envelope

The protocol is optimized for realistic cadastral scenarios. While the protocol allows parameter configuration, empirical validation on testnet yields the following descriptive recommendations for stable operation:

ParameterRecommended RangePractical Limit
Vertices per convex part4–8≤ 12
Total parts per parcel1–5≤ 10
Neighbors checked per registration3–8≤ 16
Stored quadtree cells per parcel11

Parcels fitting within this recommended envelope have measured gross registration costs in the low hundredths of SUI on testnet, with representative examples in Appendix B ranging from roughly 0.013 to 0.039 SUI depending on geometry.

2.3 Cadastral Design Philosophy

merca.earth is designed specifically for non-overlapping spatial registries where data integrity is critical.

Design Decision: Always Enforce Non-Overlapping. The protocol ALWAYS enforces non-overlapping boundaries. This is not configurable. Every parcel registration executes a full collision detection pipeline against all existing parcels in the same spatial region. If any positive-area overlap is detected, the transaction aborts.

Rationale:

  1. Strong Consistency Guarantee: It is impossible to register overlapping parcels.
  2. Gas Efficiency: Overlap checking happens exactly once (at registration time) with the cost amortized over the parcel's lifetime, rather than repeatedly checking on every query.
  3. Data Integrity: Protocol-level enforcement ensures smart contracts built on top of merca.earth never have to arbitrate boundary disputes.

Primary Use Cases:

  • Land cadastre and property registries
  • Territorial claim systems in autonomous worlds
  • Geofencing with exclusive zones (e.g., spectrum allocation, maritime borders)

(Non-goals: Generic spatial indexing for overlapping datasets like heat maps or routing networks. This protocol is strictly for exclusive spatial rights.)

3. Data Structures

The core protocol defines several fundamental structures. The state is represented strictly using the mercatr core package.

3.1 Fixed-Point Coordinate Representation

Because Move lacks native signed integers and floating-point math, all coordinates and geometry use fixed-point arithmetic.

Definition 3.1 (Fixed-Point Coordinate). A coordinate value x^Z+\hat{x} \in \mathbb{Z}^+ represents the real value x=x^/κx = \hat{x} / \kappa where κ=106\kappa = 10^6.

Signed Emulation: For intermediate SAT calculations (cross products, projections), signed values are emulated using a magnitude (u128) and a negative (bool) flag. All comparisons during the collision pipeline are deterministic exact-integer algebraic comparisons.

3.2 Parcel Object (Multi-Part Support)

The primary state object is the Polygon (representing a cadastral Parcel). The protocol supports both simple convex parcels and complex concave (multi-part) parcels through convex decomposition.

A Part represents a single convex polygon boundary:

  • Has a sequence of fixed-point xx and yy coordinates.
  • Caches a local Axis-Aligned Bounding Box (AABB).
  • Must have 3\ge 3 and Nmax\le N_{max} vertices.
  • Rejects edges shorter than the protocol minimum edge length.
  • Must be strictly convex (verified by testing cross-product signs of consecutive edges).

A Polygon (Parcel) aggregates 1 to MmaxM_{max} parts and defines the full spatial claim:

  • Caches a global AABB (union of all part AABBs).
  • Derives a single depth-prefixed Morton key mapping to the quadtree spatial index.

Formal Multipart Validity Rules: To ensure a parcel represents a contiguous, well-formed land claim, it must satisfy strict structural invariants upon registration:

  1. Interior-Disjointness: No two distinct convex parts within the same parcel may overlap in positive area.
  2. Exact Shared-Edge Connectivity: The parts must form a connected graph where adjacent parts share exactly identical edges (same vertex coordinates in reverse order).
  3. Hole-Free Boundary: The outer boundary (edges with exactly one incident part) must form a single continuous, non-intersecting cycle, meaning the parcel contains no topological holes.
  4. Compactness Floor (Anti-Sliver): To prevent malicious or accidental extreme slivers (which degrade SAT performance), the parcel must pass the implementation's boundary-perimeter compactness proxy: 8,000,0002AreaMIN_COMPACTNESS_PPMP128,000,000 \cdot 2 \cdot \text{Area} \ge \text{MIN\_COMPACTNESS\_PPM} \cdot P_1^2 where P1P_1 is the outer-boundary Manhattan perimeter, i.e. the sum of dx+dy|dx| + |dy| across all boundary edges.

3.3 Index and Config

The Index is a shared object acting as the spatial registry for a given cadastral layer.

  • cells: A map linking a depth-prefixed Morton code to a list of IDs residing in that quadtree cell.
  • polygons: A map resolving a parcel ID to its stored Polygon object.
  • config: A Config object specifying the safety limits for this index (e.g., max vertices per part, max parts per parcel, max quadtree depth).

The Index UID also serves as an attachment point for package-internal dynamic fields (e.g., per-parcel IPFS metadata), enabling feature extension without struct migration.

3.4 Capability Objects

Access control is managed via specific capability objects, isolating privileged logic from public exposure:

  • AdminCap: Minted once at deployment. Used to update configuration parameters, force-remove rogue parcels, and mint lower-level capabilities.
  • LifecycleCap: Required to register new parcels, remove existing parcels, and process standard ownership transfers. This capability is usually held by an upstream market-layer object such as MarketV2 to gate the cadastre behind economic rules.
  • TransferCap: Required to bypass normal owner authorization and force an ownership transfer. This empowers forced-buyout markets (like Market V2) to execute sales.

3.5 Events

The protocol emits deterministic events upon state changes.

Index lifecycle events:

  • Registered: Emitted when a new parcel completes the collision pipeline and is inserted. Includes the natural quadtree depth of the parcel.
  • Removed: Emitted when a parcel is deleted from the index.
  • Transferred: Emitted when parcel ownership changes.

Mutation events:

  • ParcelReshaped: Emitted when a parcel's geometry is reshaped. Includes old and new area.
  • ParcelsRepartitioned: Emitted when two adjacent parcels are repartitioned.
  • ParcelRetired: Emitted when a parcel is destroyed (split or merge absorb).
  • ParcelSplit: Emitted when a parent parcel is split into children. Includes child ID list.
  • ParcelsMerged: Emitted when two parcels are merged. Includes keep and absorbed IDs.

Metadata events:

  • MetadataSet: Emitted when IPFS metadata is set or updated on a parcel. Includes polygon_id, owner, CID string, and epoch.
  • MetadataRemoved: Emitted when IPFS metadata is removed from a parcel. Includes polygon_id and owner.

4. Algorithms and Lifecycle

The protocol manages spatial state through a rigorous set of capability-gated lifecycle operations, ensuring structural integrity and enforcing the non-overlapping constraint.

4.1 AABB Computation

An Axis-Aligned Bounding Box (AABB) is computed iteratively during Part creation by scanning the input vertex coordinates (xi,yi)(x_i, y_i) to find the minimum and maximum extremes. The union of all part-level AABBs yields the global AABB for the entire Parcel.

4.2 Quadtree Cell Mapping

Each parcel is assigned a single depth-prefixed Morton code corresponding to the smallest quadtree cell that fully encloses the parcel's global AABB. The cell size halves at each increment of depth up to DmaxD_{max}.

To map a parcel to its natural depth:

  1. The global AABB is evaluated against the spatial grid starting at D=0D = 0.
  2. The grid attempts to subdivide into 4 children. If the AABB crosses a subdivision boundary at depth dd, the parcel is pinned at depth d1d-1.
  3. The resulting Morton code is stored in the parcel's cells vector.

4.3 Parcel Registration Pipeline

The registration of a new parcel (index::register) requires a &LifecycleCap capability. During registration, the protocol executes a three-phase collision pipeline to guarantee the new claim does not overlap with any existing parcel.

Phase 1: Broadphase (Quadtree Query) The index::candidates query collects the IDs of all previously registered parcels residing in quadtree cells that intersect the new parcel's global AABB. This prunes the search space dramatically, finding only nearby neighbors.

Phase 2: Midphase (AABB Filter) For each candidate returned by the broadphase, a fast AABB intersection test (aabb::intersects) is evaluated against the new parcel's global AABB. Candidates that do not overlap at the AABB level are safely skipped.

Phase 3: Narrowphase (Separating Axis Theorem) For all remaining midphase survivors, full multi-part SAT collision detection is executed (polygon::intersects). The protocol tests every part of the new parcel against every part of the candidate parcel.

  • If ANY positive-area overlap is detected between any two parts, the entire transaction immediately aborts with EOverlap.
  • If all tests pass, the parcel is inserted into index.polygons and its ID is added to its natural cell in index.cells.

4.4 Overlap Semantics: Touching vs. Overlap

The Separating Axis Theorem (SAT) implementation strictly differentiates between touching and overlapping.

  • Overlap: Occurs when two polygons intersect and share positive area. The protocol forbids this and will abort.
  • Touching: Occurs when two polygons share exactly a boundary edge or a single corner vertex, but their interiors do not intersect (zero shared area). This is permitted.

Because the protocol leverages exact fixed-point integer arithmetic (u128 projections and strict > inequality checks), there is no numerical tolerance (ϵ\epsilon) or floating-point imprecision. Boundary touching evaluates cleanly and deterministically as non-overlapping.

4.5 Parcel Deletion

Removing a parcel (index::remove) requires a &LifecycleCap. The operation requires the ID of the parcel. The protocol:

  1. Looks up the parcel in index.polygons and verifies ownership via the current transaction sender context.
  2. Removes the ID from the specific quadtree cell in index.cells.
  3. Removes the stored parcel record from index.polygons and destroys the underlying Polygon value.

Note on storage: Because Sui returns a storage rebate on deletion, removing a parcel reduces long-run storage cost, but the exact refund amount is determined by the network's storage accounting rather than by the protocol itself.

(An administrative bypass, admin::force_remove, requires the AdminCap and invokes the package-internal index::remove_unchecked to forcefully delete any parcel without requiring the owner's signature or LifecycleCap.)

4.6 Ownership Transfer

Transferring ownership of a parcel (index::transfer_ownership) requires a &LifecycleCap and the owner's signature.

However, forced-sale protocols (such as Market V2) utilize a secondary capability, &TransferCap, which empowers the index::force_transfer function. This allows the market layer to bypass the owner's signature entirely and reassign the parcel to a new buyer upon a successful buyout.

4.7 Parcel Mutations

The protocol supports four geometry-mutating operations via the mutations module. All require a &LifecycleCap and enforce strict area conservation and non-overlap invariants.

4.7.1 Reshape Unclaimed (reshape_unclaimed)

Reshapes a single unclaimed parcel while preserving area. The new geometry must fully contain the old geometry (AABB containment and geometric containment). After reshaping, the quadtree cell placement is recomputed.

Invariants enforced:

  • New AABB contains old AABB.
  • New polygon geometrically contains old polygon.
  • No overlaps with other registered parcels.
  • Emits ParcelReshaped(polygon_id, old_area, new_area).

4.7.2 Repartition Adjacent (repartition_adjacent)

Repartitions two adjacent parcels that share an edge. Both parcels receive new geometry while total area is conserved: Aolda+Aoldb=Anewa+AnewbA_{old_a} + A_{old_b} = A_{new_a} + A_{new_b}.

Invariants enforced:

  • Parcels share at least one edge (touches_by_edge).
  • Total area conserved (exact fixed-point equality).
  • New parcels do not overlap each other.
  • No overlaps with other registered parcels.
  • Emits ParcelsRepartitioned(a_id, b_id).

4.7.3 Split Replace (split_replace)

Splits a parent parcel into NN child parcels. The parent is retired and replaced by the children, who inherit the parent's owner.

Invariants enforced:

  • Parent area equals sum of children areas (area conservation).
  • Children do not overlap each other.
  • No overlaps with other registered parcels.
  • Emits ParcelRetired(parent_id) and ParcelSplit(parent_id, child_ids).

4.7.4 Merge Keep (merge_keep)

Merges two adjacent parcels owned by the same address. One parcel (keep) receives the merged geometry; the other (absorb) is retired.

Invariants enforced:

  • Both parcels have the same owner.
  • Parcels share at least one edge.
  • Area conserved: Akeepold+Aabsorb=AkeepnewA_{keep_{old}} + A_{absorb} = A_{keep_{new}}.
  • No overlaps with other registered parcels.
  • Emits ParcelRetired(absorb_id) and ParcelsMerged(keep_id, absorb_id).

4.8 Off-Chain Metadata

The protocol supports attaching an off-chain content identifier (CID, Walrus URI, or other content-addressable reference) to any registered parcel. This enables owners to associate rich off-chain data — property descriptions, imagery, legal documents — with their on-chain parcels without incurring on-chain storage costs for the content itself.

Storage Design: Metadata is stored as a Sui dynamic field on the Index object's UID, keyed by a MetadataKey { polygon_id } struct. This design avoids any migration of existing parcel data and requires no changes to the Polygon struct. Each parcel has at most one metadata entry.

Data Model: A MetadataState record contains:

  • cid: String — the IPFS content identifier, stored as-given without on-chain validation.
  • updated_epoch: u64 — the Sui epoch at which the metadata was last set, providing a freshness signal to off-chain consumers.

Operations:

  • set_metadata(index, polygon_id, cid, ctx): Owner-gated. Sets or overwrites the CID for a parcel. Idempotent — calling twice replaces the previous value via a check-remove-add pattern on the dynamic field. Emits MetadataSet. Cost: gas only (no protocol fee).

  • get_metadata(index, polygon_id): Public read. Returns (String, u64) — the CID and updated epoch. Aborts with EMetadataNotFound if no metadata has been set.

  • has_metadata(index, polygon_id): Public existence check. Returns bool.

  • remove_metadata(index, polygon_id, ctx): Owner-gated. Removes the metadata dynamic field. Aborts with EMetadataNotFound if no metadata exists. Emits MetadataRemoved.

Ownership Enforcement: All mutating operations (set_metadata, remove_metadata) verify that the transaction sender matches polygon::owner(index::get(index, polygon_id)). Non-owners are rejected with ENotOwner (6000). Ownership transfers propagate automatically — after a transfer, only the new owner can modify metadata.

Composability: Metadata operations are PTB-composable. A parcel can be registered and its metadata set in a single Programmable Transaction Block by composing index::registermetadata::set_metadata.

Orphaned Metadata: If a parcel is removed from the index while metadata is still attached, the dynamic field persists as an orphan on the Index UID. Callers should invoke remove_metadata before parcel deletion in a PTB to reclaim storage. This is a known limitation; automated cleanup is deferred to future work.

Off-Chain Content Schema: The protocol defines a canonical JSON schema for off-chain metadata content, documented in metadata-schema.md. A conforming document may carry a toponym (name), a reference URI to an SVG visual (svg), a geographic label coordinate (label), an array of points of interest (pois), and a CommonMark description field. Only the schema_version field is required; all others are optional. Content-addressable storage networks such as Walrus (via walrus:// URIs) or IPFS (via ipfs:// URIs) are the recommended storage backends. The on-chain cid field stores any valid URI without scheme validation.


5. Computational Complexity Analysis

Because the protocol operates entirely on-chain, bounding the computational and storage cost of registration is critical for economic viability.

5.1 Instruction Cost Model

The registration pipeline's cost is dominated by the narrowphase collision detection. For a new parcel PP with mm parts, executing against CC AABB-confirmed candidate neighbors, the worst-case number of SAT projection tests is: i=1Cj=1mk=1pi(vPj+vCik)\sum_{i=1}^{C} \sum_{j=1}^{m} \sum_{k=1}^{p_i} (v_{P_j} + v_{C_{i_k}}) where pip_i is the number of parts in neighbor CiC_i, and vv represents the vertex count of a given part.

Because broadphase and midphase cull non-local candidates effectively, CC stays small even as the total number of registered parcels grows to the hundreds of thousands.

5.2 Empirical Cost Bounds

Rather than relying on unproven asymptotic bounds, the protocol's viability is secured by conservative, measured empirical claims derived directly from Sui testnet execution.

  1. Storage Dominance: Storage mapping accounts for ~93-96% of the gross registration fee. Computation costs remain relatively flat across standard cadastral geometries.
  2. Gross Cost Limits: Representative cadastral shapes measured on testnet fall between roughly 0.013 SUI and 0.039 SUI gross, depending on geometry and spatial footprint.
  3. Lifecycle Economics: Storage rebates materially reduce long-run lifecycle cost, but the exact net cost depends on network-level rebate accounting and should be treated as deployment-dependent.

(See Appendix B for specific testnet data and detailed test scenarios).


6. Correctness and Security

The protocol substitutes complex mathematical proofs with rigorous invariant enforcement, ensuring logical correctness through exhaustive test coverage.

6.1 SAT Correctness and Edge Cases

The Separating Axis Theorem requires absolute convexity for both polygons tested. The protocol enforces this invariant deterministically during Part instantiation by verifying that the cross product of all consecutive edge pairs shares the same sign.

Edge Cases Validated:

  • Zero-overlap touching: Exact edge-on-edge contact without interior penetration correctly evaluates to false (no overlap).
  • Collinear vertices: Redundant vertices on the same edge are structurally valid but do not break the projection vectors.
  • Micro-overlaps: Intersections as small as a single fixed-point unit (10610^{-6}) are deterministically caught and rejected.

6.2 Quadtree Index Completeness

The broadphase query is designed so it cannot yield false negatives. Because a parcel is pinned to the quadtree depth dd corresponding to the single cell that fully encloses its global AABB, any query spanning an overlapping region is mathematically guaranteed to intersect that cell at depth dd or one of its ancestors/descendants. Exhaustive grid-boundary unit tests guarantee no geometric orphans are left behind.

6.3 Error Handling and Invariant Breaches

If a transaction violates any protocol invariant, it aborts cleanly, rolling back all state changes without side effects. The protocol exposes a stable table of error codes.

Core protocol errors (index module):

CodeConstantModuleBreach / Condition
4001EBadVerticesindexArray <3< 3 vertices, or exceeds NmaxN_{max} limit
4002EMismatchindexxs and ys arrays have different lengths
4005ENotFoundindexQueried parcel ID does not exist
4006ENotOwnerindexSender does not own the parcel
4009EBadMaxDepthindexQuadtree max depth exceeds Dmax=31D_{max} = 31
4010ETooManyPartsindexNumber of parts exceeds MmaxM_{max}
4012EOverlapindexCore Constraint: Parcel overlaps an existing claim
4013EIndexNotEmptyindexAdmin attempted to destroy an index that still holds parcels
4014EBadCellSizeindexσ\sigma configured as zero or invalid
4015ENotAuthorizedindexMissing or invalid LifecycleCap
4016ECoordinateTooLargeindexCoordinate exceeds representable grid range

Geometry errors (polygon module):

CodeConstantModuleBreach / Condition
2001EEmptypolygonEmpty vertex or part input
2002ETooManyPartspolygonNumber of parts exceeds MmaxM_{max}
2003ENotConvexpolygonPart fails strict convexity check
2004EBadVerticespolygonVertex count out of range
2005EMismatchpolygonxs and ys arrays have different lengths
2006EPartOverlappolygonInterior disjointness violated within the same parcel
2007EInvalidMultipartContactpolygonParts share area but not via exact identical edges
2008EDisconnectedMultipartpolygonParts do not form a single connected graph
2009EInvalidBoundarypolygonOuter boundary contains a topological hole
2010EEdgeTooShortpolygonOne or more parcel edges are shorter than the minimum allowed length
2011ECompactnessTooLowpolygonArea-to-perimeter ratio is below the anti-sliver threshold
2012EAreaConservationViolationpolygonArea before and after mutation does not match

Mutation errors (mutations module):

CodeConstantModuleBreach / Condition
5001ENotContainedmutationsNew geometry does not contain old geometry (reshape)
5002EOverlapmutationsMutated parcel overlaps another registered parcel
5003ESelfRepartitionmutationsCannot repartition a parcel with itself
5004ENotAdjacentmutationsParcels do not share an edge (repartition/merge)
5005EOwnerMismatchmutationsParcels have different owners (merge)
5006ESelfMergemutationsCannot merge a parcel with itself

Metadata errors (metadata module):

CodeConstantModuleBreach / Condition
6000ENotOwnermetadataCaller is not the parcel owner
6001EMetadataNotFoundmetadataNo metadata exists for the queried parcel

Market V2 errors (market_v2 module):

CodeConstantModuleBreach / Condition
3100EInvalidPricemarket_v2Computed price is zero or invalid
3101EInvalidAreaRangemarket_v2Level min area exceeds max area
3102EDuplicateLevelmarket_v2Level already registered for this index
3103EInvalidRatemarket_v2Escalation rate out of bounds
3104EInvalidFeemarket_v2Protocol fee out of bounds
3105EUnknownIndexmarket_v2No level configured for this index
3106ESelfPurchasemarket_v2Buyer is already the parcel owner
3107EAreaOutOfRangemarket_v2Parcel area outside level bounds
3108EZeroAreaParcelmarket_v2Parcel has zero computed area
3109EInsufficientPaymentmarket_v2Payment coin value below required price
3110ENotOwnermarket_v2Caller does not own the parcel
3111ENotRegisteredmarket_v2Parcel not tracked by market
3112EZeroAreaSlicemarket_v2Slice operation captures zero area
3113ESameOwnerSlicemarket_v2Acquire slice requires different owners
3114EDifferentOwnersmarket_v2Rebalance/merge requires same owner

Market V2 tax errors:

CodeConstantModuleBreach / Condition
3200ETaxNotInitializedmarket_v2Tax system not yet initialized
3201EBatchTooLargemarket_v2Collect batch exceeds MAX_COLLECT_BATCH_SIZE
3202EWrongLevelmarket_v2Parent/child levels are not adjacent
3203EBucketNotExpiredmarket_v2Sweep attempted before bucket expiry
3204ETaxNotOwnermarket_v2Caller does not own the polygon for withdrawal
3205ENoSelfClaimablemarket_v2No tax balance available for withdrawal
3206EAlreadyInitializedmarket_v2Tax system already initialized
3207EInvalidTaxConfigmarket_v2Tax level configuration out of bounds
3208EUnknownLevelmarket_v2No tax level configured for this rank

Appendix A: Market V2 Mechanics

The protocol supports a layered architectural model where the core cadastre is governed by an upstream economic framework. The canonical Market V2 implementation enforces an escalating, forced-sale continuous auction and a cascading, bucketed upstream tax system implemented entirely on-chain.

Forced Buyout Lifecycle:

  1. A user targets an existing parcel and pays the current algorithmic buyout price.
  2. The Market contract splits the payment into 85% seller proceeds, 7% immediate treasury, and 8% deferred hierarchy pool.
  3. The hierarchy pool is deposited into the cascading tax system if tax is initialized and the sold level has a parent; otherwise it is routed directly to treasury.
  4. Any previously accumulated self_claimable tax for the seller polygon is auto-withdrawn to the seller before ownership changes.
  5. The Market contract consumes its TransferCap to invoke index::force_transfer, reassigning ownership to the buyer.
  6. The polygon's premium_ppm is advanced using the aggressive resale ladder, and sale_count is incremented.

Price Escalation Formula: The market tracks a per-parcel premium_ppm (parts per million) together with sale_count. Level tariffs are stored as price_per_km2_mist. The current buyout price is: P=Aream2×BasePricem2×premium_ppm106P = \text{Area}_{m^2} \times \text{BasePrice}_{m^2} \times \frac{\text{premium\_ppm}}{10^6}

where:

  • BasePricem2=price_per_km2_mist/106\text{BasePrice}_{m^2} = \text{price\_per\_km2\_mist} / 10^6
  • premium_ppm is initialized to 10610^6 (1.0×) at registration
  • sale_count starts at 0

Each buy_full escalates the premium via a hardcoded aggressive resale ladder:

premium_ppm=min(MAX_PREMIUM_PPM,  premium_ppm×ladder(n)106)\text{premium\_ppm}' = \min\left(\text{MAX\_PREMIUM\_PPM},\; \text{premium\_ppm} \times \frac{\text{ladder}(n)}{10^6}\right)

where n=sale_count+1n = \text{sale\_count} + 1, ladder(1..64) is the fixed RESALE_LADDER_PPM vector, and ladder(n \ge 65) = 1{,}150{,}000 (1.15× tail).

The premium_ppm model extends naturally to partial operations:

  • acquire_slice: Price is based on the captured donor area at the donor's current premium. Both receiver and donor keep their existing premium_ppm.
  • rebalance_slice: Both polygons adopt max(premiuma,premiumb)\max(\text{premium}_a, \text{premium}_b) and max(sale_counta,sale_countb)\max(\text{sale\_count}_a, \text{sale\_count}_b).
  • merge_owned: The surviving polygon adopts the area-weighted average premium of both inputs and max(sale_counta,sale_countb)\max(\text{sale\_count}_a, \text{sale\_count}_b).
  • expand_unclaimed: Premium is unchanged; the buyer pays only for the added area at the current premium.

All price and split calculations use u128 integer arithmetic internally before truncating back to u64.

A.1 Deferred Tax State

The production Market V2 implementation attaches a dynamic TaxStore to the shared MarketV2 object rather than storing tax state directly in the MarketV2 struct. This preserves Sui upgrade compatibility while keeping the full tax lifecycle on-chain.

The runtime tax state includes:

  • tax_fund: Balance<SUI> — pooled tax balance backing all deferred claims.
  • version_counters: Table<ID, u64> — per-polygon geometry version used to separate old and new tax buckets after reshapes and repartitions.
  • tax_buckets: Table<TaxBucketKey, TaxBucket> — per-polygon bucketed tax accounting.
  • tax_levels: vector<TaxLevelCfg> — per-level timing configuration for bucket cadence and expiry.
  • self_claimable: Table<ID, u64> — tax already allocated to a polygon owner but not yet withdrawn.
  • polygon_buckets: Table<ID, vector<TaxBucketKey>> — reverse index used for sweep and cleanup.

Each TaxBucketKey is the canonical bucket identity (6 fields):

  • polygon_id: ID — the polygon this bucket sits on.
  • version: u64 — polygon geometry version at accrual time.
  • bucket_id: u64 — epoch-window bucket (current_epoch / bucket_epochs).
  • origin_level_rank: u8 — level rank of the originating sale (e.g., 0 for block-level).
  • ancestor_distance: u8 — cascade step. 1 = initial direct-parent bucket created by tax_accrue; 2+ = forwarded cascade bucket created when a parent collects and forwards upstream.
  • accrual_epoch: u64 — epoch when the underlying tax first originated. Inherited through cascades, never reset. Prevents merging buckets from different origination times — two forwarded amounts landing on the same polygon in the same epoch window but originating at different epochs stay separate. This is critical for the anti-retro guard (§A.4).

Each TaxBucket stores:

  • upstream_total: u64 — total MIST deposited into this bucket.
  • total_claimed: u64 — MIST already claimed (reserved for partial claims; currently 0 in production).
  • open_epoch: u64 — epoch when this bucket instance was opened on its current polygon.
  • accrual_epoch: u64 — mirrors the key field for data access.
  • expires_at: u64 — deterministic expiry: (b+1)×bucket_epochs+expiry_epochs(b + 1) \times \text{bucket\_epochs} + \text{expiry\_epochs} where bb is the bucket_id.

Each TaxLevelCfg stores:

  • retain_upstream_ppm
  • collector_fee_bps
  • bucket_epochs
  • expiry_epochs

In the current production configuration, only bucket_epochs and expiry_epochs are economically active. set_tax_level_cfg(...) zeroes retain_upstream_ppm and collector_fee_bps, retaining them only for layout compatibility with earlier tax iterations.

Delete-after-collect semantics replace the previous EdgeClaim anti-double-claim system: consuming a bucket removes it from both tax_buckets and polygon_buckets, making repeat collection structurally impossible without additional tracking state.

A.2 Tax Accrual on Payment-Bearing Flows

Tax is created on all payment-bearing market flows:

  • register(...) — initial registration payment
  • buy_full(...) — forced buyout payment
  • expand_unclaimed(...) — expansion payment for added area
  • acquire_slice(...) — slice acquisition payment

The runtime rule is:

  1. Compute the flow-specific payment amount from area, price_per_km2_mist, and premium_ppm.
  2. Split payment into an immediate treasury portion and a deferred hierarchy pool.
  3. If tax is initialized and the sold level has a parent, deposit the hierarchy pool into tax_fund and credit the corresponding child bucket via tax_accrue(...).
  4. Otherwise route the hierarchy pool directly to treasury.

Important implementation details:

  • For buy_full(...) and acquire_slice(...), the split is 85% seller proceeds, 7% immediate treasury, 8% deferred hierarchy pool.
  • For register(...) and expand_unclaimed(...), there is no displaced seller, so 92% goes directly to treasury and 8% forms the hierarchy pool.
  • buy_full(...), register(...), and expand_unclaimed(...) accrue against the target polygon.
  • acquire_slice(...) accrues against the donor polygon, because the donor is the polygon selling area.
  • Top-level sales/registrations (no parent above the origin rank) route the hierarchy pool directly to treasury rather than opening a bucket.
  • If the computed amount is below the minimum bucket threshold (MIN_BUCKET_AMOUNT = 1000 MIST), the amount is routed directly to treasury instead of creating a dust bucket.

A.3 Versioning and Geometry Changes

Tax collection is version-aware. Geometry-changing operations call tax_close_version(...) so that future accruals use a fresh (polygon_id, version) bucket namespace.

The current implementation closes versions on:

  • acquire_slice(...) for both receiver and donor
  • expand_unclaimed(...)
  • rebalance_slice(...) for both polygons
  • merge_owned(...) for the surviving polygon

For destructive lifecycle paths, the implementation also sweeps pending child-side tax state:

  • remove(...) calls tax_close_and_sweep(...)
  • merge_owned(...) calls tax_close_and_sweep(...) for the absorbed polygon

This ensures that removed or absorbed geometry does not leave orphaned collectible child buckets behind.

A.4 Batched Parent Collection (Cascading Model)

Deferred tax is realized through a cascading direct-parent model. Each sale creates one bucket on the sold polygon. The direct parent collects, keeps a weighted share, and forwards the remainder up the chain by opening a cascade bucket on itself for the next ancestor.

Entry points:

  • collect_batch(market, parent_index, parent_id, child_index, child_refs, ctx) — accepts pre-built ChildRef structs.
  • collect_batch_flat(market, parent_index, parent_id, child_index, child_ids, child_versions, child_bucket_ids, ctx) — accepts 3 flat vectors and constructs ChildRef internally.

Batch constraints:

  1. Bounded by MAX_COLLECT_BATCH_SIZE (20).
  2. Caller must own parent_id.
  3. Parent level must be directly above the child level.

Multi-match bucket lookup:

For each (child_id, version, bucket_id) tuple the contract finds ALL matching TaxBucketKey entries in polygon_buckets[child_id] where key.version == version, key.bucket_id == bucket_id, and (key.origin_level_rank + key.ancestor_distance) == parent_level_rank. Each match is processed independently. Previous implementations took only the first match, silently hiding duplicate buckets created by independent cascade paths landing on the same polygon in the same epoch window.

Per-bucket eligibility:

Each matched bucket must pass four checks (failure silently skips the bucket without aborting the batch):

  1. Bucket exists in tax_buckets.
  2. Anti-retro guard: parent.created_epoch <= bucket_key.accrual_epoch — the parent must not have been created in a later epoch than the tax originated. Same-epoch registration is eligible; only later-created parents are ineligible. This prevents retroactive extraction while avoiding false negatives caused by coarse epoch granularity.
  3. Child version matches current version_counters[child_id].
  4. index::outer_contains_inner(parent, child) confirms full geometric containment.

Cascading split formula:

Given a hierarchy of L=6L = 6 levels (block → district → city → region → country → continent):

r=origin_level_ranks=ancestor_distanceN=max(0,  L1r)(total ancestor count above origin)g=r(ghost levels below origin)w={4if s=11otherwise(direct parent weight)R=max(0,  Ns)(remaining ancestors above this collector)D=w+R+gself_keep=upstream_total×wDforward=upstream_totalself_keep\begin{aligned} r &= \text{origin\_level\_rank} \\ s &= \text{ancestor\_distance} \\ N &= \max(0,\; L - 1 - r) & \text{(total ancestor count above origin)} \\ g &= r & \text{(ghost levels below origin)} \\ w &= \begin{cases} 4 & \text{if } s = 1 \\ 1 & \text{otherwise} \end{cases} & \text{(direct parent weight)} \\ R &= \max(0,\; N - s) & \text{(remaining ancestors above this collector)} \\ D &= w + R + g \\[6pt] \text{self\_keep} &= \left\lfloor \frac{\text{upstream\_total} \times w}{D} \right\rfloor \\ \text{forward} &= \text{upstream\_total} - \text{self\_keep} \end{aligned}

The 4× weight for s = 1 ensures the direct parent receives a significantly larger share than pass-through ancestors. Ghost levels represent hierarchy ranks below the originating sale that cannot participate, diluting each participant's share proportionally.

Delete-after-collect: The consumed bucket is removed from both tax_buckets and polygon_buckets. The bucket itself is the claim token — no EdgeClaim cursor is needed.

Cascade forwarding: If forward>0\text{forward} > 0 and R>0R > 0 and forwardMIN_BUCKET_AMOUNT\text{forward} \ge \text{MIN\_BUCKET\_AMOUNT}:

  • A cascade bucket is opened on the parent polygon with ancestor_distance = s + 1, inheriting origin_level_rank and accrual_epoch from the source bucket.
  • If a cascade bucket with the same key already exists, the forward amount is merged into it.
  • open_epoch is set to the current epoch; accrual_epoch is inherited (never reset).
  • Emits TaxBucketOpened for newly created cascade buckets.

Terminal forwarding: If forward>0\text{forward} > 0 but R=0R = 0 (top level reached) or forward<MIN_BUCKET_AMOUNT\text{forward} < \text{MIN\_BUCKET\_AMOUNT} (dust), the forward amount is routed directly to treasury.

Credits: self_keep is added to self_claimable[parent_id]. Emits TaxCollected per bucket processed.

A.5 Withdrawal and Expiry Sweep

The implementation exposes two terminal paths for deferred balances:

  • withdraw_tax(market, index, polygon_id, ctx)

    • Callable only by the current polygon owner.
    • Withdraws the polygon's self_claimable balance from tax_fund.
    • Emits TaxWithdrawn.
  • sweep_expired(market, polygon_id, version, bucket_id, ctx)

    • Permissionless.
    • Requires current_epoch >= expires_at.
    • Finds ALL bucket keys matching (polygon_id, version, bucket_id) and sweeps every match — not just the first. Multiple keys can share the same 3-tuple when independent cascade paths or different accrual epochs produce distinct buckets.
    • Moves the unclaimed remainder of each bucket from tax_fund to treasury.
    • Removes each bucket from tax_buckets and prunes polygon_buckets.
    • Emits TaxSwept per bucket swept.

This gives the market a complete lifecycle from accrual to parent collection to owner withdrawal or treasury sweep.

A.6 Market V2 Lifecycle Operations

Beyond buy_full, the Market V2 exposes a full set of area-mutation operations that compose the core mutations module with economic rules.

register(market, index, parts_xs, parts_ys, payment, ctx) Registers a new parcel. Charges area_m2 × price_per_km2_mist / 10^6 at the initial premium (1.0×). Creates PriceState { premium_ppm = 1_000_000, sale_count = 0 }. Splits payment into 92% immediate treasury and 8% hierarchy pool. If tax is initialized and the level has a parent, the hierarchy pool accrues into deferred tax.

expand_unclaimed(market, index, polygon_id, new_parts_xs, new_parts_ys, payment, ctx) Reshapes an owned parcel to a larger geometry via mutations::reshape_unclaimed. Charges only for the added area: (new_area - old_area) × price_per_km2_mist / 10^6 × premium_ppm / 10^6. Premium is unchanged. Splits payment into 92% immediate treasury and 8% hierarchy pool. Closes the tax version after expansion.

acquire_slice(market, receiver_index, receiver_id, donor_index, donor_id, receiver_xs, receiver_ys, donor_xs, donor_ys, payment, ctx) Transfers area from a donor parcel (different owner) to a receiver parcel via mutations::repartition_adjacent. The receiver must be owned by the caller; the donor must not be. Charges by captured donor area at the donor's premium. Splits payment into 85% donor proceeds, 7% immediate treasury, and 8% hierarchy pool. Both receiver and donor keep their prior premium_ppm; tax accrues against the donor. Closes versions on both parcels.

rebalance_slice(market, index, a_id, b_id, a_xs, a_ys, b_xs, b_ys, ctx) Adjusts area between two parcels owned by the same caller via mutations::repartition_adjacent. No payment is required. Both polygons adopt max(premium_a, premium_b) and max(sale_count_a, sale_count_b). Closes versions on both parcels.

merge_owned(market, index, keep_id, absorb_id, merged_xs, merged_ys, ctx) Merges two adjacent same-owner parcels via mutations::merge_keep. The survivor's premium becomes the area-weighted average of the two premiums; sale_count becomes the max of both inputs. Sweeps all tax buckets from the absorbed polygon. Closes version on the survivor.

remove(market, index, polygon_id, ctx) Governance/emergency-only removal path. Removes PriceState, sweeps all remaining tax buckets to treasury via tax_close_and_sweep, and calls index::remove through the market's internal LifecycleCap. This is not a public owner-facing deregistration flow.

A.7 Market V2 Events

Market lifecycle events:

  • MarketV2Registered: Emitted on register. Includes polygon_id, owner, area_m2, price_paid, immediate_treasury, hierarchy_pool.
  • MarketV2PurchasedFull: Emitted on buy_full. Includes polygon_id, buyer, seller, price, new_premium_ppm, seller_proceeds, immediate_treasury_fee, hierarchy_pool, sale_count.
  • ParcelExpandedUnclaimed: Emitted on expand_unclaimed. Includes polygon_id, old_area, new_area, price_paid, immediate_treasury, hierarchy_pool.
  • ParcelSliceAcquired: Emitted on acquire_slice. Includes receiver_id, donor_id.
  • ParcelSliceRebalanced: Emitted on rebalance_slice. Includes a_id, b_id.
  • ParcelOwnedMerged: Emitted on merge_owned. Includes keep_id, absorbed_id, premium_ppm.
  • PriceStateUpdated: Emitted whenever a polygon's premium_ppm changes.

Tax events:

  • TaxBucketOpened { polygon_id, version, bucket_id, origin_level_rank, ancestor_distance, accrual_epoch, amount } — emitted when a new bucket is created (initial accrual or cascade forwarding). Not emitted when an existing bucket receives additional funds.
  • TaxCollected { parent_id, child_id, child_version, bucket_id, origin_level_rank, ancestor_distance, accrual_epoch, amount, collector_fee, self_keep, forward_upstream } — emitted per bucket collected. One collect_batch call may emit multiple events when processing several child buckets.
  • TaxWithdrawn { polygon_id, owner, amount } — emitted when a polygon owner withdraws self_claimable balance.
  • TaxSwept { polygon_id, version, bucket_id, origin_level_rank, ancestor_distance, accrual_epoch, remainder } — emitted per expired bucket swept to treasury.
  • TaxVersionClosed { polygon_id, old_version, new_version } — emitted when a polygon's tax version is incremented due to geometry change.

The origin_level_rank, ancestor_distance, and accrual_epoch fields together form the full bucket identity and enable client-side event matching without ambiguity across cascade paths.

These events form the complete auditable surface for market and tax accounting.

A.8 Current Architectural Note

Historically, tax functionality was planned as a separate tax.move implementation module. In the current production code, runtime tax logic and tax data structures live in market_v2.move, while market/sources/tax.move is retained as a compatibility stub. This is an implementation-architecture detail and does not change the normative tax semantics described above.

Harberger-like variants and other alternative tax research remain outside the production V2 specification.


Appendix B: Evidence and Measurements

The protocol's operating envelope and cost models are derived from empirical deployments on the Sui testnet.

Testnet Gas Profiles (Observed):

Geometry TypeVerticesPartsBroadphase NeighborsGross Cost (MIST)
Simple Square410~13.3M
Hexagon612~17.6M
L-Shape824~30.4M
Complex Stepped1558~32.0M

Coverage Evidence: The protocol's collision logic is verified by unit integration tests, validating edge cases such as:

  • Dense Urban Grids: 25 parcels packed into a 5×55 \times 5 configuration without missing overlaps.
  • Broadphase Completeness: Stress-testing queries to ensure no geometry fragments bypass midphase filtering.

Appendix C: Roadmap and Future Extensions

Features that fall outside the immediate scope of the V2 production release but remain viable architectural expansions:

  • ZK Extension Layer: Pushing SAT narrowphase checking to an off-chain prover to support massive boundary files (e.g., >50>50 vertices) while retaining on-chain grid indexing.
  • Dynamic Grid Resizing: Allowing the root cell size (σ\sigma) to expand or subdivide autonomously based on localized parcel density.
  • 3D / Volumetric Indexing: Upgrading the 2D AABB and quadtree into an Octree model to support airspace and subterranean mineral rights.

Appendix D: Change Log

  • v0.8.0 (Current): Added §4.8 IPFS Metadata — owner-gated off-chain content identifier (CID) attachment via Sui dynamic fields on the Index UID. New metadata.move module with set_metadata, get_metadata, has_metadata, remove_metadata. Metadata errors allocated in the 6000 range. Events: MetadataSet, MetadataRemoved. No migration required for existing parcels.
  • v0.7.0: Replaced the pre-split ancestor bucket tax model with cascading direct-parent collection. Each sale creates one deferred bucket; parent collects, keeps weighted share, and forwards the remainder up chain via cascade buckets. Added accrual_epoch to TaxBucketKey to prevent merging buckets from different origination times. Anti-retro eligibility now requires parent.created_epoch <= bucket.accrual_epoch, so later-created parents are blocked while same-epoch parents remain eligible. collect_batch processes all matching buckets per child ref (multi-match), and delete-after-collect replaces the old EdgeClaim idempotency cursor. Appendix A also reflects the deployed price_per_km2_mist tariff model, aggressive resale ladder, fixed 85/7/8 secondary split, and 92/8 primary-expansion split.
  • v0.6.0: Added §4.7 Parcel Mutations (reshape, repartition, split, merge). Replaced legacy sale_count price formula with canonical premium_ppm model including partial escalation semantics. Documented all payment-bearing tax accrual points (register, buy_full, expand_unclaimed, acquire_slice). Added Market V2 lifecycle operations (§A.6) and full event catalogue (§A.7). Expanded error code tables to cover polygon, mutations, and market_v2 modules. Fixed EBadMaxDepth limit from 20 to 31. Added mutation events to §3.5.
  • v0.5.0: Reframed as merca.earth Cadastre Protocol. Upgraded concave/multi-part support from "future" to "core". Replaced theoretical cost formulas with conservative testnet-driven evidence. Isolated Market V2 mechanics from the spatial core spec. Formally defined touching vs. overlap semantics and strict polygon compactness invariants.
  • v0.1.0: Initial research draft (formerly "GeoSui"). Proposed basic grid + SAT integration and outlined theoretical broadphase gas costs.