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. For a user-friendly introduction, see How It Works → Two Layers and Key Concepts → Parcel.
1. Notation and Conventions
| Symbol | Definition |
|---|---|
| Positive integers | |
| A parcel, composed of one or more convex parts | |
| Vertex set of a convex part of parcel | |
| Number of vertices of a part | |
| Axis-aligned bounding box (AABB) of | |
| The hierarchical quadtree spatial index | |
| Finest-level cell size (fixed-point units) | |
| Fixed-point scaling factor, fixed at | |
| Smallest unit of SUI: | |
| Computation Units in Sui gas metering | |
| Maximum quadtree depth, fixed at 31 | |
| Per-parcel price multiplier in parts per million () |
All coordinates are represented as unsigned 64-bit integers encoding fixed-point values with decimal precision. That is, the real-valued coordinate is stored as .
Coordinate Bounds. The protocol enforces a hard upper bound on both axes, corresponding to one Earth circumference in Web Mercator at precision ( where ). Any vertex with or is rejected with ECoordinateOutOfWorld. This ensures a bijective mapping between geographic coordinates and protocol coordinates, preventing duplicate registration of the same physical location at wrapped coordinate offsets.
App-Side Projection. The client application uses Web Mercator (EPSG:3857) to convert geographic longitude/latitude to protocol coordinates. Latitude is clamped to (the standard Mercator square-world bound). The antimeridian ( longitude) is the coordinate-space boundary; polygons crossing it are rejected at the application layer. Full antimeridian-crossing support (automatic polygon splitting) is planned for a future protocol version.
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:
| Parameter | Recommended Range | Practical Limit |
|---|---|---|
| Vertices per convex part | 4–8 | ≤ 12 |
| Total parts per parcel | 1–5 | ≤ 10 |
| Neighbors checked per registration | 3–8 | ≤ 16 |
| Stored quadtree cells per parcel | 1 | 1 |
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:
- Strong Consistency Guarantee: It is impossible to register overlapping parcels.
- 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.
- 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 represents the real value where .
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 and coordinates.
- Caches a local Axis-Aligned Bounding Box (AABB).
- Must have and vertices.
- Rejects edges shorter than the protocol minimum edge length.
- Must be convex (verified by testing cross-product signs of consecutive edges). Collinear vertices (zero cross product) are permitted — the check enforces weak convexity, not strict.
A Polygon (Parcel) aggregates 1 to 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:
- Interior-Disjointness: No two distinct convex parts within the same parcel may overlap in positive area.
- Exact Shared-Edge Connectivity: The parts must form a connected graph where adjacent parts share exactly identical edges (same vertex coordinates in reverse order). The graph may have any topology (linear chain, fork, tree, star) as long as every part is reachable from every other part via shared edges. A single part may share edges with two or more neighbours, enabling branching shapes such as T- or Y-junctions.
- 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.
- 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: where is the outer-boundary Manhattan perimeter, i.e. the sum of 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 ofIDs residing in that quadtree cell.polygons: A map resolving a parcelIDto its storedPolygonobject.cell_size,max_depth, andcount: Immutable or monotone index-level parameters controlling grid resolution and total parcel count.occupied_depths: Au32bitmask recording which quadtree depths are populated, used by broadphase probing.config: AConfigobject specifying safety limits for this index (e.g., max vertices per part, max parts per parcel, broadphase probe budget, and per-cell occupancy cap).authorized_caps: AVecSet<ID>of capability objects currently permitted to act on this index.authorization_sealed: A boolean lock that disables further cap minting and authorization except during explicit admin rotation flows.
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 Market Wrapper and Price State
The market layer is not stored as a flat shared struct. The shared object is Market { id, paused, inner: Versioned }, where the mutable economic state lives inside MarketInner.
MarketInner stores:
versiontransfer_capandlifecycle_captreasury: Balance<SUI>price_states: Table<ID, PriceState>levels: vector<Level>
Per-parcel pricing is represented by PriceState { premium_ppm, sale_count }. premium_ppm is the multiplicative price premium in parts per million, and sale_count records completed premium-ladder steps already absorbed into the parcel's current state.
3.5 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 asMarketto 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.6 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 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 .
To map a parcel to its natural depth:
- The global AABB is evaluated against the spatial grid starting at .
- The grid attempts to subdivide into 4 children. If the AABB crosses a subdivision boundary at depth , the parcel is pinned at depth .
- The resulting Morton code is stored in the parcel's
cellsvector.
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.polygonsand itsIDis added to its natural cell inindex.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 () 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:
- Looks up the parcel in
index.polygonsand verifies ownership via the current transaction sender context. - Removes the
IDfrom the specific quadtree cell inindex.cells. - Removes the stored parcel record from
index.polygonsand destroys the underlyingPolygonvalue.
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: .
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 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)andParcelSplit(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: .
- No overlaps with other registered parcels.
- Emits
ParcelRetired(absorb_id)andParcelsMerged(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. EmitsMetadataSet. Cost: gas only (no protocol fee). -
get_metadata(index, polygon_id): Public read. Returns(String, u64)— the CID and updated epoch. Aborts withEMetadataNotFoundif no metadata has been set. -
has_metadata(index, polygon_id): Public existence check. Returnsbool. -
remove_metadata(index, polygon_id, ctx): Owner-gated. Removes the metadata dynamic field. Aborts withEMetadataNotFoundif no metadata exists. EmitsMetadataRemoved.
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::register → metadata::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 CommonMark description field, and an extensible properties map. 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 with parts, executing against AABB-confirmed candidate neighbors, the worst-case number of SAT projection tests is: where is the number of parts in neighbor , and represents the vertex count of a given part.
Because broadphase and midphase cull non-local candidates effectively, 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.
- Storage Dominance: Storage mapping accounts for ~93-96% of the gross registration fee. Computation costs remain relatively flat across standard cadastral geometries.
- 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.
- 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 () 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 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 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):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 4001 | EBadVertices | index | Array vertices, or exceeds limit |
| 4002 | EMismatch | index | xs and ys arrays have different lengths |
| 4005 | ENotFound | index | Queried parcel ID does not exist |
| 4006 | ENotOwner | index | Sender does not own the parcel |
| 4009 | EBadMaxDepth | index | Quadtree max depth exceeds |
| 4010 | ETooManyParts | index | Number of parts exceeds |
| 4012 | EOverlap | index | Core Constraint: Parcel overlaps an existing claim |
| 4013 | EIndexNotEmpty | index | Admin attempted to destroy an index that still holds parcels |
| 4014 | EBadCellSize | index | configured as zero or invalid |
| 4015 | ENotAuthorized | index | Missing or invalid LifecycleCap |
| 4016 | ECoordinateTooLarge | index | Coordinate exceeds representable grid range |
Geometry errors (polygon module):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 2001 | EEmpty | polygon | Empty vertex or part input |
| 2002 | ETooManyParts | polygon | Number of parts exceeds |
| 2003 | ENotConvex | polygon | Part fails convexity check (sign flip in consecutive edge cross products) |
| 2004 | EBadVertices | polygon | Vertex count out of range |
| 2005 | EMismatch | polygon | xs and ys arrays have different lengths |
| 2006 | EPartOverlap | polygon | Interior disjointness violated within the same parcel |
| 2007 | EInvalidMultipartContact | polygon | Parts share area but not via exact identical edges |
| 2008 | EDisconnectedMultipart | polygon | Parts do not form a single connected graph |
| 2009 | EInvalidBoundary | polygon | Outer boundary contains a topological hole |
| 2010 | EEdgeTooShort | polygon | One or more parcel edges are shorter than the minimum allowed length |
| 2011 | ECompactnessTooLow | polygon | Area-to-perimeter ratio is below the anti-sliver threshold |
| 2012 | EAreaConservationViolation | polygon | Area before and after mutation does not match |
Mutation errors (mutations module):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 5001 | ENotContained | mutations | New geometry does not contain old geometry (reshape) |
| 5002 | EOverlap | mutations | Mutated parcel overlaps another registered parcel |
| 5003 | ESelfRepartition | mutations | Cannot repartition a parcel with itself |
| 5004 | ENotAdjacent | mutations | Parcels do not share an edge (repartition/merge) |
| 5005 | EOwnerMismatch | mutations | Parcels have different owners (merge) |
| 5006 | ESelfMerge | mutations | Cannot merge a parcel with itself |
Metadata errors (metadata module):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 6000 | ENotOwner | metadata | Caller is not the parcel owner |
| 6001 | EMetadataNotFound | metadata | No metadata exists for the queried parcel |
Market errors (market module):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 3100 | EInvalidPrice | market | Computed price is zero or invalid |
| 3101 | EInvalidAreaRange | market | Level min area exceeds max area |
| 3102 | EDuplicateLevel | market | Level already registered for this index |
| 3103 | EInvalidRate | market | Escalation rate out of bounds |
| 3104 | EInvalidFee | market | Protocol fee out of bounds |
| 3105 | EUnknownIndex | market | No level configured for this index |
| 3106 | ESelfPurchase | market | Buyer is already the parcel owner |
| 3107 | EAreaOutOfRange | market | Parcel area outside level bounds |
| 3108 | EZeroAreaParcel | market | Parcel has zero computed area |
| 3109 | EInsufficientPayment | market | Payment coin value below required price |
| 3110 | ENotOwner | market | Caller does not own the parcel |
| 3111 | ENotRegistered | market | Parcel not tracked by market |
| 3112 | EZeroAreaSlice | market | Slice operation captures zero area |
| 3113 | ESameOwnerSlice | market | Acquire slice requires different owners |
| 3114 | EDifferentOwners | market | Rebalance/merge requires same owner |
Market V2 tax errors:
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 3200 | ETaxNotInitialized | market | Tax system not yet initialized |
| 3201 | EBatchTooLarge | market | Collect batch exceeds MAX_COLLECT_BATCH_SIZE |
| 3202 | EWrongLevel | market | Parent/child levels are not adjacent |
| 3203 | EBucketNotExpired | market | Sweep attempted before bucket expiry |
| 3204 | ETaxNotOwner | market | Caller does not own the polygon for withdrawal |
| 3205 | ENoSelfClaimable | market | Deprecated — self_claimable field is always 0; retained for upgrade compatibility |
| 3206 | EAlreadyInitialized | market | Tax system already initialized |
| 3207 | EInvalidTaxConfig | market | Tax level configuration out of bounds |
| 3208 | EUnknownLevel | market | No tax level configured for this rank |
Appendix A: Market Mechanics
The protocol supports a layered architectural model where the core cadastre is governed by an upstream economic framework. The canonical Market implementation enforces an escalating, forced-sale continuous auction and a cascading, bucketed upstream tax system implemented entirely on-chain.
At the object-layout level, the shared Market is a wrapper with paused: bool and inner: Versioned; the live economic state is held in MarketInner, and per-parcel quotes are resolved from PriceState { premium_ppm, sale_count } entries in MarketInner.price_states.
Forced Buyout Lifecycle:
- A user targets an existing parcel and pays the current algorithmic buyout price.
- The Market contract splits the payment into
85%seller proceeds,7%immediate treasury, and8%deferred hierarchy pool. - 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.
- The Market contract consumes its
TransferCapto invokeindex::force_transfer, reassigning ownership to the buyer. - The polygon's
premium_ppmis advanced using the aggressive resale ladder, andsale_countis 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:
where:
- registration quotes use (1.0×) as the initial premium input
- after successful registration, the stored state becomes
PriceState { premium_ppm = 2_950_000, sale_count = 1 }
Each buy_full escalates the premium via a hardcoded aggressive resale ladder:
where , 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. The donor keeps its priorpremium_ppm; the receiver is repriced to the ceiling of the area-weighted average of its old premium and the captured donor premium.sale_countis left unchanged for both parcels.rebalance_slice: Both polygons adopt the area-weighted average premium (rounded up) and .merge_owned: The surviving polygon adopts the area-weighted average premium of both inputs and .expand_unclaimed: Premium is unchanged; the buyer pays only for the added area at the current premium.
[Design intent — implementation may differ] A cleaner future economic rule may keep both parcels' premiums unchanged after acquire_slice, but the deployed code currently reprices the receiver by area-weighted blending and leaves both sale counters untouched.
All price and split calculations use u128 integer arithmetic internally before truncating back to u64.
A.1 Deferred Tax State
The production Market implementation attaches a dynamic TaxStore to the shared Market object rather than storing tax state directly in the Market 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>— deprecated field retained for Sui upgrade compatibility; always 0 in current implementation.collect_taxtransfers the owner's share directly.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 bytax_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.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: where is thebucket_id.
Each TaxLevelCfg stores:
retain_upstream_ppmcollector_fee_bpsbucket_epochsexpiry_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 paymentbuy_full(...)— forced buyout paymentexpand_unclaimed(...)— expansion payment for added areaacquire_slice(...)— slice acquisition payment
The runtime rule is:
- Compute the flow-specific payment amount from area,
price_per_km2_mist, andpremium_ppm. - Split payment into an immediate treasury portion and a deferred hierarchy pool.
- If tax is initialized and the sold level has a parent, deposit the hierarchy pool into
tax_fundand credit the corresponding child bucket viatax_accrue(...). - Otherwise route the hierarchy pool directly to treasury.
Important implementation details:
- For
buy_full(...)andacquire_slice(...), the split is85%seller proceeds,7%immediate treasury,8%deferred hierarchy pool. - For
register(...)andexpand_unclaimed(...), there is no displaced seller, so92%goes directly to treasury and8%forms the hierarchy pool. buy_full(...),register(...), andexpand_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), a bucket is created normally. The minimum applies only at cascade-forward time, not at accrual.
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 donorexpand_unclaimed(...)rebalance_slice(...)for both polygonsmerge_owned(...)for the surviving polygon
For destructive lifecycle paths, the implementation also sweeps pending child-side tax state:
remove(...)callstax_close_and_sweep(...)merge_owned(...)callstax_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 half (or an equal share at deeper hops), and forwards the remainder up the chain by opening a cascade bucket on itself for the next ancestor.
Entry point:
collect_tax(market, parent_index, parent_id, child_index, child_ids, child_bucket_ids, ctx)— permissionless flat-vector interface. Any sender can call it; the tax always transfers to the parcel owner.
Batch constraints:
- Bounded by
MAX_COLLECT_BATCH_SIZE(20). - Parent level must be directly above the child level.
Multi-match bucket lookup:
For each (child_id, bucket_id) tuple the contract finds ALL matching TaxBucketKey entries in polygon_buckets[child_id] where key.bucket_id == bucket_id and (key.origin_level_rank + key.ancestor_distance) == parent_level_rank. Version is emitted and preserved in bucket state, but it is not an input-side filter for collection. 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 three checks (failure silently skips the bucket without aborting the batch):
- Bucket exists in
tax_buckets. - 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. index::outer_contains_inner(parent, child)confirms full geometric containment.
[Design intent — implementation may differ] The tax system remains conceptually version-aware because buckets still carry version and geometry mutations still rotate version_counters. The deployed collect_tax path, however, no longer rejects collection solely because the caller omitted or supplied a stale child version; safety is enforced by bucket lookup, anti-retro, and containment.
Cascading split formula:
Given a hierarchy of levels (block → district → city → region → country → continent):
When , the collector is the top ancestor and takes the full upstream. When and , the direct parent takes 50%. For and , the collector takes — an equal share among itself and the ancestors above it. This produces zero treasury leakage for any origin rank: every MIST in the upstream pool is distributed exactly to hierarchy participants.
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 and and :
- A cascade bucket is opened on the parent polygon with
ancestor_distance = s + 1, inheritingorigin_level_rankandaccrual_epochfrom the source bucket. - If a cascade bucket with the same key already exists, the forward amount is merged into it.
open_epochis set to the current epoch;accrual_epochis inherited (never reset).- Emits
TaxBucketOpenedfor newly created cascade buckets.
Terminal forwarding: If but (top level reached), the forward is zero by construction — the top ancestor took the full upstream. If and (dust), the forward amount is added to the current collector's share rather than opening a cascade bucket.
Credits: self_keep is transferred directly to the parcel owner. Emits TaxCollected per bucket processed.
A.5 Expiry Sweep
The implementation exposes one terminal path for expired deferred balances:
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_fundto treasury. - Removes each bucket from
tax_bucketsand prunespolygon_buckets. - Emits
TaxSweptper bucket swept.
collect_tax handles both collection and immediate transfer to the parcel owner in a single atomic call. This gives the market a complete lifecycle from accrual to parent collection to treasury sweep.
A.6 Market Lifecycle Operations
Beyond buy_full, the Market 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×). Stores PriceState { premium_ppm = 2_950_000, sale_count = 1 } immediately after registration so the very next buyer sees the first resale step. 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. Tax accrues against the donor. The donor keeps its prior premium_ppm; the receiver adopts the ceiling area-weighted blend of the pre-slice receiver premium and the captured donor premium. 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 the ceiling area-weighted average premium of the two pre-repartition parcels 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 Events
Market lifecycle events:
MarketRegistered: Emitted onregister. Includespolygon_id,owner,area_m2,price_paid,immediate_treasury,hierarchy_pool.MarketPurchasedFull: Emitted onbuy_full. Includespolygon_id,buyer,seller,price,new_premium_ppm,seller_proceeds,immediate_treasury_fee,hierarchy_pool,sale_count.ParcelExpandedUnclaimed: Emitted onexpand_unclaimed. Includespolygon_id,old_area,new_area,price_paid,immediate_treasury,hierarchy_pool.ParcelSliceAcquired: Emitted onacquire_slice. Includes receiver_id, donor_id.ParcelSliceRebalanced: Emitted onrebalance_slice. Includes a_id, b_id.ParcelOwnedMerged: Emitted onmerge_owned. Includeskeep_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, self_keep, forward_upstream }— emitted per bucket collected. Onecollect_taxcall may emit multiple events when processing several child buckets.TaxWithdrawn { polygon_id, owner, amount }— emitted when tax is transferred to the parcel owner viacollect_tax.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
Tax functionality is implemented in the dedicated tax.move module. The market.move module handles core market state and lifecycle, and exposes the PTB wrappers collect_tax and sweep_expired. tax.move owns TaxStore, TaxBucket, TaxLevelCfg, tax events, and the internal collection/sweep logic called by the market wrappers with &mut UID. 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 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 Type | Vertices | Parts | Broadphase Neighbors | Gross Cost (MIST) |
|---|---|---|---|---|
| Simple Square | 4 | 1 | 0 | ~13.3M |
| Hexagon | 6 | 1 | 2 | ~17.6M |
| L-Shape | 8 | 2 | 4 | ~30.4M |
| Complex Stepped | 15 | 5 | 8 | ~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 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., vertices) while retaining on-chain grid indexing.
- Dynamic Grid Resizing: Allowing the root cell size () 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.movemodule withset_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 its cascade share, and forwards the remainder up chain via cascade buckets. Added
accrual_epochtoTaxBucketKeyto prevent merging buckets from different origination times. Anti-retro eligibility now requiresparent.created_epoch <= bucket.accrual_epoch, so later-created parents are blocked while same-epoch parents remain eligible.collect_batchprocesses all matching buckets per child ref (multi-match), and delete-after-collect replaces the oldEdgeClaimidempotency cursor. Appendix A also reflects the deployedprice_per_km2_misttariff model, aggressive resale ladder, fixed85/7/8secondary split, and92/8primary-expansion split. - v0.6.0: Added §4.7 Parcel Mutations (reshape, repartition, split, merge). Replaced legacy
sale_countprice formula with canonicalpremium_ppmmodel including partial escalation semantics. Documented all payment-bearing tax accrual points (register, buy_full, expand_unclaimed, acquire_slice). Added Market lifecycle operations (§A.6) and full event catalogue (§A.7). Expanded error code tables to cover polygon, mutations, and market modules. FixedEBadMaxDepthlimit 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.
See also: Whitepaper → 3. Protocol Overview · Architecture → Module Dependency Graph · API Reference → mercatr::signed · How It Works → Two Layers · Metadata Schema → 2. Schema Definition